Merge pull request #230 from Mygod/s

Android 12 support
This commit is contained in:
Mygod
2021-08-11 15:19:59 -04:00
committed by GitHub
54 changed files with 1888 additions and 987 deletions

288
README.md
View File

@@ -145,140 +145,180 @@ You can also use WPS to connect your 2.4GHz-only device to force the repeater to
_a.k.a. things that can go wrong if this app doesn't work._
This is a list of stuff that might impact this app's functionality if unavailable.
This is only meant to be an index. You can read more in the source code.
This is only meant to be an index.
You can read more in the source code.
API restrictions are updated up to [SHA-256 checksum `156715dfa705a048926dca876d731d72604df32e8bcac055af32866b50bc2cc8`](https://dl.google.com/developers/android/sc/non-sdk/hiddenapi-flags.csv).
Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded or implicitly used)
* [`Landroid/net/ConnectivityManager;->getLastTetherError(Ljava/lang/String;)I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#144306)
* (prior to API 30) `Landroid/net/ConnectivityManager;->getLastTetherError(Ljava/lang/String;)I,max-target-r`
* (since API 30) `Landroid/net/ConnectivityModuleConnector;->IN_PROCESS_SUFFIX:Ljava/lang/String;`
* (since API 30) [`Landroid/net/TetheringManager$TetheringEventCallback;->onTetherableInterfaceRegexpsChanged(Landroid/net/TetheringManager$TetheringInterfaceRegexps;)V,blacklist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148899)
* (since API 30) `Landroid/net/TetheringManager;->TETHERING_WIGIG:I`
* (prior to API 30) [`Landroid/net/wifi/WifiConfiguration$KeyMgmt;->FT_PSK:I,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153923)
* (prior to API 30) [`Landroid/net/wifi/WifiConfiguration$KeyMgmt;->WPA_PSK_SHA256:I,blacklist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153936)
* (since API 23, prior to API 30) [`Landroid/net/wifi/WifiConfiguration;->AP_BAND_2GHZ:I,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154057)
* (since API 23, prior to API 30) [`Landroid/net/wifi/WifiConfiguration;->AP_BAND_5GHZ:I,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154058)
* (since API 23, prior to API 30) [`Landroid/net/wifi/WifiConfiguration;->AP_BAND_ANY:I,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154059)
* (since API 23, prior to API 30) [`Landroid/net/wifi/WifiConfiguration;->apBand:I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154101)
* (since API 23, prior to API 30) [`Landroid/net/wifi/WifiConfiguration;->apChannel:I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154102)
* (since API 28, prior to API 30) [`Landroid/net/wifi/WifiManager$SoftApCallback;->onNumClientsChanged(I)V,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#132005)
* (since API 26) [`Landroid/net/wifi/WifiManager;->cancelLocalOnlyHotspotRequest()V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154947s)
* (since API 30) `Landroid/net/TetheringManager$TetheringEventCallback;->onTetherableInterfaceRegexpsChanged(Landroid/net/TetheringManager$TetheringInterfaceRegexps;)V,blocked`
* (since API 30) `Landroid/net/TetheringManager;->TETHERING_WIGIG:I,blocked`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setUserConfiguration(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,blocked`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_TYPES:[I,blocked`
* (since API 31) `Landroid/net/wifi/SoftApInfo;->getApInstanceIdentifier()Ljava/lang/String;,blocked`
* (since API 31) `Landroid/net/wifi/WifiClient;->getApInstanceIdentifier()Ljava/lang/String;,blocked`
* (prior to API 30) `Landroid/net/wifi/WifiConfiguration$KeyMgmt;->FT_PSK:I,lo-prio,max-target-o`
* (prior to API 30) `Landroid/net/wifi/WifiConfiguration$KeyMgmt;->WPA_PSK_SHA256:I,blocked`
* (since API 23, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_2GHZ:I,lo-prio,max-target-o`
* (since API 23, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_5GHZ:I,lo-prio,max-target-o`
* (since API 28, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_ANY:I,lo-prio,max-target-o`
* (since API 23, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->apBand:I,unsupported`
* (since API 23, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->apChannel:I,unsupported`
* (since API 28, prior to API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onNumClientsChanged(I)V,greylist-max-o`
* (since API 26) `Landroid/net/wifi/WifiManager;->cancelLocalOnlyHotspotRequest()V,unsupported`
* (prior to API 26) `Landroid/net/wifi/WifiManager;->setWifiApEnabled(Landroid/net/wifi/WifiConfiguration;Z)Z`
* [`Landroid/net/wifi/p2p/WifiP2pConfig$Builder;->MAC_ANY_ADDRESS:Landroid/net/MacAddress;,blacklist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#157883)
* [`Landroid/net/wifi/p2p/WifiP2pManager;->startWps(Landroid/net/wifi/p2p/WifiP2pManager$Channel;Landroid/net/wifi/WpsInfo;Landroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#158332)
* (since API 28) [`Landroid/provider/Settings$Global;->SOFT_AP_TIMEOUT_ENABLED:Ljava/lang/String;,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#183735)
* (prior to API 30) [`Lcom/android/internal/R$array;->config_tether_bluetooth_regexs:I,greylist-max-q`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#272546)
* (prior to API 30) [`Lcom/android/internal/R$array;->config_tether_usb_regexs:I,greylist-max-q`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#272549)
* (prior to API 30) [`Lcom/android/internal/R$array;->config_tether_wifi_regexs:I,greylist-max-q`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#272551)
* (since API 28, prior to API 30) [`Lcom/android/internal/R$integer;->config_wifi_framework_soft_ap_timeout_delay:I,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#245007)
* [`Lcom/android/internal/R$string;->config_ethernet_iface_regex:I,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#276573)
* `Landroid/net/wifi/p2p/WifiP2pConfig$Builder;->MAC_ANY_ADDRESS:Landroid/net/MacAddress;,blocked`
* (since API 29) `Landroid/net/wifi/p2p/WifiP2pConfig$Builder;->mNetworkName:Ljava/lang/String;,blocked`
* `Landroid/net/wifi/p2p/WifiP2pManager;->startWps(Landroid/net/wifi/p2p/WifiP2pManager$Channel;Landroid/net/wifi/WpsInfo;Landroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,max-target-r`
* (since API 28, prior to API 30) `Landroid/provider/Settings$Global;->SOFT_AP_TIMEOUT_ENABLED:Ljava/lang/String;,lo-prio,max-target-o`
* (prior to API 30) `Lcom/android/internal/R$array;->config_tether_bluetooth_regexs:I,max-target-q`
* (prior to API 30) `Lcom/android/internal/R$array;->config_tether_usb_regexs:I,max-target-q`
* (prior to API 30) `Lcom/android/internal/R$array;->config_tether_wifi_regexs:I,max-target-q`
* (on API 29) `Lcom/android/internal/R$bool;->config_wifi_p2p_mac_randomization_supported:I,blacklist`
* (since API 28, prior to API 30) `Lcom/android/internal/R$integer;->config_wifi_framework_soft_ap_timeout_delay:I,greylist-max-o`
* `Lcom/android/internal/R$string;->config_ethernet_iface_regex:I,lo-prio,max-target-o`
* (since API 27) `Lcom/android/server/connectivity/tethering/OffloadHardwareInterface;->DEFAULT_TETHER_OFFLOAD_DISABLED:I`
* (since API 30) `Lcom/android/server/wifi/WifiContext;->ACTION_RESOURCES_APK:Ljava/lang/String;`
* (since API 29) `Lcom/android/server/wifi/p2p/WifiP2pServiceImpl;->ANONYMIZED_DEVICE_ADDRESS:Ljava/lang/String;`
* (since API 30) `Lcom/android/server/SystemServer;->TETHERING_CONNECTOR_CLASS:Ljava/lang/String;`
* (since API 26) [`Ljava/lang/invoke/MethodHandles$Lookup;-><init>(Ljava/lang/Class;I)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#370415)
* (since API 26) [`Ljava/lang/invoke/MethodHandles$Lookup;->ALL_MODES:I,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#370416)
* (prior to API 29) [`Ljava/net/InetAddress;->parseNumericAddress(Ljava/lang/String;)Ljava/net/InetAddress;,core-platform-api,greylist-max-p`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#372578)
* (since API 29) `Ldalvik/system/VMRuntime;->getRuntime()Ldalvik/system/VMRuntime;,core-platform-api,unsupported`
* (since API 29) `Ldalvik/system/VMRuntime;->setHiddenApiExemptions([Ljava/lang/String;)V,blocked,core-platform-api`
* (since API 26) `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`
* (prior to API 29) `Ljava/net/InetAddress;->parseNumericAddress(Ljava/lang/String;)Ljava/net/InetAddress;,core-platform-api,max-target-p`
<details>
<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,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#35264)
* (since API 24) [`Landroid/bluetooth/BluetoothProfile;->PAN:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#35361)
* (since API 30) [`Landroid/content/Context;->TETHERING_SERVICE:Ljava/lang/String;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#41038)
* (since API 24, prior to API 30) [`Landroid/net/ConnectivityManager$OnStartTetheringCallback;-><init>()V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#144095)
* (since API 24, prior to API 30) [`Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringFailed()V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#144096)
* (since API 24, prior to API 30) [`Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringStarted()V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#144097)
* (since API 24, prior to API 30) [`Landroid/net/ConnectivityManager;->startTethering(IZLandroid/net/ConnectivityManager$OnStartTetheringCallback;Landroid/os/Handler;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#144406)
* (since API 24, prior to API 30) [`Landroid/net/ConnectivityManager;->stopTethering(I)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#144408)
* [`Landroid/net/LinkProperties;->getAllInterfaceNames()Ljava/util/List;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#146558)
* [`Landroid/net/LinkProperties;->getAllRoutes()Ljava/util/List;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#146560)
* (since API 30) [`Landroid/net/TetheringManager$StartTetheringCallback;->onTetheringFailed(I)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148881)
* (since API 30) [`Landroid/net/TetheringManager$StartTetheringCallback;->onTetheringStarted()V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148882)
* (since API 30) [`Landroid/net/TetheringManager$TetheringEventCallback;->onClientsChanged(Ljava/util/Collection;)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148896)
* (since API 30) [`Landroid/net/TetheringManager$TetheringEventCallback;->onError(Ljava/lang/String;I)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148897)
* (since API 30) [`Landroid/net/TetheringManager$TetheringEventCallback;->onOffloadStatusChanged(I)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148898)
* (since API 30) [`Landroid/net/TetheringManager$TetheringEventCallback;->onTetherableInterfacesChanged(Ljava/util/List;)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148900)
* (since API 30) [`Landroid/net/TetheringManager$TetheringEventCallback;->onTetheredInterfacesChanged(Ljava/util/List;)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148901)
* (since API 30) [`Landroid/net/TetheringManager$TetheringEventCallback;->onTetheringSupported(Z)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148902)
* (since API 30) [`Landroid/net/TetheringManager$TetheringEventCallback;->onUpstreamChanged(Landroid/net/Network;)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148903)
* (since API 30) [`Landroid/net/TetheringManager$TetheringRequest$Builder;-><init>(I)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148913)
* (since API 30) [`Landroid/net/TetheringManager$TetheringRequest$Builder;->build()Landroid/net/TetheringManager$TetheringRequest;,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148914)
* (since API 30) [`Landroid/net/TetheringManager$TetheringRequest$Builder;->setExemptFromEntitlementCheck(Z)Landroid/net/TetheringManager$TetheringRequest$Builder;,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148916)
* (since API 30) [`Landroid/net/TetheringManager$TetheringRequest$Builder;->setShouldShowEntitlementUi(Z)Landroid/net/TetheringManager$TetheringRequest$Builder;,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148917)
* [`Landroid/net/TetheringManager;->ACTION_TETHER_STATE_CHANGED:Ljava/lang/String;,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148932)
* (since API 26) [`Landroid/net/TetheringManager;->EXTRA_ACTIVE_LOCAL_ONLY:Ljava/lang/String;,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148935)
* [`Landroid/net/TetheringManager;->EXTRA_ACTIVE_TETHER:Ljava/lang/String;,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148936)
* [`Landroid/net/TetheringManager;->EXTRA_ERRORED_TETHER:Ljava/lang/String;,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148938)
* (since API 24) [`Landroid/net/TetheringManager;->TETHERING_BLUETOOTH:I,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148940)
* (since API 30) [`Landroid/net/TetheringManager;->TETHERING_ETHERNET:I,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148941)
* (since API 30) [`Landroid/net/TetheringManager;->TETHERING_NCM:I,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148943)
* (since API 24) [`Landroid/net/TetheringManager;->TETHERING_USB:I,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148944)
* (since API 24) [`Landroid/net/TetheringManager;->TETHERING_WIFI:I,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148945)
* [`Landroid/net/TetheringManager;->TETHER_ERROR_*:I,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148947)
* (since API 30) [`Landroid/net/TetheringManager;->TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION:I,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148954)
* (since API 30) [`Landroid/net/TetheringManager;->TETHER_HARDWARE_OFFLOAD_FAILED:I,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148964)
* (since API 30) [`Landroid/net/TetheringManager;->TETHER_HARDWARE_OFFLOAD_STARTED:I,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148965)
* (since API 30) [`Landroid/net/TetheringManager;->TETHER_HARDWARE_OFFLOAD_STOPPED:I,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148966)
* (since API 30) [`Landroid/net/TetheringManager;->registerTetheringEventCallback(Ljava/util/concurrent/Executor;Landroid/net/TetheringManager$TetheringEventCallback;)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#149003)
* (since API 30) [`Landroid/net/TetheringManager;->startTethering(Landroid/net/TetheringManager$TetheringRequest;Ljava/util/concurrent/Executor;Landroid/net/TetheringManager$StartTetheringCallback;)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#149009)
* (since API 30) [`Landroid/net/TetheringManager;->stopTethering(I)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#149011)
* (since API 30) [`Landroid/net/TetheringManager;->unregisterTetheringEventCallback(Landroid/net/TetheringManager$TetheringEventCallback;)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#149014)
* (since API 30) [`Landroid/net/wifi/SoftApCapability;->SOFTAP_FEATURE_*:J,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153656)
* (since API 30) [`Landroid/net/wifi/SoftApCapability;->areFeaturesSupported(J)Z,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153660)
* (since API 30) [`Landroid/net/wifi/SoftApCapability;->getMaxSupportedClients()I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153663)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;-><init>()V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153707)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;-><init>(Landroid/net/wifi/SoftApConfiguration;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153708)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->build()Landroid/net/wifi/SoftApConfiguration;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153709)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setAllowedClientList(Ljava/util/List;)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153723)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setAutoShutdownEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153724)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setBand(I)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153725)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setBlockedClientList(Ljava/util/List;)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153726)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setBssid(Landroid/net/MacAddress;)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153727)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setChannel(II)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153728)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setClientControlByUserEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153729)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setHiddenSsid(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153730)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setMaxNumberOfClients(I)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153731)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setPassphrase(Ljava/lang/String;I)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153732)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setShutdownTimeoutMillis(J)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153733)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setSsid(Ljava/lang/String;)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153734)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->BAND_2GHZ:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153738)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->BAND_5GHZ:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153739)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->BAND_6GHZ:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153740)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->BAND_ANY:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153741)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->getAllowedClientList()Ljava/util/List;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153773)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->getBand()I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153774)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->getBlockedClientList()Ljava/util/List;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153775)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->getChannel()I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153777)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->getMaxNumberOfClients()I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153778)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->getShutdownTimeoutMillis()J,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153781)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->isAutoShutdownEnabled()Z,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153784)
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->isClientControlByUserEnabled()Z,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153787)
* (since API 30) [`Landroid/net/wifi/SoftApInfo;->CHANNEL_WIDTH_*:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153813)
* (since API 30) [`Landroid/net/wifi/SoftApInfo;->CHANNEL_WIDTH_INVALID:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153819)
* (since API 30) [`Landroid/net/wifi/SoftApInfo;->getBandwidth()I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153825)
* (since API 30) [`Landroid/net/wifi/SoftApInfo;->getFrequency()I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153826)
* (since API 30) [`Landroid/net/wifi/WifiClient;->getMacAddress()Landroid/net/MacAddress;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153881)
* (prior to API 30) [`Landroid/net/wifi/WifiConfiguration$KeyMgmt;->WPA2_PSK:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153932)
* (since API 30) [`Landroid/net/wifi/WifiManager$SoftApCallback;->onBlockedClientConnecting(Landroid/net/wifi/WifiClient;I)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154681)
* (since API 30) [`Landroid/net/wifi/WifiManager$SoftApCallback;->onCapabilityChanged(Landroid/net/wifi/SoftApCapability;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154682)
* (since API 30) [`Landroid/net/wifi/WifiManager$SoftApCallback;->onConnectedClientsChanged(Ljava/util/List;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154683)
* (since API 30) [`Landroid/net/wifi/WifiManager$SoftApCallback;->onInfoChanged(Landroid/net/wifi/SoftApInfo;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154684)
* (since API 28) [`Landroid/net/wifi/WifiManager$SoftApCallback;->onStateChanged(II)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154685)
* (since API 30) [`Landroid/net/wifi/WifiManager;->SAP_CLIENT_BLOCK_REASON_CODE_*:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154840)
* (since API 28) [`Landroid/net/wifi/WifiManager;->SAP_START_FAILURE_*:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154843)
* (since API 28) [`Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_FAILED:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154868)
* (since API 30) [`Landroid/net/wifi/WifiManager;->getSoftApConfiguration()Landroid/net/wifi/SoftApConfiguration;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154985)
* (prior to API 30) [`Landroid/net/wifi/WifiManager;->getWifiApConfiguration()Landroid/net/wifi/WifiConfiguration;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154989)
* (since API 28) [`Landroid/net/wifi/WifiManager;->registerSoftApCallback(Ljava/util/concurrent/Executor;Landroid/net/wifi/WifiManager$SoftApCallback;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#155042)
* (since API 30) [`Landroid/net/wifi/WifiManager;->setSoftApConfiguration(Landroid/net/wifi/SoftApConfiguration;)Z,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#155063)
* (prior to API 30) [`Landroid/net/wifi/WifiManager;->setWifiApConfiguration(Landroid/net/wifi/WifiConfiguration;)Z,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#155067)
* (since API 28) [`Landroid/net/wifi/WifiManager;->unregisterSoftApCallback(Landroid/net/wifi/WifiManager$SoftApCallback;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#155087)
* [`Landroid/net/wifi/p2p/WifiP2pGroupList;->getGroupList()Ljava/util/List;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#158079)
* [`Landroid/net/wifi/p2p/WifiP2pManager$PersistentGroupInfoListener;->onPersistentGroupInfoAvailable(Landroid/net/wifi/p2p/WifiP2pGroupList;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#158156)
* [`Landroid/net/wifi/p2p/WifiP2pManager;->deletePersistentGroup(Landroid/net/wifi/p2p/WifiP2pManager$Channel;ILandroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#158296)
* [`Landroid/net/wifi/p2p/WifiP2pManager;->requestPersistentGroupInfo(Landroid/net/wifi/p2p/WifiP2pManager$Channel;Landroid/net/wifi/p2p/WifiP2pManager$PersistentGroupInfoListener;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#158320)
* [`Landroid/net/wifi/p2p/WifiP2pManager;->setWifiP2pChannels(Landroid/net/wifi/p2p/WifiP2pManager$Channel;IILandroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#158330)
* [`Landroid/provider/Settings$Global;->TETHER_OFFLOAD_DISABLED:Ljava/lang/String;,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#183757)
* (since API 24) `Landroid/bluetooth/BluetoothPan;->isTetheringOn()Z,sdk,system-api,test-api`
* (since API 24) `Landroid/bluetooth/BluetoothProfile;->PAN:I,sdk,system-api,test-api`
* (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`
* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringFailed()V,sdk,system-api,test-api`
* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringStarted()V,sdk,system-api,test-api`
* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager;->startTethering(IZLandroid/net/ConnectivityManager$OnStartTetheringCallback;Landroid/os/Handler;)V,sdk,system-api,test-api`
* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager;->stopTethering(I)V,sdk,system-api,test-api`
* `Landroid/net/LinkProperties;->getAllInterfaceNames()Ljava/util/List;,sdk,system-api,test-api`
* `Landroid/net/LinkProperties;->getAllRoutes()Ljava/util/List;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager$StartTetheringCallback;->onTetheringFailed(I)V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager$StartTetheringCallback;->onTetheringStarted()V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager$TetheringEventCallback;->onClientsChanged(Ljava/util/Collection;)V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager$TetheringEventCallback;->onError(Ljava/lang/String;I)V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager$TetheringEventCallback;->onOffloadStatusChanged(I)V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager$TetheringEventCallback;->onTetherableInterfacesChanged(Ljava/util/List;)V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager$TetheringEventCallback;->onTetheredInterfacesChanged(Ljava/util/List;)V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager$TetheringEventCallback;->onTetheringSupported(Z)V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager$TetheringEventCallback;->onUpstreamChanged(Landroid/net/Network;)V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager$TetheringRequest$Builder;-><init>(I)V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager$TetheringRequest$Builder;->build()Landroid/net/TetheringManager$TetheringRequest;,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`
* `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_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`
* (since API 30) `Landroid/net/TetheringManager;->TETHERING_ETHERNET:I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager;->TETHERING_NCM:I,sdk,system-api,test-api`
* (since API 24) `Landroid/net/TetheringManager;->TETHERING_USB:I,sdk,system-api,test-api`
* (since API 24) `Landroid/net/TetheringManager;->TETHERING_WIFI:I,sdk,system-api,test-api`
* `Landroid/net/TetheringManager;->TETHER_ERROR_*:I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager;->TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION:I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager;->TETHER_HARDWARE_OFFLOAD_FAILED:I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager;->TETHER_HARDWARE_OFFLOAD_STARTED:I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager;->TETHER_HARDWARE_OFFLOAD_STOPPED:I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager;->registerTetheringEventCallback(Ljava/util/concurrent/Executor;Landroid/net/TetheringManager$TetheringEventCallback;)V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager;->startTethering(Landroid/net/TetheringManager$TetheringRequest;Ljava/util/concurrent/Executor;Landroid/net/TetheringManager$StartTetheringCallback;)V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager;->stopTethering(I)V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/TetheringManager;->unregisterTetheringEventCallback(Landroid/net/TetheringManager$TetheringEventCallback;)V,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApCapability;->SOFTAP_FEATURE_BAND_24G_SUPPORTED:J,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApCapability;->SOFTAP_FEATURE_BAND_5G_SUPPORTED:J,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApCapability;->SOFTAP_FEATURE_BAND_60G_SUPPORTED:J,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApCapability;->SOFTAP_FEATURE_BAND_6G_SUPPORTED:J,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApCapability;->SOFTAP_FEATURE_*:J,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApCapability;->areFeaturesSupported(J)Z,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApCapability;->getMaxSupportedClients()I,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApCapability;->getSupportedChannelList(I)[I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;-><init>()V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;-><init>(Landroid/net/wifi/SoftApConfiguration;)V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->build()Landroid/net/wifi/SoftApConfiguration;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setAllowedClientList(Ljava/util/List;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setAutoShutdownEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (on API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBand(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBlockedClientList(Ljava/util/List;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBridgedModeOpportunisticShutdownEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBssid(Landroid/net/MacAddress;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (on API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setChannel(II)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setChannels(Landroid/util/SparseIntArray;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setClientControlByUserEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setHiddenSsid(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setIeee80211axEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setMacRandomizationSetting(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setMaxNumberOfClients(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setPassphrase(Ljava/lang/String;I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setShutdownTimeoutMillis(J)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setSsid(Ljava/lang/String;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->BAND_2GHZ:I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->BAND_5GHZ:I,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_60GHZ:I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->BAND_6GHZ:I,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_*:I,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->RANDOMIZATION_NONE:I,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->RANDOMIZATION_PERSISTENT:I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getAllowedClientList()Ljava/util/List;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getBand()I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getBlockedClientList()Ljava/util/List;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getChannel()I,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->getChannels()Landroid/util/SparseIntArray;,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->getMacRandomizationSetting()I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getMaxNumberOfClients()I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getShutdownTimeoutMillis()J,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->isAutoShutdownEnabled()Z,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->isBridgedModeOpportunisticShutdownEnabled()Z,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->isClientControlByUserEnabled()Z,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->isIeee80211axEnabled()Z,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->isUserConfiguration()Z,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApInfo;->CHANNEL_WIDTH_*:I,sdk,system-api,test-api`
* (on API 30) `Landroid/net/wifi/SoftApInfo;->CHANNEL_WIDTH_INVALID:I,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApInfo;->getAutoShutdownTimeoutMillis()J,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApInfo;->getBandwidth()I,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApInfo;->getBssid()Landroid/net/MacAddress;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApInfo;->getFrequency()I,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApInfo;->getWifiStandard()I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/WifiClient;->getMacAddress()Landroid/net/MacAddress;,sdk,system-api,test-api`
* (prior to API 30) `Landroid/net/wifi/WifiConfiguration$KeyMgmt;->WPA2_PSK:I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onBlockedClientConnecting(Landroid/net/wifi/WifiClient;I)V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onCapabilityChanged(Landroid/net/wifi/SoftApCapability;)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`
* (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`
* (since API 23) `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`
* (since API 23) `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 23) `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`
* (since API 23) `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`
* (since API 23) `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`
* (since API 23) `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_FAILED:I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/WifiManager;->getSoftApConfiguration()Landroid/net/wifi/SoftApConfiguration;,sdk,system-api,test-api`
* (prior to API 30) `Landroid/net/wifi/WifiManager;->getWifiApConfiguration()Landroid/net/wifi/WifiConfiguration;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/WifiManager;->isApMacRandomizationSupported()Z,sdk,system-api,test-api`
* (since API 28) `Landroid/net/wifi/WifiManager;->registerSoftApCallback(Ljava/util/concurrent/Executor;Landroid/net/wifi/WifiManager$SoftApCallback;)V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/WifiManager;->setSoftApConfiguration(Landroid/net/wifi/SoftApConfiguration;)Z,sdk,system-api,test-api`
* (prior to API 30) `Landroid/net/wifi/WifiManager;->setWifiApConfiguration(Landroid/net/wifi/WifiConfiguration;)Z,sdk,system-api,test-api`
* (since API 28) `Landroid/net/wifi/WifiManager;->unregisterSoftApCallback(Landroid/net/wifi/WifiManager$SoftApCallback;)V,sdk,system-api,test-api`
* `Landroid/net/wifi/p2p/WifiP2pGroupList;->getGroupList()Ljava/util/List;,sdk,system-api,test-api`
* `Landroid/net/wifi/p2p/WifiP2pManager$PersistentGroupInfoListener;->onPersistentGroupInfoAvailable(Landroid/net/wifi/p2p/WifiP2pGroupList;)V,sdk,system-api,test-api`
* `Landroid/net/wifi/p2p/WifiP2pManager;->deletePersistentGroup(Landroid/net/wifi/p2p/WifiP2pManager$Channel;ILandroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,sdk,system-api,test-api`
* `Landroid/net/wifi/p2p/WifiP2pManager;->requestPersistentGroupInfo(Landroid/net/wifi/p2p/WifiP2pManager$Channel;Landroid/net/wifi/p2p/WifiP2pManager$PersistentGroupInfoListener;)V,sdk,system-api,test-api`
* `Landroid/net/wifi/p2p/WifiP2pManager;->setWifiP2pChannels(Landroid/net/wifi/p2p/WifiP2pManager$Channel;IILandroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,sdk,system-api,test-api`
* `Landroid/provider/Settings$Global;->TETHER_OFFLOAD_DISABLED:Ljava/lang/String;,sdk,system-api,test-api`
</details>
@@ -290,12 +330,10 @@ Nonexported system resources:
* (since API 30) `@com.android.networkstack.tethering:array/config_tether_wifi_p2p_regexs`
* (since API 30) `@com.android.networkstack.tethering:array/config_tether_wifi_regexs`
* (since API 30) `@com.android.networkstack.tethering:array/config_tether_wigig_regexs`
* (since API 30) `@com.android.wifi.resources:bool/config_wifi_p2p_mac_randomization_supported`
* (since API 30) `@com.android.wifi.resources:integer/config_wifiFrameworkSoftApShutDownTimeoutMilliseconds`
Other:
* (since API 29) `android.net.wifi.p2p.WifiP2pConfig` needs to be parcelized in a very specific order, except for possible extra fields at the end. (used only for safe mode)
* 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;
(prior to API 24) `RULE_PRIORITY_DEFAULT_NETWORK` is assumed to be 22000 (or at least > 18000).

View File

@@ -9,8 +9,8 @@ buildscript {
}
dependencies {
classpath(kotlin("gradle-plugin", "1.5.20"))
classpath("com.android.tools.build:gradle:7.0.0-beta05")
classpath(kotlin("gradle-plugin", "1.5.21"))
classpath("com.android.tools.build:gradle:7.0.0")
classpath("com.google.firebase:firebase-crashlytics-gradle:2.7.1")
classpath("com.google.android.gms:oss-licenses-plugin:0.10.4")
classpath("com.google.gms:google-services:4.3.8")

View File

@@ -9,23 +9,23 @@ plugins {
}
android {
val javaVersion = JavaVersion.VERSION_1_8
val javaVersion = JavaVersion.VERSION_11
val targetSdk = 29
buildToolsVersion = "30.0.3"
buildToolsVersion = "31.0.0"
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = javaVersion
targetCompatibility = javaVersion
}
compileSdk = 30
compileSdk = 31
kotlinOptions.jvmTarget = javaVersion.toString()
defaultConfig {
applicationId = "be.mygod.vpnhotspot"
minSdk = 21
this.targetSdk = targetSdk
resourceConfigurations.addAll(arrayOf("it", "ru", "zh-rCN", "zh-rTW"))
versionCode = 262
versionName = "2.11.9"
versionCode = 277
versionName = "2.12.6"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions.annotationProcessorOptions.arguments.apply {
put("room.expandProjection", "true")
@@ -70,11 +70,11 @@ dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
kapt("androidx.room:room-compiler:$roomVersion")
implementation(kotlin("stdlib-jdk8"))
implementation("androidx.appcompat:appcompat:1.3.0") // https://issuetracker.google.com/issues/151603528
implementation("androidx.appcompat:appcompat:1.3.1") // https://issuetracker.google.com/issues/151603528
implementation("androidx.browser:browser:1.3.0")
implementation("androidx.core:core-ktx:1.6.0")
implementation("androidx.emoji:emoji:1.1.0")
implementation("androidx.fragment:fragment-ktx:1.3.5")
implementation("androidx.fragment:fragment-ktx:1.3.6")
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
@@ -83,9 +83,9 @@ dependencies {
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("com.android.billingclient:billing-ktx:4.0.0")
implementation("com.google.android.gms:play-services-oss-licenses:17.0.0")
implementation("com.google.android.material:material:1.4.0")
implementation("com.google.android.material:material:1.5.0-alpha01")
implementation("com.google.firebase:firebase-analytics-ktx:19.0.0")
implementation("com.google.firebase:firebase-crashlytics:18.1.0")
implementation("com.google.firebase:firebase-crashlytics:18.2.0")
implementation("com.google.zxing:core:3.4.1")
implementation("com.jakewharton.timber:timber:4.7.1")
implementation("com.linkedin.dexmaker:dexmaker:2.28.1")

View File

@@ -9,9 +9,6 @@
<uses-feature
android:name="android.hardware.ethernet"
android:required="false"/>
<uses-feature
android:name="android.software.leanback"
android:required="false"/>
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false"/>
@@ -24,6 +21,9 @@
<uses-feature
android:name="android.hardware.wifi.direct"
android:required="false"/>
<uses-feature
android:name="android.software.leanback"
android:required="false"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
@@ -52,12 +52,15 @@
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- Required since API 29 -->
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!-- Required since API 31 -->
<!-- Required since API 31, when targeting API 31 -->
<!--
<uses-permission-sdk-23 android:name="android.permission.BLUETOOTH_CONNECT"/>
-->
<application
android:name=".App"
android:allowBackup="false"
android:dataExtractionRules="@xml/no_backup"
android:label="@string/app_name"
android:banner="@mipmap/banner"
android:hasFragileUserData="true"
@@ -69,6 +72,7 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTask"
android:windowSoftInputMode="stateAlwaysHidden">
@@ -100,6 +104,7 @@
<service
android:name=".manage.RepeaterTileService"
android:directBootAware="true"
android:exported="true"
android:icon="@drawable/ic_action_settings_input_antenna"
android:label="@string/title_repeater"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
@@ -115,6 +120,7 @@
android:name=".manage.LocalOnlyHotspotTileService"
android:directBootAware="true"
android:enabled="@bool/api_ge_26"
android:exported="true"
android:icon="@drawable/ic_action_perm_scan_wifi"
android:label="@string/tethering_temp_hotspot"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
@@ -129,6 +135,7 @@
<service
android:name=".manage.TetheringTileService$Wifi"
android:directBootAware="true"
android:exported="true"
android:icon="@drawable/ic_device_wifi_tethering"
android:label="@string/tethering_manage_wifi"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
@@ -136,10 +143,14 @@
<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>
<service
android:name=".manage.TetheringTileService$Usb"
android:directBootAware="true"
android:exported="true"
android:icon="@drawable/ic_device_usb"
android:label="@string/tethering_manage_usb"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
@@ -147,10 +158,14 @@
<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>
<service
android:name=".manage.TetheringTileService$Bluetooth"
android:directBootAware="true"
android:exported="true"
android:icon="@drawable/ic_device_bluetooth"
android:label="@string/tethering_manage_bluetooth"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
@@ -158,11 +173,15 @@
<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>
<service
android:name=".manage.TetheringTileService$Ethernet"
android:directBootAware="true"
android:enabled="@bool/api_ge_30"
android:exported="true"
android:icon="@drawable/ic_content_inbox"
android:label="@string/tethering_manage_ethernet"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
@@ -170,11 +189,15 @@
<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>
<service
android:name=".manage.TetheringTileService$Ncm"
android:directBootAware="true"
android:enabled="@bool/api_ge_30"
android:exported="true"
android:icon="@drawable/ic_action_settings_ethernet"
android:label="@string/tethering_manage_ncm"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
@@ -182,11 +205,15 @@
<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>
<service
android:name=".manage.TetheringTileService$WiGig"
android:directBootAware="true"
android:enabled="@bool/api_ge_30"
android:exported="true"
android:icon="@drawable/ic_image_flash_on"
android:label="@string/tethering_manage_wigig"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
@@ -194,12 +221,16 @@
<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>
<!--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"
@@ -207,12 +238,16 @@
<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
android:name=".BootReceiver"
android:directBootAware="true"
android:enabled="false">
android:enabled="false"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />

View File

@@ -8,7 +8,6 @@ import android.os.RemoteException
import android.system.Os
import android.system.OsConstants
import androidx.collection.LongSparseArray
import androidx.collection.set
import androidx.collection.valueIterator
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
@@ -102,6 +101,7 @@ class RootServer {
private fun readUnexpectedStderr(): String? {
if (!this::process.isInitialized) return null
Logger.me.d("Attempting to read stderr")
var available = process.errorStream.available()
return if (available <= 0) null else String(ByteArrayOutputStream().apply {
try {
@@ -146,7 +146,9 @@ class RootServer {
try {
val token2 = UUID.randomUUID().toString()
val persistence = File(context.codeCacheDir, ".librootkotlinx-uuid")
val uuid = context.packageName + '@' + if (persistence.canRead()) persistence.readText() else {
val uuid = context.packageName + '@' + try {
persistence.readText()
} catch (_: FileNotFoundException) {
UUID.randomUUID().toString().also { persistence.writeText(it) }
}
val (script, relocated) = AppProcess.relocateScript(uuid)
@@ -218,9 +220,9 @@ class RootServer {
throw e
} finally {
Logger.me.d("Waiting for exit")
errorReader.await()
withContext(NonCancellable) { errorReader.await() }
process.waitFor()
withContext(NonCancellable) { closeInternal(true) }
closeInternal(true)
}
}
}
@@ -246,7 +248,7 @@ class RootServer {
@Suppress("UNCHECKED_CAST")
val callback = Callback.Ordinary(this, counter, classLoader, future as CompletableDeferred<Parcelable?>)
if (active) {
callbackLookup[counter] = callback
callbackLookup.append(counter, callback)
sendLocked(command)
} else future.cancel()
callback
@@ -277,7 +279,7 @@ class RootServer {
@Suppress("UNCHECKED_CAST")
val callback = Callback.Channel(this@RootServer, counter, classLoader, this as SendChannel<Parcelable?>)
if (active) {
callbackLookup[counter] = callback
callbackLookup.append(counter, callback)
sendLocked(command)
} else callback.finish.cancel()
callback
@@ -290,7 +292,7 @@ class RootServer {
}
}
private suspend fun closeInternal(fromWorker: Boolean = false) = synchronized(callbackLookup) {
private fun closeInternal(fromWorker: Boolean = false) = synchronized(callbackLookup) {
if (active) {
active = false
Logger.me.d(if (fromWorker) "Shutting down from worker" else "Shutting down from client")
@@ -432,7 +434,7 @@ class RootServer {
}
is RootCommand<*> -> {
val commandJob = Job()
cancellables[callback] = { commandJob.cancel() }
cancellables.append(callback) { commandJob.cancel() }
defaultWorker.launch(commandJob) {
val result = try {
val result = command.execute();
@@ -450,7 +452,7 @@ class RootServer {
val result = try {
coroutineScope {
command.create(this).also {
cancellables[callback] = { it.cancel() }
cancellables.append(callback) { it.cancel() }
}.consumeEach { result ->
withContext(callbackWorker) { output.pushResult(callback, result) }
}

View File

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

View File

@@ -62,6 +62,7 @@ class App : Application() {
}
}
Timber.plant(object : Timber.DebugTree() {
@SuppressLint("LogNotTimber")
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
if (t == null) {
if (priority != Log.DEBUG || BuildConfig.DEBUG) Log.println(priority, tag, message)

View File

@@ -6,16 +6,13 @@ import android.net.wifi.WifiManager
import android.os.Build
import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.net.wifi.WifiApManager.wifiApState
import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.StickyEvent1
import be.mygod.vpnhotspot.util.broadcastReceiver
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.*
import timber.log.Timber
@@ -43,7 +40,8 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
null -> return // stopped
"" -> WifiApManager.cancelLocalOnlyHotspotRequest()
}
reservation?.close() ?: stopService()
reservation?.close()
stopService()
}
}
@@ -56,24 +54,6 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
override val coroutineContext = dispatcher + Job()
private var routingManager: RoutingManager? = null
private var timeoutMonitor: TetherTimeoutMonitor? = null
private var receiverRegistered = false
private val receiver = broadcastReceiver { _, intent ->
val ifaces = (intent.localOnlyTetheredIfaces ?: return@broadcastReceiver).filter {
TetherType.ofInterface(it) != TetherType.WIFI_P2P
}
Timber.d("onTetherStateChangedLocked: $ifaces")
check(ifaces.size <= 1)
val iface = ifaces.singleOrNull()
binder.iface = iface
if (iface.isNullOrEmpty()) stopService() else launch {
val routingManager = routingManager
if (routingManager == null) {
this@LocalOnlyHotspotService.routingManager = RoutingManager.LocalOnly(this@LocalOnlyHotspotService,
iface).apply { start() }
IpNeighbourMonitor.registerCallback(this@LocalOnlyHotspotService)
} else check(iface == routingManager.downstream)
}
}
override val activeIfaces get() = binder.iface.let { if (it.isNullOrEmpty()) emptyList() else listOf(it) }
override fun onBind(intent: Intent?) = binder
@@ -87,20 +67,36 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) {
if (reservation == null) onFailed(-2) else {
this@LocalOnlyHotspotService.reservation = reservation
if (!receiverRegistered) {
val configuration = binder.configuration!!
if (Build.VERSION.SDK_INT < 30 && configuration.isAutoShutdownEnabled) {
timeoutMonitor = TetherTimeoutMonitor(configuration.shutdownTimeoutMillis,
coroutineContext) { reservation.close() }
}
registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
receiverRegistered = true
// based on: https://android.googlesource.com/platform/packages/services/Car/+/df5cd06/service/src/com/android/car/CarProjectionService.java#160
val sticky = registerReceiver(null, IntentFilter(WifiApManager.WIFI_AP_STATE_CHANGED_ACTION))!!
val apState = sticky.wifiApState
val iface = sticky.getStringExtra(WifiApManager.EXTRA_WIFI_AP_INTERFACE_NAME)
if (apState != WifiApManager.WIFI_AP_STATE_ENABLED || iface.isNullOrEmpty()) {
if (apState == WifiApManager.WIFI_AP_STATE_FAILED) {
SmartSnackbar.make(getString(R.string.tethering_temp_hotspot_failure,
WifiApManager.failureReasonLookup(sticky.getIntExtra(
WifiApManager.EXTRA_WIFI_AP_FAILURE_REASON, 0)))).show()
}
return stopService()
}
binder.iface = iface
launch {
check(routingManager == null)
routingManager = RoutingManager.LocalOnly(
this@LocalOnlyHotspotService, iface).apply { start() }
IpNeighbourMonitor.registerCallback(this@LocalOnlyHotspotService)
}
}
}
override fun onStopped() {
Timber.d("LOHCallback.onStopped")
reservation?.close()
reservation = null
}
@@ -152,15 +148,11 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
}
private fun unregisterReceiver(exit: Boolean = false) {
if (receiverRegistered) {
unregisterReceiver(receiver)
IpNeighbourMonitor.unregisterCallback(this)
if (Build.VERSION.SDK_INT >= 28) {
timeoutMonitor?.close()
timeoutMonitor = null
}
receiverRegistered = false
}
launch {
routingManager?.stop()
routingManager = null

View File

@@ -15,7 +15,6 @@ import android.provider.Settings
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.core.content.edit
import be.mygod.librootkotlinx.useParcel
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
@@ -53,14 +52,10 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
private const val KEY_AUTO_SHUTDOWN = "service.repeater.autoShutdown"
private const val KEY_SHUTDOWN_TIMEOUT = "service.repeater.shutdownTimeout"
private const val KEY_DEVICE_ADDRESS = "service.repeater.mac"
/**
* Placeholder for bypassing networkName check.
*/
private const val PLACEHOLDER_NETWORK_NAME = "DIRECT-00-VPNHotspot"
var persistentSupported = false
@delegate:TargetApi(29)
@get:RequiresApi(29)
private val hasP2pValidateName by lazy {
val array = Build.VERSION.SECURITY_PATCH.split('-', limit = 3)
val y = array.getOrNull(0)?.toIntOrNull()
@@ -70,6 +65,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
val safeModeConfigurable get() = Build.VERSION.SDK_INT >= 29 && hasP2pValidateName
val safeMode get() = Build.VERSION.SDK_INT >= 29 &&
(!hasP2pValidateName || app.pref.getBoolean(KEY_SAFE_MODE, true))
private val mNetworkName by lazy { UnblockCentral.WifiP2pConfig_Builder_mNetworkName }
var networkName: String?
get() = app.pref.getString(KEY_NETWORK_NAME, null)
@@ -79,7 +75,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
set(value) = app.pref.edit { putString(KEY_PASSPHRASE, value) }
var operatingBand: Int
@SuppressLint("InlinedApi")
get() = app.pref.getInt(KEY_OPERATING_BAND, SoftApConfigurationCompat.BAND_ANY)
get() = app.pref.getInt(KEY_OPERATING_BAND, SoftApConfigurationCompat.BAND_LEGACY)
set(value) = app.pref.edit { putInt(KEY_OPERATING_BAND, value) }
var operatingChannel: Int
get() {
@@ -398,59 +394,39 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
}
val networkName = networkName
val passphrase = passphrase
try {
@SuppressLint("MissingPermission") // missing permission will simply leading to returning ERROR
if (!safeMode || networkName == null || passphrase == null) {
persistNextGroup = true
p2pManager.createGroup(channel, listener)
} else @TargetApi(29) {
p2pManager.createGroup(channel, WifiP2pConfig.Builder().apply {
setNetworkName(PLACEHOLDER_NETWORK_NAME)
try {
mNetworkName.set(this, networkName) // bypass networkName check
} catch (e: ReflectiveOperationException) {
Timber.w(e)
try {
setNetworkName(networkName)
} catch (e: IllegalArgumentException) {
Timber.w(e)
return startFailure(e.readableMessage)
}
}
setPassphrase(passphrase)
when (val oc = operatingChannel) {
0 -> setGroupOperatingBand(when (val band = operatingBand) {
SoftApConfigurationCompat.BAND_ANY -> WifiP2pConfig.GROUP_OWNER_BAND_AUTO
SoftApConfigurationCompat.BAND_2GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_2GHZ
SoftApConfigurationCompat.BAND_5GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_5GHZ
else -> throw IllegalArgumentException("Unknown band $band")
else -> {
require(SoftApConfigurationCompat.isLegacyEitherBand(band)) { "Unknown band $band" }
WifiP2pConfig.GROUP_OWNER_BAND_AUTO
}
})
else -> {
setGroupOperatingFrequency(SoftApConfigurationCompat.channelToFrequency(operatingBand, oc))
}
}
setDeviceAddress(deviceAddress?.toPlatform())
}.build().run {
useParcel { p ->
p.writeParcelable(this, 0)
val end = p.dataPosition()
p.setDataPosition(0)
val creator = p.readString()
val deviceAddress = p.readString()
val wps = p.readParcelable<WpsInfo>(javaClass.classLoader)
val long = p.readLong()
check(p.readString() == PLACEHOLDER_NETWORK_NAME)
check(p.readString() == passphrase)
val extrasLength = end - p.dataPosition()
check(extrasLength and 3 == 0) // parcel should be padded
if (extrasLength != 4) app.logEvent("p2p_config_extras_unexpected_length") {
param("length", extrasLength.toLong())
}
val extras = (0 until extrasLength / 4).map { p.readInt() }
p.setDataPosition(0)
p.writeString(creator)
p.writeString(deviceAddress)
p.writeParcelable(wps, 0)
p.writeLong(long)
p.writeString(networkName)
p.writeString(passphrase)
extras.forEach(p::writeInt)
p.setDataPosition(0)
p.readParcelable(javaClass.classLoader)
}
}, listener)
}
} catch (e: SecurityException) {
Timber.w(e)
startFailure(e.readableMessage)
}.build(), listener)
}
}
/**
@@ -512,7 +488,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
if (reason != WifiP2pManager.BUSY) {
SmartSnackbar.make(formatReason(R.string.repeater_remove_group_failure, reason)).show()
} // else assuming it's already gone
launch { cleanLocked() }
onSuccess()
}
})
}

View File

@@ -12,25 +12,29 @@ import be.mygod.vpnhotspot.App.Companion.app
import java.util.*
object ServiceNotification {
private const val CHANNEL = "tethering"
private const val CHANNEL_ID = 1
private const val CHANNEL_ACTIVE = "tethering"
private const val CHANNEL_INACTIVE = "tethering-inactive"
private const val NOTIFICATION_ID = 1
private val deviceCountsMap = WeakHashMap<Service, Map<String, Int>>()
private val inactiveMap = WeakHashMap<Service, List<String>>()
private val manager = app.getSystemService<NotificationManager>()!!
private fun buildNotification(context: Context): Notification {
val builder = NotificationCompat.Builder(context, CHANNEL)
.setWhen(0)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setColor(ContextCompat.getColor(context, R.color.colorPrimary))
.setContentTitle(context.getText(R.string.notification_tethering_title))
.setSmallIcon(R.drawable.ic_quick_settings_tile_on)
.setContentIntent(PendingIntent.getActivity(context, 0,
Intent(context, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
val deviceCounts = deviceCountsMap.values.flatMap { it.entries }.sortedBy { it.key }
val inactive = inactiveMap.values.flatten()
val isInactive = inactive.isNotEmpty() && deviceCounts.isEmpty()
val builder = NotificationCompat.Builder(context, if (isInactive) CHANNEL_INACTIVE else CHANNEL_ACTIVE).apply {
setWhen(0)
setCategory(NotificationCompat.CATEGORY_SERVICE)
color = ContextCompat.getColor(context, R.color.colorPrimary)
setContentTitle(context.getText(R.string.notification_tethering_title))
setSmallIcon(R.drawable.ic_quick_settings_tile_on)
setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
priority = if (isInactive) NotificationCompat.PRIORITY_MIN else NotificationCompat.PRIORITY_LOW
}
var lines = deviceCounts.map { (dev, size) ->
context.resources.getQuantityString(R.plurals.notification_connected_devices, size, size, dev)
}
@@ -54,23 +58,28 @@ object ServiceNotification {
synchronized(this) {
deviceCountsMap[service] = deviceCounts
if (inactive.isEmpty()) inactiveMap.remove(service) else inactiveMap[service] = inactive
service.startForeground(CHANNEL_ID, buildNotification(service))
service.startForeground(NOTIFICATION_ID, buildNotification(service))
}
}
fun stopForeground(service: Service) = synchronized(this) {
deviceCountsMap.remove(service)
if (deviceCountsMap.isEmpty()) service.stopForeground(true) else {
service.stopForeground(false)
manager.notify(CHANNEL_ID, buildNotification(service))
}
deviceCountsMap.remove(service) ?: return@synchronized
val shutdown = deviceCountsMap.isEmpty()
service.stopForeground(shutdown)
if (!shutdown) manager.notify(NOTIFICATION_ID, buildNotification(service))
}
fun updateNotificationChannels() {
if (Build.VERSION.SDK_INT >= 26) @TargetApi(26) {
val tethering = NotificationChannel(CHANNEL,
app.getText(R.string.notification_channel_tethering), NotificationManager.IMPORTANCE_LOW)
tethering.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
manager.createNotificationChannel(tethering)
NotificationChannel(CHANNEL_ACTIVE,
app.getText(R.string.notification_channel_tethering), NotificationManager.IMPORTANCE_LOW).apply {
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
manager.createNotificationChannel(this)
}
NotificationChannel(CHANNEL_INACTIVE,
app.getText(R.string.notification_channel_monitor), NotificationManager.IMPORTANCE_LOW).apply {
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
manager.createNotificationChannel(this)
}
// remove old service channels
manager.deleteNotificationChannel("hotspot")
manager.deleteNotificationChannel("repeater")

View File

@@ -14,13 +14,12 @@ import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor
import be.mygod.vpnhotspot.net.monitor.IpMonitor
import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
import be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreferenceDialogFragment
import be.mygod.vpnhotspot.preference.AutoCompleteNetworkPreferenceDialogFragment
import be.mygod.vpnhotspot.preference.SharedPreferenceDataStore
import be.mygod.vpnhotspot.preference.SummaryFallbackProvider
import be.mygod.vpnhotspot.root.Dump
import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.allInterfaceNames
import be.mygod.vpnhotspot.util.launchUrl
import be.mygod.vpnhotspot.util.showAllowingStateLoss
import be.mygod.vpnhotspot.widget.SmartSnackbar
@@ -144,16 +143,12 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
}
}
override fun onDisplayPreferenceDialog(preference: Preference) {
when (preference.key) {
override fun onDisplayPreferenceDialog(preference: Preference) = when (preference.key) {
UpstreamMonitor.KEY, FallbackUpstreamMonitor.KEY ->
AlwaysAutoCompleteEditTextPreferenceDialogFragment().apply {
setArguments(preference.key, Services.connectivity.allNetworks.mapNotNull {
Services.connectivity.getLinkProperties(it)?.allInterfaceNames
}.flatten().toTypedArray())
AutoCompleteNetworkPreferenceDialogFragment().apply {
setArguments(preference.key)
setTargetFragment(this@SettingsPreferenceFragment, 0)
}.showAllowingStateLoss(parentFragmentManager, preference.key)
else -> super.onDisplayPreferenceDialog(preference)
}
}
}

View File

@@ -4,7 +4,10 @@ import android.content.ComponentName
import android.content.IntentFilter
import android.content.ServiceConnection
import android.net.wifi.p2p.WifiP2pDevice
import android.os.Build
import android.os.IBinder
import android.os.Parcelable
import androidx.annotation.RequiresApi
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
@@ -13,13 +16,19 @@ import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.RepeaterService
import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toCompat
import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.net.wifi.WifiClient
import be.mygod.vpnhotspot.root.WifiApCommands
import be.mygod.vpnhotspot.util.broadcastReceiver
class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callback, DefaultLifecycleObserver {
class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callback, DefaultLifecycleObserver,
WifiApManager.SoftApCallbackCompat {
private var tetheredInterfaces = emptySet<String>()
private val receiver = broadcastReceiver { _, intent ->
tetheredInterfaces = (intent.tetheredIfaces ?: return@broadcastReceiver).toSet() +
@@ -29,6 +38,7 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
private var repeater: RepeaterService.Binder? = null
private var p2p: Collection<WifiP2pDevice> = emptyList()
private var wifiAp = emptyList<Pair<String, MacAddressCompat>>()
private var neighbours: Collection<IpNeighbour> = emptyList()
val clients = MutableLiveData<List<Client>>()
val fullMode = object : DefaultLifecycleObserver {
@@ -42,11 +52,18 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
private fun populateClients() {
val clients = HashMap<Pair<String, MacAddressCompat>, Client>()
val group = repeater?.group
val p2pInterface = group?.`interface`
if (p2pInterface != null) {
for (client in p2p) clients[p2pInterface to MacAddressCompat.fromString(client.deviceAddress)] =
WifiP2pClient(p2pInterface, client)
repeater?.group?.`interface`?.let { p2pInterface ->
for (client in p2p) {
val addr = MacAddressCompat.fromString(client.deviceAddress!!)
clients[p2pInterface to addr] = object : Client(addr, p2pInterface) {
override val icon: Int get() = TetherType.WIFI_P2P.icon
}
}
}
for (client in wifiAp) {
clients[client] = object : Client(client.second, client.first) {
override val icon: Int get() = TetherType.WIFI.icon
}
}
for (neighbour in neighbours) {
val key = neighbour.dev to neighbour.lladdr
@@ -70,8 +87,10 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
override fun onStart(owner: LifecycleOwner) {
app.registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
IpNeighbourMonitor.registerCallback(this, false)
if (Build.VERSION.SDK_INT >= 31) WifiApCommands.registerSoftApCallback(this)
}
override fun onStop(owner: LifecycleOwner) {
if (Build.VERSION.SDK_INT >= 31) WifiApCommands.unregisterSoftApCallback(this)
IpNeighbourMonitor.unregisterCallback(this)
app.unregisterReceiver(receiver)
}
@@ -94,4 +113,12 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
this.neighbours = neighbours
populateClients()
}
@RequiresApi(31)
override fun onConnectedClientsChanged(clients: List<Parcelable>) {
wifiAp = clients.mapNotNull {
val client = WifiClient(it)
client.apInstanceIdentifier?.run { this to client.macAddress.toCompat() }
}
}
}

View File

@@ -37,7 +37,7 @@ import be.mygod.vpnhotspot.net.monitor.TrafficRecorder
import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.room.ClientStats
import be.mygod.vpnhotspot.room.TrafficRecord
import be.mygod.vpnhotspot.util.SpanFormatter
import be.mygod.vpnhotspot.util.format
import be.mygod.vpnhotspot.util.showAllowingStateLoss
import be.mygod.vpnhotspot.util.toPluralInt
import be.mygod.vpnhotspot.widget.SmartSnackbar
@@ -82,10 +82,11 @@ class ClientsFragment : Fragment() {
data class StatsArg(val title: CharSequence, val stats: ClientStats) : Parcelable
class StatsDialogFragment : AlertDialogFragment<StatsArg, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setTitle(SpanFormatter.format(getText(R.string.clients_stats_title), arg.title))
val context = context
val resources = resources
val format = NumberFormat.getIntegerInstance(resources.configuration.locale)
val locale = resources.configuration.locale
setTitle(getText(R.string.clients_stats_title).format(locale, arg.title))
val format = NumberFormat.getIntegerInstance(locale)
setMessage("%s\n%s\n%s".format(
resources.getQuantityString(R.plurals.clients_stats_message_1, arg.stats.count.toPluralInt(),
format.format(arg.stats.count), DateUtils.formatDateTime(context, arg.stats.timestamp,

View File

@@ -1,10 +0,0 @@
package be.mygod.vpnhotspot.client
import android.net.wifi.p2p.WifiP2pDevice
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.TetherType
class WifiP2pClient(p2pInterface: String, p2p: WifiP2pDevice) :
Client(MacAddressCompat.fromString(p2p.deviceAddress!!), p2pInterface) {
override val icon: Int get() = TetherType.WIFI_P2P.icon
}

View File

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

View File

@@ -1,13 +1,13 @@
package be.mygod.vpnhotspot.manage
import android.Manifest
import android.annotation.TargetApi
import android.content.*
import android.os.Build
import android.os.IBinder
import android.provider.Settings
import android.view.View
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.recyclerview.widget.RecyclerView
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.LocalOnlyHotspotService
@@ -18,7 +18,7 @@ import be.mygod.vpnhotspot.util.formatAddresses
import be.mygod.vpnhotspot.widget.SmartSnackbar
import java.net.NetworkInterface
@TargetApi(26)
@RequiresApi(26)
class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager(), ServiceConnection {
companion object {
val permission = if (Build.VERSION.SDK_INT >= 29) {

View File

@@ -5,7 +5,6 @@ import android.content.ComponentName
import android.content.DialogInterface
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.net.wifi.SoftApConfiguration
import android.net.wifi.p2p.WifiP2pGroup
import android.os.Build
@@ -30,6 +29,7 @@ import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.wifi.P2pSupplicantConfiguration
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
import be.mygod.vpnhotspot.net.wifi.WifiApDialogFragment
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.formatAddresses
import be.mygod.vpnhotspot.util.showAllowingStateLoss
@@ -89,11 +89,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
when (binder?.service?.status) {
RepeaterService.Status.IDLE -> if (Build.VERSION.SDK_INT < 29) parent.requireContext().let { context ->
ContextCompat.startForegroundService(context, Intent(context, RepeaterService::class.java))
} else if (parent.requireContext().checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED ||
parent.shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) {
parent.startRepeater.launch(Manifest.permission.ACCESS_FINE_LOCATION)
} else SmartSnackbar.make(R.string.repeater_missing_location_permissions).shortToast().show()
} else parent.startRepeater.launch(Manifest.permission.ACCESS_FINE_LOCATION)
RepeaterService.Status.ACTIVE -> binder.shutdown()
else -> { }
}
@@ -192,23 +188,23 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
return SoftApConfigurationCompat(
ssid = networkName,
passphrase = passphrase,
band = RepeaterService.operatingBand,
channel = RepeaterService.operatingChannel,
securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, // is not actually used
isAutoShutdownEnabled = RepeaterService.isAutoShutdownEnabled,
shutdownTimeoutMillis = RepeaterService.shutdownTimeoutMillis).apply {
bssid = RepeaterService.deviceAddress
setChannel(RepeaterService.operatingChannel, RepeaterService.operatingBand)
setMacRandomizationEnabled(WifiApManager.p2pMacRandomizationSupported)
} to false
}
} else binder?.let { binder ->
val group = binder.group ?: binder.fetchPersistentGroup().let { binder.group }
if (group != null) return SoftApConfigurationCompat(
ssid = group.networkName,
channel = RepeaterService.operatingChannel,
band = SoftApConfigurationCompat.BAND_ANY,
securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, // is not actually used
isAutoShutdownEnabled = RepeaterService.isAutoShutdownEnabled,
shutdownTimeoutMillis = RepeaterService.shutdownTimeoutMillis).run {
setChannel(RepeaterService.operatingChannel)
setMacRandomizationEnabled(WifiApManager.p2pMacRandomizationSupported)
try {
val config = P2pSupplicantConfiguration(group)
config.init(binder.obtainDeviceAddress()?.toString())
@@ -230,6 +226,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
return null
}
private suspend fun updateConfiguration(config: SoftApConfigurationCompat) {
val (band, channel) = config.requireSingleBand()
if (RepeaterService.safeMode) {
RepeaterService.networkName = config.ssid
RepeaterService.deviceAddress = config.bssid
@@ -246,8 +243,8 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
}
holder.config = null
}
RepeaterService.operatingBand = config.band
RepeaterService.operatingChannel = config.channel
RepeaterService.operatingBand = band
RepeaterService.operatingChannel = channel
RepeaterService.isAutoShutdownEnabled = config.isAutoShutdownEnabled
RepeaterService.shutdownTimeoutMillis = config.shutdownTimeoutMillis
}

View File

@@ -2,40 +2,41 @@ package be.mygod.vpnhotspot.manage
import android.Manifest
import android.annotation.TargetApi
import android.content.ClipData
import android.bluetooth.BluetoothAdapter
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.net.MacAddress
import android.os.Build
import android.os.Parcelable
import android.provider.Settings
import android.text.SpannableStringBuilder
import android.text.format.DateUtils
import android.view.View
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.core.net.toUri
import androidx.core.os.BuildCompat
import androidx.core.view.updatePaddingRelative
import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.BuildConfig
import be.mygod.vpnhotspot.MainActivity
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.net.wifi.*
import be.mygod.vpnhotspot.net.wifi.WifiApManager.wifiApState
import be.mygod.vpnhotspot.root.WifiApCommands
import be.mygod.vpnhotspot.util.readableMessage
import be.mygod.vpnhotspot.util.*
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.lang.reflect.InvocationTargetException
import java.util.*
sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
TetheringManager.StartTetheringCallback {
@@ -63,12 +64,16 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
} catch (e: RuntimeException) {
app.logEvent("manage_write_settings") { param("message", e.toString()) }
}
if (manager.isStarted) try {
when (manager.isStarted) {
true -> try {
manager.stop()
} catch (e: InvocationTargetException) {
if (e.targetException !is SecurityException) Timber.w(e)
manager.onException(e)
} else manager.start()
}
false -> manager.start()
null -> manager.onClickNull()
}
}
}
@@ -79,13 +84,13 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
override val icon get() = tetherType.icon
override val title get() = this@TetherManager.title
override val text get() = this@TetherManager.text
override val active get() = isStarted
override val active get() = isStarted == true
}
val data = Data()
abstract val title: CharSequence
abstract val tetherType: TetherType
open val isStarted get() = parent.enabledTypes.contains(tetherType)
open val isStarted: Boolean? get() = parent.enabledTypes.contains(tetherType)
protected open val text: CharSequence get() = baseError ?: ""
protected var baseError: String? = null
@@ -93,6 +98,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
protected abstract fun start()
protected abstract fun stop()
protected open fun onClickNull(): Unit = throw UnsupportedOperationException()
override fun onTetheringStarted() = data.notifyChange()
override fun onTetheringFailed(error: Int?) {
@@ -119,11 +125,13 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
(viewHolder as ViewHolder).manager = this
}
fun updateErrorMessage(errored: List<String>) {
fun updateErrorMessage(errored: List<String>, lastErrors: Map<String, Int>) {
val interested = errored.filter { TetherType.ofInterface(it) == tetherType }
baseError = if (interested.isEmpty()) null else interested.joinToString("\n") { iface ->
"$iface: " + try {
TetheringManager.tetherErrorLookup(TetheringManager.getLastTetherError(iface))
TetheringManager.tetherErrorLookup(if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
TetheringManager.getLastTetherError(iface)
} else lastErrors[iface] ?: 0)
} catch (e: InvocationTargetException) {
if (Build.VERSION.SDK_INT !in 24..25 || e.cause !is SecurityException) Timber.w(e) else Timber.d(e)
e.readableMessage
@@ -135,77 +143,156 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
@RequiresApi(24)
class Wifi(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver,
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 numClients: Int? = null
private var frequency = 0
private var bandwidth = WifiApManager.CHANNEL_WIDTH_INVALID
private var capability: Pair<Int, Long>? = null
private var info = emptyList<Parcelable>()
private var capability: Parcelable? = null
init {
if (Build.VERSION.SDK_INT >= 28) parent.viewLifecycleOwner.lifecycle.addObserver(this)
if (Build.VERSION.SDK_INT >= 23) parent.viewLifecycleOwner.lifecycle.addObserver(this)
}
@TargetApi(28)
override fun onStart(owner: LifecycleOwner) {
WifiApCommands.registerSoftApCallback(this)
if (Build.VERSION.SDK_INT < 28) {
parent.requireContext().registerReceiver(receiver,
IntentFilter(WifiApManager.WIFI_AP_STATE_CHANGED_ACTION))
} else WifiApCommands.registerSoftApCallback(this)
}
@TargetApi(28)
override fun onStop(owner: LifecycleOwner) {
WifiApCommands.unregisterSoftApCallback(this)
if (Build.VERSION.SDK_INT < 28) {
parent.requireContext().unregisterReceiver(receiver)
} else WifiApCommands.unregisterSoftApCallback(this)
}
override fun onStateChanged(state: Int, failureReason: Int) {
if (state < 10 || state > 14) {
Timber.w(Exception("Unknown state $state, $failureReason"))
return
}
this.failureReason = if (state == 14) failureReason else null // WIFI_AP_STATE_FAILED
if (!WifiApManager.checkWifiApState(state)) return
this.failureReason = if (state == WifiApManager.WIFI_AP_STATE_FAILED) failureReason else null
data.notifyChange()
}
override fun onNumClientsChanged(numClients: Int) {
this.numClients = numClients
if (Build.VERSION.SDK_INT >= 30) data.notifyChange() // only emits when onCapabilityChanged can be called
}
override fun onInfoChanged(frequency: Int, bandwidth: Int) {
this.frequency = frequency
this.bandwidth = bandwidth
data.notifyChange()
}
override fun onCapabilityChanged(maxSupportedClients: Int, supportedFeatures: Long) {
capability = maxSupportedClients to supportedFeatures
override fun onInfoChanged(info: List<Parcelable>) {
this.info = info
data.notifyChange()
}
override fun onBlockedClientConnecting(client: MacAddress, blockedReason: Int) {
val reason = WifiApManager.clientBlockLookup(blockedReason, true)
Timber.i("$client blocked from connecting: $reason ($blockedReason)")
SmartSnackbar.make(parent.getString(R.string.tethering_manage_wifi_client_blocked, client, reason)).apply {
action(R.string.tethering_manage_wifi_copy_mac) {
app.clipboard.setPrimaryClip(ClipData.newPlainText(null, client.toString()))
}
}.show()
override fun onCapabilityChanged(capability: Parcelable) {
this.capability = capability
data.notifyChange()
}
override val title get() = parent.getString(R.string.tethering_manage_wifi)
override val tetherType get() = TetherType.WIFI
override val type get() = VIEW_TYPE_WIFI
override val text get() = listOfNotNull(failureReason?.let { WifiApManager.failureReasonLookup(it) }, baseError,
if (frequency != 0 || bandwidth != WifiApManager.CHANNEL_WIDTH_INVALID) {
parent.getString(R.string.tethering_manage_wifi_info, frequency,
SoftApConfigurationCompat.frequencyToChannel(frequency),
WifiApManager.channelWidthLookup(bandwidth, true))
} else null,
capability?.let { (maxSupportedClients, supportedFeatures) ->
app.resources.getQuantityString(R.plurals.tethering_manage_wifi_capabilities, maxSupportedClients,
numClients ?: "?", maxSupportedClients, sequence {
var features = supportedFeatures
if (features == 0L) yield(parent.getString(R.string.tethering_manage_wifi_no_features))
else while (features != 0L) {
@TargetApi(30)
private fun formatCapability(locale: Locale) = capability?.let { parcel ->
val capability = SoftApCapability(parcel)
val numClients = numClients
val maxClients = capability.maxSupportedClients
var features = capability.supportedFeatures
if (Build.VERSION.SDK_INT >= 31) for ((flag, band) in arrayOf(
SoftApCapability.SOFTAP_FEATURE_BAND_24G_SUPPORTED to SoftApConfigurationCompat.BAND_2GHZ,
SoftApCapability.SOFTAP_FEATURE_BAND_5G_SUPPORTED to SoftApConfigurationCompat.BAND_5GHZ,
SoftApCapability.SOFTAP_FEATURE_BAND_6G_SUPPORTED to SoftApConfigurationCompat.BAND_6GHZ,
SoftApCapability.SOFTAP_FEATURE_BAND_60G_SUPPORTED to SoftApConfigurationCompat.BAND_60GHZ,
)) {
if (capability.getSupportedChannelList(band).isEmpty()) continue
// reduce double reporting
features = features and flag.inv()
}
val result = parent.resources.getQuantityText(R.plurals.tethering_manage_wifi_capabilities, numClients ?: 0)
.format(locale, numClients ?: "?", maxClients, sequence {
if (WifiApManager.isApMacRandomizationSupported) yield(parent.getText(
R.string.tethering_manage_wifi_feature_ap_mac_randomization))
if (Services.wifi.isStaApConcurrencySupported) yield(parent.getText(
R.string.tethering_manage_wifi_feature_sta_ap_concurrency))
if (Build.VERSION.SDK_INT >= 31) {
if (Services.wifi.isBridgedApConcurrencySupported) yield(parent.getText(
R.string.tethering_manage_wifi_feature_bridged_ap_concurrency))
if (Services.wifi.isStaBridgedApConcurrencySupported) yield(parent.getText(
R.string.tethering_manage_wifi_feature_sta_bridged_ap_concurrency))
}
if (features != 0L) while (features != 0L) {
val bit = features.takeLowestOneBit()
yield(WifiApManager.featureLookup(bit, true))
yield(SoftApCapability.featureLookup(bit, true))
features = features and bit.inv()
}
}.joinToString())
}).joinToString("\n")
}.joinToSpanned().let {
if (it.isEmpty()) parent.getText(R.string.tethering_manage_wifi_no_features) else it
})
if (Build.VERSION.SDK_INT >= 31) {
val list = SoftApConfigurationCompat.BAND_TYPES.map { band ->
val channels = capability.getSupportedChannelList(band)
if (channels.isNotEmpty()) StringBuilder().apply {
append(SoftApConfigurationCompat.bandLookup(band, true))
append(" (")
channels.sort()
var pending: Int? = null
var last = channels[0]
append(last)
for (channel in channels.asSequence().drop(1)) {
if (channel == last + 1) pending = channel else {
pending?.let {
append('-')
append(it)
pending = null
}
append(',')
append(channel)
}
last = channel
}
pending?.let {
append('-')
append(it)
}
append(')')
} else null
}.filterNotNull()
if (list.isNotEmpty()) result.append(parent.getText(R.string.tethering_manage_wifi_supported_channels)
.format(locale, list.joinToString("; ")))
}
result
} ?: numClients?.let { numClients ->
app.resources.getQuantityText(R.plurals.tethering_manage_wifi_clients, numClients).format(locale,
numClients)
}
override val text get() = parent.resources.configuration.locale.let { locale ->
listOfNotNull(failureReason?.let { WifiApManager.failureReasonLookup(it) }, baseError, info.run {
if (isEmpty()) null else joinToSpanned("\n") @TargetApi(30) { parcel ->
val info = SoftApInfo(parcel)
val frequency = info.frequency
val channel = SoftApConfigurationCompat.frequencyToChannel(frequency)
val bandwidth = SoftApInfo.channelWidthLookup(info.bandwidth, true)
if (Build.VERSION.SDK_INT >= 31) {
val bssid = info.bssid.let { if (it == null) null else makeMacSpan(it.toString()) }
val bssidAp = info.apInstanceIdentifier?.let {
when (bssid) {
null -> it
is String -> "$bssid%$it" // take the fast route if possible
else -> SpannableStringBuilder(bssid).append("%$it")
}
} ?: bssid ?: "?"
val timeout = info.autoShutdownTimeoutMillis
parent.getText(if (timeout == 0L) {
R.string.tethering_manage_wifi_info_timeout_disabled
} else R.string.tethering_manage_wifi_info_timeout_enabled).format(locale,
frequency, channel, bandwidth, bssidAp, info.wifiStandard,
// http://unicode.org/cldr/trac/ticket/3407
DateUtils.formatElapsedTime(timeout / 1000))
} else parent.getText(R.string.tethering_manage_wifi_info).format(locale,
frequency, channel, bandwidth)
}
}, formatCapability(locale)).joinToSpanned("\n")
}
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI, this::onException)
@@ -220,25 +307,24 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_USB, this::onException)
}
@RequiresApi(24)
class Bluetooth(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver {
companion object {
// TODO: migrate to framework Manifest.permission when stable
private const val BLUETOOTH_CONNECT = "android.permission.BLUETOOTH_CONNECT"
}
private val tethering = BluetoothTethering(parent.requireContext()) { data.notifyChange() }
class Bluetooth(parent: TetheringFragment, adapter: BluetoothAdapter) :
TetherManager(parent), DefaultLifecycleObserver {
private val tethering = BluetoothTethering(parent.requireContext(), adapter) { data.notifyChange() }
init {
parent.viewLifecycleOwner.lifecycle.addObserver(this)
}
fun ensureInit(context: Context) = tethering.ensureInit(context)
fun ensureInit(context: Context) {
tethering.ensureInit(context)
onTetheringStarted() // force flush
}
override fun onResume(owner: LifecycleOwner) {
if (!BuildCompat.isAtLeastS() || parent.requireContext().checkSelfPermission(BLUETOOTH_CONNECT) ==
PackageManager.PERMISSION_GRANTED) {
ensureInit(parent.requireContext())
} else if (parent.shouldShowRequestPermissionRationale(BLUETOOTH_CONNECT)) {
parent.requestBluetooth.launch(BLUETOOTH_CONNECT)
if (Build.VERSION.SDK_INT < 31 || parent.requireContext().checkSelfPermission(
Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
tethering.ensureInit(parent.requireContext())
} else if (parent.shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_CONNECT)) {
parent.requestBluetooth.launch(Manifest.permission.BLUETOOTH_CONNECT)
}
}
override fun onDestroy(owner: LifecycleOwner) = tethering.close()
@@ -246,17 +332,17 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
override val title get() = parent.getString(R.string.tethering_manage_bluetooth)
override val tetherType get() = TetherType.BLUETOOTH
override val type get() = VIEW_TYPE_BLUETOOTH
override val isStarted get() = tethering.active == true
override val isStarted get() = tethering.active
override val text get() = listOfNotNull(
if (tethering.active == null) tethering.activeFailureCause?.readableMessage else null,
baseError).joinToString("\n")
override fun start() = BluetoothTethering.start(this)
override fun start() = tethering.start(this)
override fun stop() {
TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, this::onException)
Thread.sleep(1) // give others a room to breathe
tethering.stop(this::onException)
onTetheringStarted() // force flush state
}
override fun onClickNull() = ManageBar.start(parent.requireContext())
}
@RequiresApi(30)
class Ethernet(parent: TetheringFragment) : TetherManager(parent) {

View File

@@ -3,6 +3,7 @@
package be.mygod.vpnhotspot.manage
import android.annotation.TargetApi
import android.bluetooth.BluetoothManager
import android.content.*
import android.os.Build
import android.os.Bundle
@@ -15,6 +16,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DefaultItemAnimator
@@ -34,6 +36,7 @@ import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.root.WifiApCommands
import be.mygod.vpnhotspot.util.*
import be.mygod.vpnhotspot.widget.SmartSnackbar
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import timber.log.Timber
@@ -42,16 +45,24 @@ import java.net.NetworkInterface
import java.net.SocketException
class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickListener {
inner class ManagerAdapter : ListAdapter<Manager, RecyclerView.ViewHolder>(Manager) {
inner class ManagerAdapter : ListAdapter<Manager, RecyclerView.ViewHolder>(Manager),
TetheringManager.TetheringEventCallback {
internal val repeaterManager by lazy { RepeaterManager(this@TetheringFragment) }
@get:RequiresApi(26)
internal val localOnlyHotspotManager by lazy @TargetApi(26) { LocalOnlyHotspotManager(this@TetheringFragment) }
internal val bluetoothManager by lazy @TargetApi(24) { TetherManager.Bluetooth(this@TetheringFragment) }
@get:RequiresApi(24)
internal val bluetoothManager by lazy {
if (Build.VERSION.SDK_INT >= 24) requireContext().getSystemService<BluetoothManager>()?.adapter?.let {
TetherManager.Bluetooth(this@TetheringFragment, it)
} else null
}
@get:RequiresApi(24)
private val tetherManagers by lazy @TargetApi(24) {
listOf(TetherManager.Wifi(this@TetheringFragment),
listOfNotNull(
TetherManager.Wifi(this@TetheringFragment),
TetherManager.Usb(this@TetheringFragment),
bluetoothManager)
bluetoothManager,
)
}
@get:RequiresApi(30)
private val tetherManagers30 by lazy @TargetApi(30) {
@@ -59,30 +70,32 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
TetherManager.Ncm(this@TetheringFragment),
TetherManager.WiGig(this@TetheringFragment))
}
private val wifiManagerLegacy by lazy @Suppress("Deprecation") {
TetherManager.WifiLegacy(this@TetheringFragment)
}
private val wifiManagerLegacy by lazy { TetherManager.WifiLegacy(this@TetheringFragment) }
private var enabledIfaces = emptyList<String>()
var activeIfaces = emptyList<String>()
var localOnlyIfaces = emptyList<String>()
var erroredIfaces = emptyList<String>()
private var listDeferred = CompletableDeferred<List<Manager>>(emptyList())
private fun updateEnabledTypes() {
this@TetheringFragment.enabledTypes = enabledIfaces.map { TetherType.ofInterface(it) }.toSet()
fun updateEnabledTypes() {
this@TetheringFragment.enabledTypes =
(activeIfaces + localOnlyIfaces).map { TetherType.ofInterface(it) }.toSet()
}
suspend fun notifyInterfaceChanged(lastList: List<Manager>? = null) {
@Suppress("NAME_SHADOWING") val lastList = lastList ?: listDeferred.await()
val first = lastList.indexOfFirst { it is InterfaceManager }
if (first >= 0) notifyItemRangeChanged(first, lastList.indexOfLast { it is InterfaceManager } - first + 1)
val lastErrors = mutableMapOf<String, Int>()
override fun onError(ifName: String, error: Int) {
if (error == 0) lastErrors.remove(ifName) else lastErrors[ifName] = error
}
suspend fun notifyTetherTypeChanged() {
updateEnabledTypes()
val lastList = listDeferred.await()
notifyInterfaceChanged(lastList)
val first = lastList.indexOfLast { it !is TetherManager } + 1
var first = lastList.indexOfFirst { it is InterfaceManager }
if (first >= 0) notifyItemRangeChanged(first, lastList.indexOfLast { it is InterfaceManager } - first + 1)
first = lastList.indexOfLast { it !is TetherManager } + 1
notifyItemRangeChanged(first, lastList.size - first)
}
fun update(activeIfaces: List<String>, localOnlyIfaces: List<String>, erroredIfaces: List<String>) {
fun update() {
val deferred = CompletableDeferred<List<Manager>>()
listDeferred = deferred
ifaceLookup = try {
@@ -91,8 +104,6 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
Timber.d(e)
emptyMap()
}
enabledIfaces = activeIfaces + localOnlyIfaces
updateEnabledTypes()
val list = ArrayList<Manager>()
if (Services.p2p != null) list.add(repeaterManager)
@@ -104,11 +115,11 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
list.add(ManageBar)
if (Build.VERSION.SDK_INT >= 24) {
list.addAll(tetherManagers)
tetherManagers.forEach { it.updateErrorMessage(erroredIfaces) }
tetherManagers.forEach { it.updateErrorMessage(erroredIfaces, lastErrors) }
}
if (Build.VERSION.SDK_INT >= 30) {
list.addAll(tetherManagers30)
tetherManagers30.forEach { it.updateErrorMessage(erroredIfaces) }
tetherManagers30.forEach { it.updateErrorMessage(erroredIfaces, lastErrors) }
}
if (Build.VERSION.SDK_INT < 26) {
list.add(wifiManagerLegacy)
@@ -125,7 +136,10 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
@RequiresApi(29)
val startRepeater = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) requireActivity().startForegroundService(Intent(activity, RepeaterService::class.java))
if (granted) requireActivity().startForegroundService(Intent(activity, RepeaterService::class.java)) else {
Snackbar.make((activity as MainActivity).binding.fragmentHolder,
R.string.repeater_missing_location_permissions, Snackbar.LENGTH_LONG).show()
}
}
@RequiresApi(26)
val startLocalOnlyHotspot = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
@@ -133,7 +147,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
}
@RequiresApi(31)
val requestBluetooth = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) adapter.bluetoothManager.ensureInit(requireContext())
if (granted) adapter.bluetoothManager!!.ensureInit(requireContext())
}
var ifaceLookup: Map<String, NetworkInterface> = emptyMap()
@@ -142,9 +156,12 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
var binder: TetheringService.Binder? = null
private val adapter = ManagerAdapter()
private val receiver = broadcastReceiver { _, intent ->
adapter.update(intent.tetheredIfaces ?: return@broadcastReceiver,
intent.localOnlyTetheredIfaces ?: return@broadcastReceiver,
intent.getStringArrayListExtra(TetheringManager.EXTRA_ERRORED_TETHER) ?: return@broadcastReceiver)
adapter.activeIfaces = intent.tetheredIfaces ?: return@broadcastReceiver
adapter.localOnlyIfaces = intent.localOnlyTetheredIfaces ?: return@broadcastReceiver
adapter.erroredIfaces = intent.getStringArrayListExtra(TetheringManager.EXTRA_ERRORED_TETHER)
?: return@broadcastReceiver
adapter.updateEnabledTypes()
adapter.update()
}
private fun updateMonitorList(canMonitor: List<String> = emptyList()) {
@@ -219,7 +236,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
AlertDialogFragment.setResultListener<WifiApDialogFragment, WifiApDialogFragment.Arg>(this) { which, ret ->
if (which == DialogInterface.BUTTON_POSITIVE) viewLifecycleOwner.lifecycleScope.launchWhenCreated {
val configuration = ret!!.configuration
@@ -251,7 +268,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
binding.interfaces.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
binding.interfaces.itemAnimator = DefaultItemAnimator()
binding.interfaces.adapter = adapter
adapter.update(emptyList(), emptyList(), emptyList())
adapter.update()
ServiceForegroundConnector(this, this, TetheringService::class)
(activity as MainActivity).binding.toolbar.apply {
inflateMenu(R.menu.toolbar_tethering)
@@ -275,19 +292,22 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
binder = service as TetheringService.Binder
service.routingsChanged[this] = {
lifecycleScope.launchWhenStarted { adapter.notifyInterfaceChanged() }
}
service.routingsChanged[this] = { lifecycleScope.launchWhenStarted { adapter.update() } }
requireContext().registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
if (Build.VERSION.SDK_INT >= 30) TetherType.listener[this] = {
lifecycleScope.launchWhenStarted { adapter.notifyTetherTypeChanged() }
if (Build.VERSION.SDK_INT >= 30) {
TetheringManager.registerTetheringEventCallback(null, adapter)
TetherType.listener[this] = { lifecycleScope.launchWhenStarted { adapter.notifyTetherTypeChanged() } }
}
}
override fun onServiceDisconnected(name: ComponentName?) {
(binder ?: return).routingsChanged -= this
binder = null
if (Build.VERSION.SDK_INT >= 30) TetherType.listener -= this
if (Build.VERSION.SDK_INT >= 30) {
TetherType.listener -= this
TetheringManager.unregisterTetheringEventCallback(adapter)
adapter.lastErrors.clear()
}
requireContext().unregisterReceiver(receiver)
}
}

View File

@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot.manage
import android.bluetooth.BluetoothManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
@@ -11,6 +12,7 @@ import android.service.quicksettings.Tile
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.TetheringService
import be.mygod.vpnhotspot.net.TetherType
@@ -151,15 +153,16 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
override val labelString get() = R.string.tethering_manage_bluetooth
override val tetherType get() = TetherType.BLUETOOTH
override fun start() = BluetoothTethering.start(this)
override fun start() = tethering!!.start(this)
override fun stop() {
TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, this::onException)
Thread.sleep(1) // give others a room to breathe
tethering!!.stop(this::onException)
onTetheringStarted() // force flush state
}
override fun onStartListening() {
tethering = BluetoothTethering(this) { updateTile() }
tethering = getSystemService<BluetoothManager>()?.adapter?.let {
BluetoothTethering(this, it) { updateTile() }
}
super.onStartListening()
}
override fun onStopListening() {
@@ -187,7 +190,7 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
icon = tileOff
}
null -> {
state = Tile.STATE_UNAVAILABLE
state = Tile.STATE_INACTIVE
icon = tileOff
subtitle(tethering?.activeFailureCause?.readableMessage)
}
@@ -198,7 +201,8 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
}
override fun onClick() {
when (tethering?.active) {
val tethering = tethering
if (tethering == null) tapPending = true else when (tethering.active) {
true -> {
val binder = binder
if (binder == null) tapPending = true else {
@@ -212,7 +216,7 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
}
}
false -> start()
else -> tapPending = true
else -> ManageBar.start(this)
}
}
}

View File

@@ -71,9 +71,9 @@ enum class TetherType(@DrawableRes val icon: Int) {
}
@RequiresApi(30)
override fun onTetherableInterfaceRegexpsChanged(args: Array<out Any?>?) = synchronized(this) {
override fun onTetherableInterfaceRegexpsChanged(reg: Any?) = synchronized(this) {
if (requiresUpdate) return@synchronized
Timber.i("onTetherableInterfaceRegexpsChanged: ${args?.contentDeepToString()}")
Timber.i("onTetherableInterfaceRegexpsChanged: $reg")
TetheringManager.unregisterTetheringEventCallback(this)
requiresUpdate = true
listener()

View File

@@ -7,7 +7,6 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.net.ConnectivityManager
import android.net.Network
import android.os.Build
@@ -67,7 +66,11 @@ object TetheringManager {
}
private object InPlaceExecutor : Executor {
override fun execute(command: Runnable) = command.run()
override fun execute(command: Runnable) = try {
command.run()
} catch (e: Exception) {
Timber.w(e) // prevent Binder stub swallowing the exception
}
}
/**
@@ -135,7 +138,7 @@ object TetheringManager {
* Requires MANAGE_USB permission, unfortunately.
*
* Source: https://android.googlesource.com/platform/frameworks/base/+/7ca5d3a/services/usb/java/com/android/server/usb/UsbService.java#389
* @see [startTethering].
* @see startTethering
*/
@RequiresApi(24)
const val TETHERING_USB = 1
@@ -143,14 +146,14 @@ object TetheringManager {
* Bluetooth tethering type.
*
* Requires BLUETOOTH permission.
* @see [startTethering].
* @see startTethering
*/
@RequiresApi(24)
const val TETHERING_BLUETOOTH = 2
/**
* Ncm local tethering type.
*
* @see [startTethering]
* @see startTethering
*/
@RequiresApi(30)
const val TETHERING_NCM = 4
@@ -158,7 +161,7 @@ object TetheringManager {
* Ethernet tethering type.
*
* Requires MANAGE_USB permission, also.
* @see [startTethering]
* @see startTethering
*/
@RequiresApi(30)
const val TETHERING_ETHERNET = 5
@@ -248,13 +251,12 @@ object TetheringManager {
val proxy = ProxyBuilder.forClass(classOnStartTetheringCallback).apply {
dexCache(cacheDir)
handler { proxy, method, args ->
if (args.isNotEmpty()) Timber.w("Unexpected args for ${method.name}: $args")
@Suppress("NAME_SHADOWING") val callback = reference.get()
when (method.name) {
"onTetheringStarted" -> callback?.onTetheringStarted()
"onTetheringFailed" -> callback?.onTetheringFailed()
else -> ProxyBuilder.callSuper(proxy, method, args)
if (args.isEmpty()) when (method.name) {
"onTetheringStarted" -> return@handler callback?.onTetheringStarted()
"onTetheringFailed" -> return@handler callback?.onTetheringFailed()
}
ProxyBuilder.callSuper(proxy, method, args)
}
}.build()
startTetheringLegacy(Services.connectivity, type, showProvisioningUi, proxy, handler)
@@ -276,13 +278,9 @@ object TetheringManager {
arrayOf(interfaceStartTetheringCallback), object : InvocationHandler {
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
@Suppress("NAME_SHADOWING") val callback = reference.get()
return when (val name = method.name) {
"onTetheringStarted" -> {
if (!args.isNullOrEmpty()) Timber.w("Unexpected args for $name: $args")
callback?.onTetheringStarted()
}
"onTetheringFailed" -> {
if (args?.size != 1) Timber.w("Unexpected args for $name: $args")
return when {
method.matches("onTetheringStarted") -> callback?.onTetheringStarted()
method.matches("onTetheringFailed", Integer.TYPE) -> {
callback?.onTetheringFailed(args?.get(0) as Int)
}
else -> callSuper(interfaceStartTetheringCallback, proxy, method, args)
@@ -446,7 +444,7 @@ object TetheringManager {
* *@param reg The new regular expressions.
* @hide
*/
fun onTetherableInterfaceRegexpsChanged(args: Array<out Any?>?) {}
fun onTetherableInterfaceRegexpsChanged(reg: Any?) {}
/**
* Called when there was a change in the list of tetherable interfaces. Tetherable
@@ -542,40 +540,34 @@ object TetheringManager {
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
@Suppress("NAME_SHADOWING")
val callback = reference.get()
val noArgs = args?.size ?: 0
return when (val name = method.name) {
"onTetheringSupported" -> {
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
return when {
method.matches("onTetheringSupported", Boolean::class.java) -> {
callback?.onTetheringSupported(args!![0] as Boolean)
}
"onUpstreamChanged" -> {
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
method.matches1<Network>("onUpstreamChanged") -> {
callback?.onUpstreamChanged(args!![0] as Network?)
}
"onTetherableInterfaceRegexpsChanged" -> {
if (regexpsSent) callback?.onTetherableInterfaceRegexpsChanged(args)
method.name == "onTetherableInterfaceRegexpsChanged" &&
method.parameters.singleOrNull()?.type?.name ==
"android.net.TetheringManager\$TetheringInterfaceRegexps" -> {
if (regexpsSent) callback?.onTetherableInterfaceRegexpsChanged(args!!.single())
regexpsSent = true
}
"onTetherableInterfacesChanged" -> {
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
method.matches1<java.util.List<*>>("onTetherableInterfacesChanged") -> {
@Suppress("UNCHECKED_CAST")
callback?.onTetherableInterfacesChanged(args!![0] as List<String?>)
}
"onTetheredInterfacesChanged" -> {
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
method.matches1<java.util.List<*>>("onTetheredInterfacesChanged") -> {
@Suppress("UNCHECKED_CAST")
callback?.onTetheredInterfacesChanged(args!![0] as List<String?>)
}
"onError" -> {
if (noArgs != 2) Timber.w("Unexpected args for $name: $args")
method.matches("onError", String::class.java, Integer.TYPE) -> {
callback?.onError(args!![0] as String, args[1] as Int)
}
"onClientsChanged" -> {
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
method.matches1<java.util.Collection<*>>("onClientsChanged") -> {
callback?.onClientsChanged(args!![0] as Collection<*>)
}
"onOffloadStatusChanged" -> {
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
method.matches("onOffloadStatusChanged", Integer.TYPE) -> {
callback?.onOffloadStatusChanged(args!![0] as Int)
}
else -> callSuper(interfaceTetheringEventCallback, proxy, method, args)
@@ -629,6 +621,7 @@ object TetheringManager {
* @return error The error code of the last error tethering or untethering the named
* interface
*/
@Deprecated("Use {@link TetheringEventCallback#onError(String, int)} instead.")
fun getLastTetherError(iface: String): Int = getLastTetherError(Services.connectivity, iface) as Int
val tetherErrorLookup = ConstantLookup("TETHER_ERROR_",

View File

@@ -6,7 +6,10 @@ import android.net.LinkProperties
import android.net.Network
import android.net.NetworkCapabilities
import android.os.Build
import android.os.Handler
import android.os.Looper
import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.globalNetworkRequestBuilder
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -18,10 +21,10 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
* Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1:
* https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
*/
private val networkRequest = networkRequestBuilder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
.build()
private val networkRequest = globalNetworkRequestBuilder().apply {
addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
}.build()
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
val properties = Services.connectivity.getLinkProperties(network)
@@ -51,9 +54,15 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
callback.onAvailable(currentLinkProperties)
}
} else {
if (Build.VERSION.SDK_INT in 24..27) @TargetApi(24) {
when (Build.VERSION.SDK_INT) {
in 31..Int.MAX_VALUE -> @TargetApi(31) {
Services.connectivity.registerBestMatchingNetworkCallback(networkRequest, networkCallback,
Handler(Looper.getMainLooper()))
}
in 24..27 -> @TargetApi(24) {
Services.connectivity.registerDefaultNetworkCallback(networkCallback)
} else try {
}
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
@@ -61,6 +70,7 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
GlobalScope.launch { callback.onFallback() }
return
}
}
registered = true
}
}

View File

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

View File

@@ -46,7 +46,7 @@ class TetherTimeoutMonitor(private val timeout: Long = 0,
val info = WifiApManager.resolvedActivity.activityInfo
val resources = app.packageManager.getResourcesForApplication(info.applicationInfo)
resources.getInteger(resources.findIdentifier("config_wifiFrameworkSoftApShutDownTimeoutMilliseconds",
"integer", "com.android.wifi.resources", info.packageName))
"integer", WifiApManager.RESOURCES_PACKAGE, info.packageName))
}
} catch (e: RuntimeException) {
Timber.w(e)

View File

@@ -2,9 +2,6 @@ package be.mygod.vpnhotspot.net.monitor
import android.content.SharedPreferences
import android.net.LinkProperties
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import be.mygod.vpnhotspot.App.Companion.app
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -23,13 +20,6 @@ abstract class UpstreamMonitor {
}
private var monitor = generateMonitor()
fun networkRequestBuilder() = NetworkRequest.Builder().apply {
if (Build.VERSION.SDK_INT == 23) { // workarounds for OEM bugs
removeCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
removeCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL)
}
}
fun registerCallback(callback: Callback) = synchronized(this) { monitor.registerCallback(callback) }
fun unregisterCallback(callback: Callback) = synchronized(this) { monitor.unregisterCallback(callback) }

View File

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

View File

@@ -75,7 +75,13 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
if (matchedBssid.isEmpty()) {
check(block.pskLine == null && block.psk == null)
if (match.groups[5] != null) {
block.psk = match.groupValues[5].apply { check(length in 8..63) }
block.psk = match.groupValues[5].apply {
when (length) {
in 8..63 -> { }
64 -> error("WPA-PSK hex not supported")
else -> error("Unknown length $length")
}
}
}
block.pskLine = block.size
} else if (bssids.any { matchedBssid.equals(it, true) }) {

View File

@@ -0,0 +1,41 @@
package be.mygod.vpnhotspot.net.wifi
import android.os.Parcelable
import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.util.LongConstantLookup
@JvmInline
@RequiresApi(30)
value class SoftApCapability(val inner: Parcelable) {
companion object {
val clazz by lazy { Class.forName("android.net.wifi.SoftApCapability") }
private val getMaxSupportedClients by lazy { clazz.getDeclaredMethod("getMaxSupportedClients") }
private val areFeaturesSupported by lazy { clazz.getDeclaredMethod("areFeaturesSupported", Long::class.java) }
@get:RequiresApi(31)
private val getSupportedChannelList by lazy {
clazz.getDeclaredMethod("getSupportedChannelList", Int::class.java)
}
@RequiresApi(31)
const val SOFTAP_FEATURE_BAND_24G_SUPPORTED = 32L
@RequiresApi(31)
const val SOFTAP_FEATURE_BAND_5G_SUPPORTED = 64L
@RequiresApi(31)
const val SOFTAP_FEATURE_BAND_6G_SUPPORTED = 128L
@RequiresApi(31)
const val SOFTAP_FEATURE_BAND_60G_SUPPORTED = 256L
val featureLookup by lazy { LongConstantLookup(clazz, "SOFTAP_FEATURE_") }
}
val maxSupportedClients get() = getMaxSupportedClients(inner) as Int
val supportedFeatures: Long get() {
var supportedFeatures = 0L
var probe = 1L
while (probe != 0L) {
if (areFeaturesSupported(inner, probe) as Boolean) supportedFeatures = supportedFeatures or probe
probe += probe
}
return supportedFeatures
}
fun getSupportedChannelList(band: Int) = getSupportedChannelList(inner, band) as IntArray
}

View File

@@ -6,11 +6,15 @@ import android.net.MacAddress
import android.net.wifi.SoftApConfiguration
import android.os.Build
import android.os.Parcelable
import android.util.SparseIntArray
import androidx.annotation.RequiresApi
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.util.ConstantLookup
import be.mygod.vpnhotspot.util.UnblockCentral
import kotlinx.parcelize.Parcelize
import timber.log.Timber
@Parcelize
data class SoftApConfigurationCompat(
@@ -19,13 +23,18 @@ data class SoftApConfigurationCompat(
var bssidAddr: Long? = null,
var passphrase: String? = null,
var isHiddenSsid: Boolean = false,
/**
* To read legacy band/channel pair, use [requireSingleBand]. For easy access, see [getChannel].
*
* You should probably set or modify this field directly only when you want to use bridged AP,
* see also [android.net.wifi.WifiManager.isBridgedApConcurrencySupported].
* Otherwise, use [optimizeChannels] or [setChannel].
*/
@TargetApi(23)
var band: Int = BAND_2GHZ,
@TargetApi(23)
var channel: Int = 0,
var channels: SparseIntArray = SparseIntArray(1).apply { append(BAND_2GHZ, 0) },
var securityType: Int = SoftApConfiguration.SECURITY_TYPE_OPEN,
@TargetApi(30)
var maxNumberOfClients: Int = 0,
var securityType: Int = SoftApConfiguration.SECURITY_TYPE_OPEN,
@TargetApi(28)
var isAutoShutdownEnabled: Boolean = true,
@TargetApi(28)
@@ -36,12 +45,41 @@ data class SoftApConfigurationCompat(
var blockedClientList: List<MacAddress> = emptyList(),
@RequiresApi(30)
var allowedClientList: List<MacAddress> = emptyList(),
@TargetApi(31)
var macRandomizationSetting: Int = RANDOMIZATION_PERSISTENT,
@TargetApi(31)
var isBridgedModeOpportunisticShutdownEnabled: Boolean = true,
@TargetApi(31)
var isIeee80211axEnabled: Boolean = true,
@TargetApi(31)
var isUserConfiguration: Boolean = true,
var underlying: Parcelable? = null) : Parcelable {
companion object {
const val BAND_2GHZ = 1
const val BAND_5GHZ = 2
@TargetApi(30)
const val BAND_6GHZ = 4
const val BAND_ANY = 7
@TargetApi(31)
const val BAND_60GHZ = 8
const val BAND_LEGACY = BAND_2GHZ or BAND_5GHZ
val BAND_TYPES by lazy {
if (Build.VERSION.SDK_INT >= 31) try {
return@lazy UnblockCentral.SoftApConfiguration_BAND_TYPES
} catch (e: ReflectiveOperationException) {
Timber.w(e)
}
intArrayOf(BAND_2GHZ, BAND_5GHZ, BAND_6GHZ, BAND_60GHZ)
}
@RequiresApi(31)
val bandLookup = ConstantLookup<SoftApConfiguration>("BAND_")
@TargetApi(31)
const val RANDOMIZATION_NONE = 0
@TargetApi(31)
const val RANDOMIZATION_PERSISTENT = 1
fun isLegacyEitherBand(band: Int) = band and BAND_LEGACY == BAND_LEGACY
/**
* [android.net.wifi.WifiConfiguration.KeyMgmt.WPA2_PSK]
*/
@@ -53,7 +91,8 @@ data class SoftApConfigurationCompat(
/**
* Based on:
* https://elixir.bootlin.com/linux/v5.7.6/source/net/wireless/util.c#L75
* https://elixir.bootlin.com/linux/v5.12.8/source/net/wireless/util.c#L75
* TODO: update for Android 12
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/wifi/java/android/net/wifi/ScanResult.java;l=624;drc=f7ccda05642b55700d67a288462bada488fc7f5e
*/
fun channelToFrequency(band: Int, chan: Int) = when (band) {
@@ -67,18 +106,24 @@ data class SoftApConfigurationCompat(
in 1..Int.MAX_VALUE -> 5000 + chan * 5
else -> throw IllegalArgumentException("Invalid 5GHz channel $chan")
}
BAND_6GHZ -> if (chan in 1..253) {
5940 + chan * 5
} else throw IllegalArgumentException("Invalid 6GHz channel $chan")
// BAND_60GHZ -> if (chan in 1 until 7) 56160 + chan * 2160
BAND_6GHZ -> when (chan) {
2 -> 5935
in 1..233 -> 5950 + chan * 5
else -> throw IllegalArgumentException("Invalid 6GHz channel $chan")
}
BAND_60GHZ -> {
require(chan in 1 until 7) { "Invalid 60GHz channel $chan" }
56160 + chan * 2160
}
else -> throw IllegalArgumentException("Invalid band $band")
}
fun frequencyToChannel(freq: Int) = when (freq) {
2484 -> 14
in Int.MIN_VALUE until 2484 -> (freq - 2407) / 5
in 4910..4980 -> (freq - 4000) / 5
in Int.MIN_VALUE until 5945 -> (freq - 5000) / 5
in Int.MIN_VALUE..45000 -> (freq - 5940) / 5
in Int.MIN_VALUE until 5925 -> (freq - 5000) / 5
5935 -> 2
in Int.MIN_VALUE..45000 -> (freq - 5950) / 5
in 58320..70200 -> (freq - 56160) / 2160
else -> throw IllegalArgumentException("Invalid frequency $freq")
}
@@ -122,6 +167,14 @@ data class SoftApConfigurationCompat(
private val getChannel by lazy @TargetApi(30) {
SoftApConfiguration::class.java.getDeclaredMethod("getChannel")
}
@get:RequiresApi(31)
private val getChannels by lazy @TargetApi(31) {
SoftApConfiguration::class.java.getDeclaredMethod("getChannels")
}
@get:RequiresApi(31)
private val getMacRandomizationSetting by lazy @TargetApi(31) {
SoftApConfiguration::class.java.getDeclaredMethod("getMacRandomizationSetting")
}
@get:RequiresApi(30)
private val getMaxNumberOfClients by lazy @TargetApi(30) {
SoftApConfiguration::class.java.getDeclaredMethod("getMaxNumberOfClients")
@@ -134,10 +187,22 @@ data class SoftApConfigurationCompat(
private val isAutoShutdownEnabled by lazy @TargetApi(30) {
SoftApConfiguration::class.java.getDeclaredMethod("isAutoShutdownEnabled")
}
@get:RequiresApi(31)
private val isBridgedModeOpportunisticShutdownEnabled by lazy @TargetApi(31) {
SoftApConfiguration::class.java.getDeclaredMethod("isBridgedModeOpportunisticShutdownEnabled")
}
@get:RequiresApi(30)
private val isClientControlByUserEnabled by lazy @TargetApi(30) {
SoftApConfiguration::class.java.getDeclaredMethod("isClientControlByUserEnabled")
}
@get:RequiresApi(31)
private val isIeee80211axEnabled by lazy @TargetApi(31) {
SoftApConfiguration::class.java.getDeclaredMethod("isIeee80211axEnabled")
}
@get:RequiresApi(31)
private val isUserConfiguration by lazy @TargetApi(31) {
SoftApConfiguration::class.java.getDeclaredMethod("isUserConfiguration")
}
@get:RequiresApi(30)
private val classBuilder by lazy { Class.forName("android.net.wifi.SoftApConfiguration\$Builder") }
@@ -159,6 +224,10 @@ data class SoftApConfigurationCompat(
private val setBlockedClientList by lazy {
classBuilder.getDeclaredMethod("setBlockedClientList", java.util.List::class.java)
}
@get:RequiresApi(31)
private val setBridgedModeOpportunisticShutdownEnabled by lazy {
classBuilder.getDeclaredMethod("setBridgedModeOpportunisticShutdownEnabled", Boolean::class.java)
}
@get:RequiresApi(30)
private val setBssid by lazy @TargetApi(30) {
classBuilder.getDeclaredMethod("setBssid", MacAddress::class.java)
@@ -167,12 +236,24 @@ data class SoftApConfigurationCompat(
private val setChannel by lazy {
classBuilder.getDeclaredMethod("setChannel", Int::class.java, Int::class.java)
}
@get:RequiresApi(31)
private val setChannels by lazy {
classBuilder.getDeclaredMethod("setChannels", SparseIntArray::class.java)
}
@get:RequiresApi(30)
private val setClientControlByUserEnabled by lazy {
classBuilder.getDeclaredMethod("setClientControlByUserEnabled", Boolean::class.java)
}
@get:RequiresApi(30)
private val setHiddenSsid by lazy { classBuilder.getDeclaredMethod("setHiddenSsid", Boolean::class.java) }
@get:RequiresApi(31)
private val setIeee80211axEnabled by lazy {
classBuilder.getDeclaredMethod("setIeee80211axEnabled", Boolean::class.java)
}
@get:RequiresApi(31)
private val setMacRandomizationSetting by lazy {
classBuilder.getDeclaredMethod("setMacRandomizationSetting", Int::class.java)
}
@get:RequiresApi(30)
private val setMaxNumberOfClients by lazy {
classBuilder.getDeclaredMethod("setMaxNumberOfClients", Int::class.java)
@@ -187,6 +268,8 @@ data class SoftApConfigurationCompat(
}
@get:RequiresApi(30)
private val setSsid by lazy { classBuilder.getDeclaredMethod("setSsid", String::class.java) }
@get:RequiresApi(31)
private val setUserConfiguration by lazy @TargetApi(31) { UnblockCentral.setUserConfiguration(classBuilder) }
@Deprecated("Class deprecated in framework")
@Suppress("DEPRECATION")
@@ -196,14 +279,14 @@ data class SoftApConfigurationCompat(
preSharedKey,
hiddenSSID,
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/wifi/java/android/net/wifi/SoftApConfToXmlMigrationUtil.java;l=87;drc=aa6527cf41671d1ed417b8ebdb6b3aa614f62344
if (Build.VERSION.SDK_INT >= 23) when (val band = apBand.getInt(this)) {
SparseIntArray(1).also {
if (Build.VERSION.SDK_INT >= 23) it.append(when (val band = apBand.getInt(this)) {
0 -> BAND_2GHZ
1 -> BAND_5GHZ
-1 -> BAND_2GHZ or BAND_5GHZ
-1 -> BAND_LEGACY
else -> throw IllegalArgumentException("Unexpected band $band")
} else BAND_ANY,
if (Build.VERSION.SDK_INT >= 23) apChannel.getInt(this) else 0,
0,
}, apChannel.getInt(this)) else it.append(BAND_LEGACY, 0)
},
allowedKeyManagement.nextSetBit(0).let { selected ->
require(allowedKeyManagement.nextSetBit(selected + 1) < 0) {
"More than 1 key managements supplied: $allowedKeyManagement"
@@ -224,7 +307,7 @@ data class SoftApConfigurationCompat(
}
}
},
if (Build.VERSION.SDK_INT >= 28) TetherTimeoutMonitor.enabled else false,
isAutoShutdownEnabled = if (Build.VERSION.SDK_INT >= 28) TetherTimeoutMonitor.enabled else false,
underlying = this)
@RequiresApi(30)
@@ -234,16 +317,22 @@ data class SoftApConfigurationCompat(
bssid?.toCompat()?.addr,
passphrase,
isHiddenSsid,
getBand(this) as Int,
getChannel(this) as Int,
getMaxNumberOfClients(this) as Int,
if (Build.VERSION.SDK_INT >= 31) getChannels(this) as SparseIntArray else SparseIntArray(1).also {
it.append(getBand(this) as Int, getChannel(this) as Int)
},
securityType,
getMaxNumberOfClients(this) as Int,
isAutoShutdownEnabled(this) as Boolean,
getShutdownTimeoutMillis(this) as Long,
isClientControlByUserEnabled(this) as Boolean,
getBlockedClientList(this) as List<MacAddress>,
getAllowedClientList(this) as List<MacAddress>,
this)
if (Build.VERSION.SDK_INT >= 31) getMacRandomizationSetting(this) as Int else RANDOMIZATION_PERSISTENT,
Build.VERSION.SDK_INT < 31 || isBridgedModeOpportunisticShutdownEnabled(this) as Boolean,
Build.VERSION.SDK_INT < 31 || isIeee80211axEnabled(this) as Boolean,
Build.VERSION.SDK_INT < 31 || isUserConfiguration(this) as Boolean,
this,
)
}
@Suppress("DEPRECATION")
@@ -253,6 +342,47 @@ data class SoftApConfigurationCompat(
bssidAddr = value?.addr
}
/**
* Only single band/channel can be supplied on API 23-30
*/
fun requireSingleBand(): Pair<Int, Int> {
require(channels.size() == 1) { "Unsupported number of bands configured" }
return channels.keyAt(0) to channels.valueAt(0)
}
fun getChannel(band: Int): Int {
var result = -1
repeat(channels.size()) { i ->
if (band and channels.keyAt(i) != band) return@repeat
require(result == -1) { "Duplicate band found" }
result = channels.valueAt(i)
}
return result
}
fun setChannel(channel: Int, band: Int = BAND_LEGACY) {
channels = SparseIntArray(1).apply {
append(when {
channel <= 0 || band != BAND_LEGACY -> band
channel > 14 -> BAND_5GHZ
else -> BAND_2GHZ
}, channel)
}
}
fun optimizeChannels(channels: SparseIntArray = this.channels) {
this.channels = SparseIntArray(channels.size()).apply {
var setBand = 0
repeat(channels.size()) { i -> if (channels.valueAt(i) == 0) setBand = setBand or channels.keyAt(i) }
if (setBand != 0) append(setBand, 0) // merge all bands into one
repeat(channels.size()) { i ->
val band = channels.keyAt(i)
if (band and setBand == 0) put(band, channels.valueAt(i))
}
}
}
fun setMacRandomizationEnabled(enabled: Boolean) {
macRandomizationSetting = if (enabled) RANDOMIZATION_PERSISTENT else RANDOMIZATION_NONE
}
/**
* Based on:
* https://android.googlesource.com/platform/packages/apps/Settings/+/android-5.0.0_r1/src/com/android/settings/wifi/WifiApDialog.java#88
@@ -263,6 +393,7 @@ data class SoftApConfigurationCompat(
@Deprecated("Class deprecated in framework, use toPlatform().toWifiConfiguration()")
@Suppress("DEPRECATION")
fun toWifiConfiguration(): android.net.wifi.WifiConfiguration {
val (band, channel) = requireSingleBand()
val wc = underlying as? android.net.wifi.WifiConfiguration
val result = if (wc == null) android.net.wifi.WifiConfiguration() else android.net.wifi.WifiConfiguration(wc)
val original = wc?.toCompat()
@@ -273,11 +404,14 @@ data class SoftApConfigurationCompat(
apBand.setInt(result, when (band) {
BAND_2GHZ -> 0
BAND_5GHZ -> 1
BAND_2GHZ or BAND_5GHZ, BAND_ANY -> -1
else -> throw IllegalArgumentException("Convert fail, unsupported band setting :$band")
else -> {
require(Build.VERSION.SDK_INT >= 28) { "A band must be specified on this platform" }
require(isLegacyEitherBand(band)) { "Convert fail, unsupported band setting :$band" }
-1
}
})
apChannel.setInt(result, channel)
} else require(band == BAND_ANY) { "Specifying band is unsupported on this platform" }
} else require(isLegacyEitherBand(band)) { "Specifying band is unsupported on this platform" }
if (original?.securityType != securityType) {
result.allowedKeyManagement.clear()
result.allowedKeyManagement.set(when (securityType) {
@@ -304,7 +438,10 @@ data class SoftApConfigurationCompat(
setSsid(builder, ssid)
setPassphrase(builder, if (securityType == SoftApConfiguration.SECURITY_TYPE_OPEN) null else passphrase,
securityType)
if (Build.VERSION.SDK_INT >= 31) setChannels(builder, channels) else {
val (band, channel) = requireSingleBand()
if (channel == 0) setBand(builder, band) else setChannel(builder, channel, band)
}
setBssid(builder, bssid?.toPlatform())
setMaxNumberOfClients(builder, maxNumberOfClients)
setShutdownTimeoutMillis(builder, shutdownTimeoutMillis)
@@ -313,6 +450,16 @@ data class SoftApConfigurationCompat(
setHiddenSsid(builder, isHiddenSsid)
setAllowedClientList(builder, allowedClientList)
setBlockedClientList(builder, blockedClientList)
if (Build.VERSION.SDK_INT >= 31) {
setMacRandomizationSetting(builder, macRandomizationSetting)
setBridgedModeOpportunisticShutdownEnabled(builder, isBridgedModeOpportunisticShutdownEnabled)
setIeee80211axEnabled(builder, isIeee80211axEnabled)
if (sac?.let { isUserConfiguration(it) as Boolean } != false != isUserConfiguration) try {
setUserConfiguration(builder, isUserConfiguration)
} catch (e: ReflectiveOperationException) {
Timber.w(e) // as far as we are concerned, this field is not used anywhere so ignore for now
}
}
return build(builder) as SoftApConfiguration
}

View File

@@ -0,0 +1,45 @@
package be.mygod.vpnhotspot.net.wifi
import android.annotation.TargetApi
import android.net.MacAddress
import android.os.Parcelable
import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.util.ConstantLookup
import be.mygod.vpnhotspot.util.UnblockCentral
import timber.log.Timber
@JvmInline
@RequiresApi(30)
value class SoftApInfo(val inner: Parcelable) {
companion object {
val clazz by lazy { Class.forName("android.net.wifi.SoftApInfo") }
private val getFrequency by lazy { clazz.getDeclaredMethod("getFrequency") }
private val getBandwidth by lazy { clazz.getDeclaredMethod("getBandwidth") }
@get:RequiresApi(31)
private val getBssid by lazy { clazz.getDeclaredMethod("getBssid") }
@get:RequiresApi(31)
private val getWifiStandard by lazy { clazz.getDeclaredMethod("getWifiStandard") }
@get:RequiresApi(31)
private val getApInstanceIdentifier by lazy @TargetApi(31) { UnblockCentral.getApInstanceIdentifier(clazz) }
@get:RequiresApi(31)
private val getAutoShutdownTimeoutMillis by lazy { clazz.getDeclaredMethod("getAutoShutdownTimeoutMillis") }
val channelWidthLookup = ConstantLookup("CHANNEL_WIDTH_") { clazz }
}
val frequency get() = getFrequency(inner) as Int
val bandwidth get() = getBandwidth(inner) as Int
@get:RequiresApi(31)
val bssid get() = getBssid(inner) as MacAddress?
@get:RequiresApi(31)
val wifiStandard get() = getWifiStandard(inner) as Int
@get:RequiresApi(31)
val apInstanceIdentifier get() = try {
getApInstanceIdentifier(inner) as? String
} catch (e: ReflectiveOperationException) {
Timber.w(e)
null
}
@get:RequiresApi(31)
val autoShutdownTimeoutMillis get() = getAutoShutdownTimeoutMillis(inner) as Long
}

View File

@@ -10,10 +10,13 @@ import android.os.Parcelable
import android.text.Editable
import android.text.TextWatcher
import android.util.Base64
import android.util.SparseIntArray
import android.view.MenuItem
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
@@ -30,8 +33,8 @@ import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
import be.mygod.vpnhotspot.util.QRCodeDialog
import be.mygod.vpnhotspot.util.readableMessage
import be.mygod.vpnhotspot.util.showAllowingStateLoss
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.parcelize.Parcelize
import timber.log.Timber
/**
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/39b4674/src/com/android/settings/wifi/WifiApDialog.java
@@ -40,27 +43,30 @@ import kotlinx.parcelize.Parcelize
* Related: https://android.googlesource.com/platform/packages/apps/Settings/+/defb1183ecb00d6231bac7d934d07f58f90261ea
*/
class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiApDialogFragment.Arg>(), TextWatcher,
Toolbar.OnMenuItemClickListener {
Toolbar.OnMenuItemClickListener, AdapterView.OnItemSelectedListener {
companion object {
private const val BASE64_FLAGS = Base64.NO_PADDING or Base64.NO_WRAP
private val nonMacChars = "[^0-9a-fA-F:]+".toRegex()
private val channels by lazy {
val list = ArrayList<BandOption.Channel>()
for (chan in 1..14) list.add(BandOption.Channel(SoftApConfigurationCompat.BAND_2GHZ, chan))
for (chan in 1..196) list.add(BandOption.Channel(SoftApConfigurationCompat.BAND_5GHZ, chan))
if (Build.VERSION.SDK_INT >= 30) {
for (chan in 1..253) list.add(BandOption.Channel(SoftApConfigurationCompat.BAND_6GHZ, chan))
private val baseOptions by lazy { listOf(ChannelOption.Disabled, ChannelOption.Auto) }
private val channels2G by lazy {
baseOptions + (1..14).map { ChannelOption(it, SoftApConfigurationCompat.BAND_2GHZ) }
}
list
private val channels5G by lazy {
baseOptions + (1..196).map { ChannelOption(it, SoftApConfigurationCompat.BAND_5GHZ) }
}
@get:RequiresApi(30)
private val channels6G by lazy {
baseOptions + (1..233).map { ChannelOption(it, SoftApConfigurationCompat.BAND_6GHZ) }
}
@get:RequiresApi(31)
private val channels60G by lazy {
baseOptions + (1..6).map { ChannelOption(it, SoftApConfigurationCompat.BAND_60GHZ) }
}
/**
* Source: https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/c2fc6a1/service/java/com/android/server/wifi/p2p/SupplicantP2pIfaceHal.java#1396
*/
private val p2pChannels by lazy {
(1..165).map {
val band = if (it <= 14) SoftApConfigurationCompat.BAND_2GHZ else SoftApConfigurationCompat.BAND_5GHZ
BandOption.Channel(band, it)
}
baseOptions + (15..165).map { ChannelOption(it, SoftApConfigurationCompat.BAND_5GHZ) }
}
}
@@ -73,35 +79,20 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
*/
val p2pMode: Boolean = false) : Parcelable
private sealed class BandOption {
open val band get() = SoftApConfigurationCompat.BAND_ANY
open val channel get() = 0
object BandAny : BandOption() {
private open class ChannelOption(val channel: Int = 0, private val band: Int = 0) {
object Disabled : ChannelOption(-1) {
override fun toString() = app.getString(R.string.wifi_ap_choose_disabled)
}
object Auto : ChannelOption() {
override fun toString() = app.getString(R.string.wifi_ap_choose_auto)
}
object Band2GHz : BandOption() {
override val band get() = SoftApConfigurationCompat.BAND_2GHZ
override fun toString() = app.getString(R.string.wifi_ap_choose_2G)
}
object Band5GHz : BandOption() {
override val band get() = SoftApConfigurationCompat.BAND_5GHZ
override fun toString() = app.getString(R.string.wifi_ap_choose_5G)
}
@RequiresApi(30)
object Band6GHz : BandOption() {
override val band get() = SoftApConfigurationCompat.BAND_6GHZ
override fun toString() = app.getString(R.string.wifi_ap_choose_6G)
}
class Channel(override val band: Int, override val channel: Int) : BandOption() {
override fun toString() = "${SoftApConfigurationCompat.channelToFrequency(band, channel)} MHz ($channel)"
}
}
private lateinit var dialogView: DialogWifiApBinding
private lateinit var bandOptions: MutableList<BandOption>
private lateinit var base: SoftApConfigurationCompat
private var started = false
private val currentChannels5G get() = if (arg.p2pMode && !RepeaterService.safeMode) p2pChannels else channels5G
override val ret get() = Arg(generateConfig())
private fun generateConfig(full: Boolean = true) = base.copy(
@@ -117,9 +108,17 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
if (text.isNullOrEmpty()) 0 else text.toString().toLong()
}
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) {
val bandOption = dialogView.band.selectedItem as BandOption
band = bandOption.band
channel = bandOption.channel
val channels = SparseIntArray(4)
for ((band, spinner) in arrayOf(SoftApConfigurationCompat.BAND_2GHZ to dialogView.band2G,
SoftApConfigurationCompat.BAND_5GHZ to dialogView.band5G,
SoftApConfigurationCompat.BAND_6GHZ to dialogView.band6G,
SoftApConfigurationCompat.BAND_60GHZ to dialogView.band60G)) {
val channel = (spinner.selectedItem as ChannelOption?)?.channel
if (channel != null && channel >= 0) channels.append(band, channel)
}
if (!arg.p2pMode && Build.VERSION.SDK_INT >= 31 && dialogView.bridgedMode.isChecked) {
this.channels = channels
} else optimizeChannels(channels)
}
bssid = if (dialogView.bssid.length() != 0) {
MacAddressCompat.fromString(dialogView.bssid.text.toString())
@@ -132,6 +131,10 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
.filter { it.isNotEmpty() }.map { MacAddressCompat.fromString(it).toPlatform() }
blockedClientList = (dialogView.blockedList.text ?: "").split(nonMacChars)
.filter { it.isNotEmpty() }.map { MacAddressCompat.fromString(it).toPlatform() }
setMacRandomizationEnabled(dialogView.macRandomization.isChecked)
isBridgedModeOpportunisticShutdownEnabled = dialogView.bridgedModeOpportunisticShutdown.isChecked
isIeee80211axEnabled = dialogView.ieee80211ax.isChecked
isUserConfiguration = dialogView.userConfig.isChecked
}
}
@@ -164,43 +167,69 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
TetherTimeoutMonitor.defaultTimeout)
dialogView.timeout.addTextChangedListener(this@WifiApDialogFragment)
} else dialogView.timeoutWrapper.isGone = true
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) dialogView.band.apply {
bandOptions = mutableListOf<BandOption>().apply {
if (arg.p2pMode) {
add(BandOption.BandAny)
if (RepeaterService.safeMode) {
add(BandOption.Band2GHz)
add(BandOption.Band5GHz)
addAll(channels)
} else addAll(p2pChannels)
} else {
if (Build.VERSION.SDK_INT >= 28) add(BandOption.BandAny)
add(BandOption.Band2GHz)
add(BandOption.Band5GHz)
if (Build.VERSION.SDK_INT >= 30) add(BandOption.Band6GHz)
addAll(channels)
}
}
adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0, bandOptions).apply {
fun Spinner.configure(options: List<ChannelOption>) {
adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0, options).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
} else dialogView.bandWrapper.isGone = true
dialogView.bssid.addTextChangedListener(this@WifiApDialogFragment)
if (arg.p2pMode) dialogView.hiddenSsid.isGone = true
if (arg.p2pMode || Build.VERSION.SDK_INT < 30) {
dialogView.maxClientWrapper.isGone = true
dialogView.clientUserControl.isGone = true
dialogView.blockedListWrapper.isGone = true
dialogView.allowedListWrapper.isGone = true
} else {
onItemSelectedListener = this@WifiApDialogFragment
}
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) {
dialogView.band2G.configure(channels2G)
dialogView.band5G.configure(currentChannels5G)
if (Build.VERSION.SDK_INT >= 30 && !arg.p2pMode) dialogView.band6G.configure(channels6G)
else dialogView.bandWrapper6G.isGone = true
if (Build.VERSION.SDK_INT >= 31 && !arg.p2pMode) dialogView.band60G.configure(channels60G) else {
dialogView.bandWrapper60G.isGone = true
dialogView.bridgedMode.isGone = true
dialogView.bridgedModeOpportunisticShutdown.isGone = true
}
} else dialogView.bandGroup.isGone = true
if (!arg.p2pMode && Build.VERSION.SDK_INT >= 30) {
dialogView.maxClient.addTextChangedListener(this@WifiApDialogFragment)
dialogView.blockedList.addTextChangedListener(this@WifiApDialogFragment)
dialogView.allowedList.addTextChangedListener(this@WifiApDialogFragment)
} else dialogView.accessControlGroup.isGone = true
dialogView.bssid.addTextChangedListener(this@WifiApDialogFragment)
if (arg.p2pMode) dialogView.hiddenSsid.isGone = true
if (arg.p2pMode && Build.VERSION.SDK_INT >= 29) dialogView.macRandomization.isEnabled = false
else if (arg.p2pMode || Build.VERSION.SDK_INT < 31) dialogView.macRandomization.isGone = true
if (arg.p2pMode || Build.VERSION.SDK_INT < 31) {
dialogView.ieee80211ax.isGone = true
dialogView.userConfig.isGone = true
}
base = arg.configuration
populateFromConfiguration()
}
private fun locate(band: Int, channels: List<ChannelOption>): Int {
val channel = base.getChannel(band)
val selection = channels.indexOfFirst { it.channel == channel }
return if (selection == -1) {
Timber.w(Exception("Unable to locate $band, $channel, ${arg.p2pMode && !RepeaterService.safeMode}"))
0
} else selection
}
private var userBridgedMode = false
private fun setBridgedMode(): Boolean {
var auto = 0
var set = 0
for (s in arrayOf(dialogView.band2G, dialogView.band5G, dialogView.band6G,
dialogView.band60G)) when (s.selectedItem) {
is ChannelOption.Auto -> auto = 1
!is ChannelOption.Disabled -> ++set
}
if (auto + set > 1) {
if (dialogView.bridgedMode.isEnabled) {
userBridgedMode = dialogView.bridgedMode.isChecked
dialogView.bridgedMode.isEnabled = false
dialogView.bridgedMode.isChecked = true
}
} else if (!dialogView.bridgedMode.isEnabled) {
dialogView.bridgedMode.isEnabled = true
dialogView.bridgedMode.isChecked = userBridgedMode
}
return auto + set > 0
}
private fun populateFromConfiguration() {
dialogView.ssid.setText(base.ssid)
if (!arg.p2pMode) dialogView.security.setSelection(base.securityType)
@@ -208,10 +237,13 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
dialogView.autoShutdown.isChecked = base.isAutoShutdownEnabled
dialogView.timeout.setText(base.shutdownTimeoutMillis.let { if (it == 0L) "" else it.toString() })
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) {
val selection = if (base.channel != 0) {
bandOptions.indexOfFirst { it.channel == base.channel }
} else bandOptions.indexOfFirst { it.band == base.band }
dialogView.band.setSelection(if (selection == -1) 0 else selection)
dialogView.band2G.setSelection(locate(SoftApConfigurationCompat.BAND_2GHZ, channels2G))
dialogView.band5G.setSelection(locate(SoftApConfigurationCompat.BAND_5GHZ, currentChannels5G))
dialogView.band6G.setSelection(locate(SoftApConfigurationCompat.BAND_6GHZ, channels6G))
dialogView.band60G.setSelection(locate(SoftApConfigurationCompat.BAND_60GHZ, channels60G))
userBridgedMode = base.channels.size() > 1
dialogView.bridgedMode.isChecked = userBridgedMode
setBridgedMode()
}
dialogView.bssid.setText(base.bssid?.toString())
dialogView.hiddenSsid.isChecked = base.isHiddenSsid
@@ -219,6 +251,11 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
dialogView.clientUserControl.isChecked = base.isClientControlByUserEnabled
dialogView.blockedList.setText(base.blockedClientList.joinToString("\n"))
dialogView.allowedList.setText(base.allowedClientList.joinToString("\n"))
dialogView.macRandomization.isChecked =
base.macRandomizationSetting == SoftApConfigurationCompat.RANDOMIZATION_PERSISTENT
dialogView.bridgedModeOpportunisticShutdown.isChecked = base.isBridgedModeOpportunisticShutdownEnabled
dialogView.ieee80211ax.isChecked = base.isIeee80211axEnabled
dialogView.userConfig.isChecked = base.isUserConfiguration
}
override fun onStart() {
@@ -254,6 +291,29 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
}
}
dialogView.timeoutWrapper.error = timeoutError
val isBandValid = when {
arg.p2pMode || Build.VERSION.SDK_INT in 23 until 30 -> {
val option5G = dialogView.band5G.selectedItem
when (dialogView.band2G.selectedItem) {
is ChannelOption.Disabled -> option5G !is ChannelOption.Disabled &&
(!arg.p2pMode || RepeaterService.safeMode || option5G !is ChannelOption.Auto)
is ChannelOption.Auto ->
(arg.p2pMode || Build.VERSION.SDK_INT >= 28) && option5G is ChannelOption.Auto ||
(!arg.p2pMode || RepeaterService.safeMode) && option5G is ChannelOption.Disabled
else -> option5G is ChannelOption.Disabled
}
}
Build.VERSION.SDK_INT == 30 -> {
var expected = 1
var set = 0
for (s in arrayOf(dialogView.band2G, dialogView.band5G, dialogView.band6G)) when (s.selectedItem) {
is ChannelOption.Auto -> expected = 0
!is ChannelOption.Disabled -> ++set
}
set == expected
}
else -> setBridgedMode()
}
dialogView.bssidWrapper.error = null
val bssidValid = dialogView.bssid.length() == 0 || try {
MacAddressCompat.fromString(dialogView.bssid.text.toString())
@@ -290,7 +350,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
val canCopy = timeoutError == null && bssidValid && maxClientError == null && blockedListError == null &&
allowedListError == null
(dialog as? AlertDialog)?.getButton(DialogInterface.BUTTON_POSITIVE)?.isEnabled =
ssidLength in 1..32 && passwordValid && canCopy
ssidLength in 1..32 && passwordValid && isBandValid && canCopy
dialogView.toolbar.menu.findItem(android.R.id.copy).isEnabled = canCopy
}
@@ -298,6 +358,9 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { }
override fun afterTextChanged(editable: Editable) = validate()
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) = validate()
override fun onNothingSelected(parent: AdapterView<*>?) = error("unreachable")
override fun onMenuItemClick(item: MenuItem?): Boolean {
return when (item?.itemId) {
android.R.id.copy -> {
@@ -318,7 +381,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
}
true
} catch (e: RuntimeException) {
SmartSnackbar.make(e).show()
Toast.makeText(context, e.readableMessage, Toast.LENGTH_LONG).show()
false
}
R.id.share_qr -> {

View File

@@ -3,18 +3,16 @@ package be.mygod.vpnhotspot.net.wifi
import android.annotation.TargetApi
import android.content.Intent
import android.content.pm.PackageManager
import android.net.MacAddress
import android.content.res.Resources
import android.net.wifi.SoftApConfiguration
import android.net.wifi.WifiManager
import android.os.Build
import android.os.Handler
import android.os.Parcelable
import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
import be.mygod.vpnhotspot.util.ConstantLookup
import be.mygod.vpnhotspot.util.LongConstantLookup
import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.callSuper
import be.mygod.vpnhotspot.util.*
import timber.log.Timber
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
@@ -27,6 +25,8 @@ object WifiApManager {
*/
@RequiresApi(30)
private const val ACTION_RESOURCES_APK = "com.android.server.wifi.intent.action.SERVICE_WIFI_RESOURCES_APK"
@RequiresApi(30)
const val RESOURCES_PACKAGE = "com.android.wifi.resources"
/**
* Based on: https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/000ad45/service/java/com/android/server/wifi/WifiContext.java#66
*/
@@ -34,6 +34,115 @@ object WifiApManager {
val resolvedActivity get() = app.packageManager.queryIntentActivities(Intent(ACTION_RESOURCES_APK),
PackageManager.MATCH_SYSTEM_ONLY).single()
private const val CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED = "config_wifi_p2p_mac_randomization_supported"
val p2pMacRandomizationSupported get() = when (Build.VERSION.SDK_INT) {
29 -> Resources.getSystem().run {
getBoolean(getIdentifier(CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED, "bool", "android"))
}
in 30..Int.MAX_VALUE -> @TargetApi(30) {
val info = resolvedActivity.activityInfo
val resources = app.packageManager.getResourcesForApplication(info.applicationInfo)
resources.getBoolean(resources.findIdentifier(CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED, "bool",
RESOURCES_PACKAGE, info.packageName))
}
else -> false
}
@get:RequiresApi(30)
private val apMacRandomizationSupported by lazy {
WifiManager::class.java.getDeclaredMethod("isApMacRandomizationSupported")
}
@get:RequiresApi(30)
val isApMacRandomizationSupported get() = apMacRandomizationSupported(Services.wifi) as Boolean
/**
* Broadcast intent action indicating that Wi-Fi AP has been enabled, disabled,
* enabling, disabling, or failed.
*/
const val WIFI_AP_STATE_CHANGED_ACTION = "android.net.wifi.WIFI_AP_STATE_CHANGED"
/**
* The lookup key for an int that indicates whether Wi-Fi AP is enabled,
* disabled, enabling, disabling, or failed. Retrieve it with [Intent.getIntExtra].
*
* @see WIFI_AP_STATE_DISABLED
* @see WIFI_AP_STATE_DISABLING
* @see WIFI_AP_STATE_ENABLED
* @see WIFI_AP_STATE_ENABLING
* @see WIFI_AP_STATE_FAILED
*/
private const val EXTRA_WIFI_AP_STATE = "wifi_state"
/**
* An extra containing the int error code for Soft AP start failure.
* Can be obtained from the [WIFI_AP_STATE_CHANGED_ACTION] using [Intent.getIntExtra].
* This extra will only be attached if [EXTRA_WIFI_AP_STATE] is
* attached and is equal to [WIFI_AP_STATE_FAILED].
*
* The error code will be one of:
* {@link #SAP_START_FAILURE_GENERAL},
* {@link #SAP_START_FAILURE_NO_CHANNEL},
* {@link #SAP_START_FAILURE_UNSUPPORTED_CONFIGURATION}
*
* Source: https://android.googlesource.com/platform/frameworks/base/+/android-6.0.0_r1/wifi/java/android/net/wifi/WifiManager.java#210
*/
@get:RequiresApi(23)
val EXTRA_WIFI_AP_FAILURE_REASON get() =
if (Build.VERSION.SDK_INT >= 30) "android.net.wifi.extra.WIFI_AP_FAILURE_REASON" else "wifi_ap_error_code"
/**
* The lookup key for a String extra that stores the interface name used for the Soft AP.
* This extra is included in the broadcast [WIFI_AP_STATE_CHANGED_ACTION].
* Retrieve its value with [Intent.getStringExtra].
*
* Source: https://android.googlesource.com/platform/frameworks/base/+/android-8.0.0_r1/wifi/java/android/net/wifi/WifiManager.java#413
*/
@get:RequiresApi(26)
val EXTRA_WIFI_AP_INTERFACE_NAME get() =
if (Build.VERSION.SDK_INT >= 30) "android.net.wifi.extra.WIFI_AP_INTERFACE_NAME" else "wifi_ap_interface_name"
fun checkWifiApState(state: Int) = if (state < WIFI_AP_STATE_DISABLING || state > WIFI_AP_STATE_FAILED) {
Timber.w(Exception("Unknown state $state"))
false
} else true
val Intent.wifiApState get() =
getIntExtra(EXTRA_WIFI_AP_STATE, WIFI_AP_STATE_DISABLED).also { checkWifiApState(it) }
/**
* Wi-Fi AP is currently being disabled. The state will change to
* [WIFI_AP_STATE_DISABLED] if it finishes successfully.
*
* @see WIFI_AP_STATE_CHANGED_ACTION
* @see #getWifiApState()
*/
const val WIFI_AP_STATE_DISABLING = 10
/**
* Wi-Fi AP is disabled.
*
* @see WIFI_AP_STATE_CHANGED_ACTION
* @see #getWifiState()
*/
const val WIFI_AP_STATE_DISABLED = 11
/**
* Wi-Fi AP is currently being enabled. The state will change to
* {@link #WIFI_AP_STATE_ENABLED} if it finishes successfully.
*
* @see WIFI_AP_STATE_CHANGED_ACTION
* @see #getWifiApState()
*/
const val WIFI_AP_STATE_ENABLING = 12
/**
* Wi-Fi AP is enabled.
*
* @see WIFI_AP_STATE_CHANGED_ACTION
* @see #getWifiApState()
*/
const val WIFI_AP_STATE_ENABLED = 13
/**
* Wi-Fi AP is in a failed state. This state will occur when an error occurs during
* enabling or disabling
*
* @see WIFI_AP_STATE_CHANGED_ACTION
* @see #getWifiApState()
*/
const val WIFI_AP_STATE_FAILED = 14
private val getWifiApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getWifiApConfiguration") }
@Suppress("DEPRECATION")
private val setWifiApConfiguration by lazy {
@@ -63,40 +172,65 @@ object WifiApManager {
/**
* Called when soft AP state changes.
*
* @param state new new AP state. One of {@link #WIFI_AP_STATE_DISABLED},
* {@link #WIFI_AP_STATE_DISABLING}, {@link #WIFI_AP_STATE_ENABLED},
* {@link #WIFI_AP_STATE_ENABLING}, {@link #WIFI_AP_STATE_FAILED}
* @param state the new AP state. One of [WIFI_AP_STATE_DISABLED], [WIFI_AP_STATE_DISABLING],
* [WIFI_AP_STATE_ENABLED], [WIFI_AP_STATE_ENABLING], [WIFI_AP_STATE_FAILED]
* @param failureReason reason when in failed state. One of
* {@link #SAP_START_FAILURE_GENERAL}, {@link #SAP_START_FAILURE_NO_CHANNEL}
* {@link #SAP_START_FAILURE_GENERAL},
* {@link #SAP_START_FAILURE_NO_CHANNEL},
* {@link #SAP_START_FAILURE_UNSUPPORTED_CONFIGURATION}
*/
fun onStateChanged(state: Int, failureReason: Int) { }
/**
* Called when number of connected clients to soft AP changes.
*
* It is not recommended to use this legacy method on API 30+.
*
* @param numClients number of connected clients
*/
@Deprecated("onConnectedClientsChanged")
fun onNumClientsChanged(numClients: Int) { }
/**
* Called when the connected clients to soft AP changes.
*
* @param clients the currently connected clients
*/
@RequiresApi(30)
fun onConnectedClientsChanged(clients: List<MacAddress>) {
@Suppress("DEPRECATION")
onNumClientsChanged(clients.size)
fun onConnectedClientsChanged(clients: List<Parcelable>) = onNumClientsChanged(clients.size)
/**
* Called when information of softap changes.
*
* @param info is the softap information. [SoftApInfo]
* At most one will be returned on API 30.
*/
@RequiresApi(30)
fun onInfoChanged(info: List<Parcelable>) { }
/**
* Called when capability of softap changes.
*
* @param capability is the softap capability. [SoftApCapability]
*/
@RequiresApi(30)
fun onCapabilityChanged(capability: Parcelable) { }
/**
* Called when client trying to connect but device blocked the client with specific reason.
*
* Can be used to ask user to update client to allowed list or blocked list
* when reason is {@link SAP_CLIENT_BLOCK_REASON_CODE_BLOCKED_BY_USER}, or
* indicate the block due to maximum supported client number limitation when reason is
* {@link SAP_CLIENT_BLOCK_REASON_CODE_NO_MORE_STAS}.
*
* @param client the currently blocked client.
* @param blockedReason one of blocked reason from [SapClientBlockedReason]
*/
@RequiresApi(30)
fun onBlockedClientConnecting(client: Parcelable, blockedReason: Int) { }
}
@RequiresApi(30)
fun onInfoChanged(frequency: Int, bandwidth: Int) { }
@RequiresApi(30)
fun onCapabilityChanged(maxSupportedClients: Int, supportedFeatures: Long) { }
@RequiresApi(30)
fun onBlockedClientConnecting(client: MacAddress, blockedReason: Int) { }
}
@RequiresApi(28)
val failureReasonLookup = ConstantLookup<WifiManager>("SAP_START_FAILURE_",
"SAP_START_FAILURE_GENERAL", "SAP_START_FAILURE_NO_CHANNEL")
@RequiresApi(23)
val failureReasonLookup = ConstantLookup<WifiManager>("SAP_START_FAILURE_", "GENERAL", "NO_CHANNEL")
@get:RequiresApi(30)
val clientBlockLookup by lazy { ConstantLookup<WifiManager>("SAP_CLIENT_") }
@@ -111,25 +245,6 @@ object WifiApManager {
WifiManager::class.java.getDeclaredMethod("unregisterSoftApCallback", interfaceSoftApCallback)
}
private val getMacAddress by lazy {
Class.forName("android.net.wifi.WifiClient").getDeclaredMethod("getMacAddress")
}
private val classSoftApInfo by lazy { Class.forName("android.net.wifi.SoftApInfo") }
private val getFrequency by lazy { classSoftApInfo.getDeclaredMethod("getFrequency") }
private val getBandwidth by lazy { classSoftApInfo.getDeclaredMethod("getBandwidth") }
@RequiresApi(30)
val channelWidthLookup = ConstantLookup("CHANNEL_WIDTH_") { classSoftApInfo }
const val CHANNEL_WIDTH_INVALID = 0
private val classSoftApCapability by lazy { Class.forName("android.net.wifi.SoftApCapability") }
private val getMaxSupportedClients by lazy { classSoftApCapability.getDeclaredMethod("getMaxSupportedClients") }
private val areFeaturesSupported by lazy {
classSoftApCapability.getDeclaredMethod("areFeaturesSupported", Long::class.java)
}
@get:RequiresApi(30)
val featureLookup by lazy { LongConstantLookup(classSoftApCapability, "SOFTAP_FEATURE_") }
@RequiresApi(28)
fun registerSoftApCallback(callback: SoftApCallbackCompat, executor: Executor): Any {
val proxy = Proxy.newProxyInstance(interfaceSoftApCallback.classLoader,
@@ -141,49 +256,37 @@ object WifiApManager {
} else invokeActual(proxy, method, args)
private fun invokeActual(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
val noArgs = args?.size ?: 0
return when (val name = method.name) {
"onStateChanged" -> {
if (noArgs != 2) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
return when {
method.matches("onStateChanged", Integer.TYPE, Integer.TYPE) -> {
callback.onStateChanged(args!![0] as Int, args[1] as Int)
}
"onNumClientsChanged" -> @Suppress("DEPRECATION") {
method.matches("onNumClientsChanged", Integer.TYPE) -> @Suppress("DEPRECATION") {
if (Build.VERSION.SDK_INT >= 30) Timber.w(Exception("Unexpected onNumClientsChanged"))
if (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
callback.onNumClientsChanged(args!![0] as Int)
}
"onConnectedClientsChanged" -> @TargetApi(30) {
method.matches1<java.util.List<*>>("onConnectedClientsChanged") -> @TargetApi(30) {
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onConnectedClientsChanged"))
if (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
callback.onConnectedClientsChanged((args!![0] as? Iterable<*> ?: return null)
.map { getMacAddress(it) as MacAddress })
@Suppress("UNCHECKED_CAST")
callback.onConnectedClientsChanged(args!![0] as List<Parcelable>)
}
"onInfoChanged" -> @TargetApi(30) {
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onInfoChanged"))
if (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
val softApInfo = args!![0]
if (softApInfo != null && classSoftApInfo.isAssignableFrom(softApInfo.javaClass)) {
callback.onInfoChanged(getFrequency(softApInfo) as Int, getBandwidth(softApInfo) as Int)
} else null
method.matches1<java.util.List<*>>("onInfoChanged") -> @TargetApi(31) {
if (Build.VERSION.SDK_INT < 31) Timber.w(Exception("Unexpected onInfoChanged API 31+"))
@Suppress("UNCHECKED_CAST")
callback.onInfoChanged(args!![0] as List<Parcelable>)
}
"onCapabilityChanged" -> @TargetApi(30) {
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onCapabilityChanged"))
if (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
val softApCapability = args!![0]
var supportedFeatures = 0L
var probe = 1L
while (probe != 0L) {
if (areFeaturesSupported(softApCapability, probe) as Boolean) {
supportedFeatures = supportedFeatures or probe
Build.VERSION.SDK_INT >= 30 && method.matches("onInfoChanged", SoftApInfo.clazz) -> {
if (Build.VERSION.SDK_INT >= 31) return null // ignore old version calls
val arg = args!![0]
val info = SoftApInfo(arg as Parcelable)
callback.onInfoChanged( // check for legacy empty info with CHANNEL_WIDTH_INVALID
if (info.frequency == 0 && info.bandwidth == 0) emptyList() else listOf(arg))
}
probe += probe
Build.VERSION.SDK_INT >= 30 && method.matches("onCapabilityChanged", SoftApCapability.clazz) -> {
callback.onCapabilityChanged(args!![0] as Parcelable)
}
callback.onCapabilityChanged(getMaxSupportedClients(softApCapability) as Int, supportedFeatures)
}
"onBlockedClientConnecting" -> @TargetApi(30) {
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onBlockedClientConnecting"))
if (noArgs != 2) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
callback.onBlockedClientConnecting(getMacAddress(args!![0]) as MacAddress, args[1] as Int)
Build.VERSION.SDK_INT >= 30 && method.matches("onBlockedClientConnecting", WifiClient.clazz,
Int::class.java) -> {
callback.onBlockedClientConnecting(args!![0] as Parcelable, args[1] as Int)
}
else -> callSuper(interfaceSoftApCallback, proxy, method, args)
}

View File

@@ -0,0 +1,28 @@
package be.mygod.vpnhotspot.net.wifi
import android.annotation.TargetApi
import android.net.MacAddress
import android.os.Parcelable
import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.util.UnblockCentral
import timber.log.Timber
@JvmInline
@RequiresApi(30)
value class WifiClient(val inner: Parcelable) {
companion object {
val clazz by lazy { Class.forName("android.net.wifi.WifiClient") }
private val getMacAddress by lazy { clazz.getDeclaredMethod("getMacAddress") }
@get:RequiresApi(31)
private val getApInstanceIdentifier by lazy @TargetApi(31) { UnblockCentral.getApInstanceIdentifier(clazz) }
}
val macAddress get() = getMacAddress(inner) as MacAddress
@get:RequiresApi(31)
val apInstanceIdentifier get() = try {
getApInstanceIdentifier(inner) as? String
} catch (e: ReflectiveOperationException) {
Timber.w(e)
null
}
}

View File

@@ -9,8 +9,8 @@ import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.util.callSuper
import be.mygod.vpnhotspot.util.matchesCompat
import kotlinx.coroutines.CompletableDeferred
import timber.log.Timber
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy
@@ -96,12 +96,11 @@ object WifiP2pManagerHelper {
return result.future.await()
}
private val interfacePersistentGroupInfoListener by lazy @SuppressLint("PrivateApi") {
private val interfacePersistentGroupInfoListener by lazy {
Class.forName("android.net.wifi.p2p.WifiP2pManager\$PersistentGroupInfoListener")
}
private val getGroupList by lazy @SuppressLint("PrivateApi") {
Class.forName("android.net.wifi.p2p.WifiP2pGroupList").getDeclaredMethod("getGroupList")
}
private val classWifiP2pGroupList by lazy { Class.forName("android.net.wifi.p2p.WifiP2pGroupList") }
private val getGroupList by lazy { classWifiP2pGroupList.getDeclaredMethod("getGroupList") }
private val requestPersistentGroupInfo by lazy {
WifiP2pManager::class.java.getDeclaredMethod("requestPersistentGroupInfo",
WifiP2pManager.Channel::class.java, interfacePersistentGroupInfoListener)
@@ -112,15 +111,13 @@ object WifiP2pManagerHelper {
* Requires one of NETWORK_SETTING, NETWORK_STACK, or READ_WIFI_CREDENTIAL permission since API 30.
*
* @param c is the channel created at {@link #initialize}
* @param listener for callback when persistent group info list is available. Can be null.
*/
suspend fun WifiP2pManager.requestPersistentGroupInfo(c: WifiP2pManager.Channel): Collection<WifiP2pGroup> {
val result = CompletableDeferred<Collection<WifiP2pGroup>>()
requestPersistentGroupInfo(this, c, Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader,
arrayOf(interfacePersistentGroupInfoListener), object : InvocationHandler {
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? = when (method.name) {
"onPersistentGroupInfoAvailable" -> {
if (args?.size != 1) Timber.w(IllegalArgumentException("Unexpected args: $args"))
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? = when {
method.matchesCompat("onPersistentGroupInfoAvailable", args, classWifiP2pGroupList) -> {
@Suppress("UNCHECKED_CAST")
result.complete(getGroupList(args!![0]) as Collection<WifiP2pGroup>)
}

View File

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

View File

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

View File

@@ -13,8 +13,8 @@ import androidx.preference.Preference
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor
import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
import be.mygod.vpnhotspot.util.SpanFormatter
import be.mygod.vpnhotspot.util.allRoutes
import be.mygod.vpnhotspot.util.format
import be.mygod.vpnhotspot.util.parseNumericAddress
import timber.log.Timber
@@ -73,7 +73,7 @@ class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(co
}
private fun onUpdate() = (context as LifecycleOwner).lifecycleScope.launchWhenStarted {
summary = SpanFormatter.format(context.getText(R.string.settings_service_upstream_monitor_summary),
primary.charSequence, fallback.charSequence)
summary = context.getText(R.string.settings_service_upstream_monitor_summary).format(
context.resources.configuration.locale, primary.charSequence, fallback.charSequence)
}
}

View File

@@ -102,8 +102,8 @@ class ProcessListener(private val terminateRegex: Regex,
try {
launch(parent) {
try {
process.inputStream.bufferedReader().useLines {
for (line in it) {
process.inputStream.bufferedReader().useLines { lines ->
for (line in lines) {
trySend(ProcessData.StdoutLine(line)).onClosed { return@useLines }.onFailure { throw it!! }
if (terminateRegex.containsMatchIn(line)) process.destroy()
}
@@ -112,8 +112,8 @@ class ProcessListener(private val terminateRegex: Regex,
}
launch(parent) {
try {
process.errorStream.bufferedReader().useLines {
for (line in it) trySend(ProcessData.StdoutLine(line)).onClosed {
process.errorStream.bufferedReader().useLines { lines ->
for (line in lines) trySend(ProcessData.StdoutLine(line)).onClosed {
return@useLines
}.onFailure { throw it!! }
}

View File

@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot.root
import android.annotation.SuppressLint
import android.os.Parcelable
import android.util.Log
import be.mygod.librootkotlinx.*
@@ -13,6 +14,7 @@ object RootManager : RootSession(), Logger {
class RootInit : RootCommandNoResult {
override suspend fun execute(): Parcelable? {
Timber.plant(object : Timber.DebugTree() {
@SuppressLint("LogNotTimber")
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
if (priority >= Log.WARN) {
System.err.println("$priority/$tag: $message")

View File

@@ -1,7 +1,6 @@
package be.mygod.vpnhotspot.root
import android.os.Parcelable
import android.util.Log
import be.mygod.librootkotlinx.RootCommand
import be.mygod.librootkotlinx.RootCommandOneWay
import be.mygod.vpnhotspot.net.Routing
@@ -10,6 +9,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import timber.log.Timber
object RoutingCommands {
@Parcelize
@@ -20,7 +20,7 @@ object RoutingCommands {
process.outputStream.bufferedWriter().use(Routing.Companion::appendCleanCommands)
when (val code = process.waitFor()) {
0 -> { }
else -> Log.d("RoutingCommands.Clean", "Unexpected exit code $code")
else -> Timber.w("Unexpected exit code $code")
}
check(process.waitFor() == 0)
}

View File

@@ -1,13 +1,18 @@
package be.mygod.vpnhotspot.root
import android.net.MacAddress
import android.annotation.TargetApi
import android.content.ClipData
import android.os.Build
import android.os.Parcelable
import androidx.annotation.RequiresApi
import be.mygod.librootkotlinx.ParcelableBoolean
import be.mygod.librootkotlinx.RootCommand
import be.mygod.librootkotlinx.RootCommandChannel
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.net.wifi.WifiClient
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
@@ -26,32 +31,29 @@ object WifiApCommands {
}
@Parcelize
data class OnNumClientsChanged(val numClients: Int) : SoftApCallbackParcel() {
@Suppress("DEPRECATION")
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
callback.onNumClientsChanged(numClients)
}
@Parcelize
@RequiresApi(30)
data class OnConnectedClientsChanged(val clients: List<MacAddress>) : SoftApCallbackParcel() {
data class OnConnectedClientsChanged(val clients: List<Parcelable>) : SoftApCallbackParcel() {
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
callback.onConnectedClientsChanged(clients)
}
@Parcelize
@RequiresApi(30)
data class OnInfoChanged(val frequency: Int, val bandwidth: Int) : SoftApCallbackParcel() {
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
callback.onInfoChanged(frequency, bandwidth)
data class OnInfoChanged(val info: List<Parcelable>) : SoftApCallbackParcel() {
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) = callback.onInfoChanged(info)
}
@Parcelize
@RequiresApi(30)
data class OnCapabilityChanged(val maxSupportedClients: Int,
val supportedFeatures: Long) : SoftApCallbackParcel() {
data class OnCapabilityChanged(val capability: Parcelable) : SoftApCallbackParcel() {
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
callback.onCapabilityChanged(maxSupportedClients, supportedFeatures)
callback.onCapabilityChanged(capability)
}
@Parcelize
@RequiresApi(30)
data class OnBlockedClientConnecting(val client: MacAddress, val blockedReason: Int) : SoftApCallbackParcel() {
data class OnBlockedClientConnecting(val client: Parcelable, val blockedReason: Int) : SoftApCallbackParcel() {
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
callback.onBlockedClientConnecting(client, blockedReason)
}
@@ -60,7 +62,7 @@ object WifiApCommands {
@Parcelize
@RequiresApi(28)
class RegisterSoftApCallback : RootCommandChannel<SoftApCallbackParcel> {
override fun create(scope: CoroutineScope) = scope.produce<SoftApCallbackParcel>(capacity = capacity) {
override fun create(scope: CoroutineScope) = scope.produce(capacity = capacity) {
val finish = CompletableDeferred<Unit>()
val key = WifiApManager.registerSoftApCallback(object : WifiApManager.SoftApCallbackCompat {
private fun push(parcel: SoftApCallbackParcel) {
@@ -71,20 +73,18 @@ object WifiApCommands {
override fun onStateChanged(state: Int, failureReason: Int) =
push(SoftApCallbackParcel.OnStateChanged(state, failureReason))
@Suppress("OverridingDeprecatedMember")
override fun onNumClientsChanged(numClients: Int) =
push(SoftApCallbackParcel.OnNumClientsChanged(numClients))
@RequiresApi(30)
override fun onConnectedClientsChanged(clients: List<MacAddress>) =
override fun onConnectedClientsChanged(clients: List<Parcelable>) =
push(SoftApCallbackParcel.OnConnectedClientsChanged(clients))
@RequiresApi(30)
override fun onInfoChanged(frequency: Int, bandwidth: Int) =
push(SoftApCallbackParcel.OnInfoChanged(frequency, bandwidth))
override fun onInfoChanged(info: List<Parcelable>) = push(SoftApCallbackParcel.OnInfoChanged(info))
@RequiresApi(30)
override fun onCapabilityChanged(maxSupportedClients: Int, supportedFeatures: Long) =
push(SoftApCallbackParcel.OnCapabilityChanged(maxSupportedClients, supportedFeatures))
override fun onCapabilityChanged(capability: Parcelable) =
push(SoftApCallbackParcel.OnCapabilityChanged(capability))
@RequiresApi(30)
override fun onBlockedClientConnecting(client: MacAddress, blockedReason: Int) =
override fun onBlockedClientConnecting(client: Parcelable, blockedReason: Int) =
push(SoftApCallbackParcel.OnBlockedClientConnecting(client, blockedReason))
}) {
scope.launch {
@@ -125,8 +125,21 @@ object WifiApCommands {
}
is SoftApCallbackParcel.OnInfoChanged -> synchronized(callbacks) { lastCallback.info = parcel }
is SoftApCallbackParcel.OnCapabilityChanged -> synchronized(callbacks) { lastCallback.capability = parcel }
is SoftApCallbackParcel.OnBlockedClientConnecting -> @TargetApi(30) { // passively consume events
val client = WifiClient(parcel.client)
val macAddress = client.macAddress
var name = macAddress.toString()
if (Build.VERSION.SDK_INT >= 31) client.apInstanceIdentifier?.let { name += "%$it" }
val reason = WifiApManager.clientBlockLookup(parcel.blockedReason, true)
Timber.i("$name blocked from connecting: $reason (${parcel.blockedReason})")
SmartSnackbar.make(app.getString(R.string.tethering_manage_wifi_client_blocked, name, reason)).apply {
action(R.string.tethering_manage_wifi_copy_mac) {
app.clipboard.setPrimaryClip(ClipData.newPlainText(null, macAddress.toString()))
}
for (callback in synchronized(callbacks) { callbacks }) parcel.dispatch(callback)
}.show()
}
}
for (callback in synchronized(callbacks) { callbacks.toList() }) parcel.dispatch(callback)
}
@RequiresApi(28)
fun registerSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) {
@@ -142,8 +155,8 @@ object WifiApCommands {
SmartSnackbar.make(e).show()
}
}
lastCallback
} else null
null
} else lastCallback
}?.toSequence()?.forEach { it?.dispatch(callback) }
@RequiresApi(28)
fun unregisterSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) {

View File

@@ -7,12 +7,12 @@ import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R
import timber.log.Timber
class ConstantLookup(private val prefix: String, private val lookup29: Array<out String>,
class ConstantLookup(private val prefix: String, private val lookup29: Array<out String?>,
private val clazz: () -> Class<*>) {
private val lookup by lazy {
SparseArrayCompat<String>().apply {
for (field in clazz().declaredFields) try {
if (field.name.startsWith(prefix)) put(field.getInt(null), field.name)
if (field.type == Int::class.java && field.name.startsWith(prefix)) put(field.getInt(null), field.name)
} catch (e: Exception) {
Timber.w(e)
}
@@ -25,22 +25,22 @@ class ConstantLookup(private val prefix: String, private val lookup29: Array<out
} catch (e: ReflectiveOperationException) {
Timber.w(e)
}
return lookup29.getOrNull(reason)?.let { if (trimPrefix) it.substring(prefix.length) else it }
return lookup29.getOrNull(reason)?.let { if (trimPrefix) it else prefix + it }
?: app.getString(R.string.failure_reason_unknown, reason)
}
}
@Suppress("FunctionName")
fun ConstantLookup(prefix: String, vararg lookup29: String, clazz: () -> Class<*>) =
fun ConstantLookup(prefix: String, vararg lookup29: String?, clazz: () -> Class<*>) =
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 }
class LongConstantLookup(private val clazz: Class<*>, private val prefix: String) {
private val lookup = LongSparseArray<String>().apply {
for (field in clazz.declaredFields) try {
if (field.name.startsWith(prefix)) put(field.getLong(null), field.name)
if (field.type == Long::class.java && field.name.startsWith(prefix)) put(field.getLong(null), field.name)
} catch (e: Exception) {
Timber.w(e)
}

View File

@@ -1,15 +1,12 @@
package be.mygod.vpnhotspot.util
import android.annotation.SuppressLint
import android.content.Context
import android.net.ConnectivityManager
import android.net.wifi.WifiManager
import android.net.wifi.p2p.WifiP2pManager
import android.util.Log
import androidx.core.content.getSystemService
import timber.log.Timber
@SuppressLint("LogNotTimber")
object Services {
private lateinit var contextInit: () -> Context
val context by lazy { contextInit() }
@@ -22,7 +19,7 @@ object Services {
try {
context.getSystemService<WifiP2pManager>()
} catch (e: RuntimeException) {
if (android.os.Process.myUid() == 0) Log.w("WifiP2pManager", e) else Timber.w(e)
Timber.w(e)
null
}
}

View File

@@ -1,84 +0,0 @@
package be.mygod.vpnhotspot.util
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.SpannedString
import java.util.*
/**
* Provides [String.format] style functions that work with [Spanned] strings and preserve formatting.
*
* https://github.com/george-steel/android-utils/blob/289aff11e53593a55d780f9f5986e49343a79e55/src/org/oshkimaadziig/george/androidutils/SpanFormatter.java
*
* @author George T. Steel
*/
object SpanFormatter {
private val formatSequence = "%([0-9]+\\$|<?)([^a-zA-z%]*)([[a-zA-Z%]&&[^tT]]|[tT][a-zA-Z])".toPattern()
/**
* Version of [String.format] that works on [Spanned] strings to preserve rich text formatting.
* Both the `format` as well as any `%s args` can be Spanned and will have their formatting preserved.
* Due to the way [Spannable]s work, any argument's spans will can only be included **once** in the result.
* Any duplicates will appear as text only.
*
* @param format the format string (see [java.util.Formatter.format])
* @param args
* the list of arguments passed to the formatter. If there are
* more arguments than required by `format`,
* additional arguments are ignored.
* @return the formatted string (with spans).
*/
fun format(format: CharSequence, vararg args: Any) = format(Locale.getDefault(), format, *args)
/**
* Version of [String.format] that works on [Spanned] strings to preserve rich text formatting.
* Both the `format` as well as any `%s args` can be Spanned and will have their formatting preserved.
* Due to the way [Spannable]s work, any argument's spans will can only be included **once** in the result.
* Any duplicates will appear as text only.
*
* @param locale
* the locale to apply; `null` value means no localization.
* @param format the format string (see [java.util.Formatter.format])
* @param args
* the list of arguments passed to the formatter.
* @return the formatted string (with spans).
* @see String.format
*/
fun format(locale: Locale, format: CharSequence, vararg args: Any): SpannedString {
val out = SpannableStringBuilder(format)
var i = 0
var argAt = -1
while (i < out.length) {
val m = formatSequence.matcher(out)
if (!m.find(i)) break
i = m.start()
val exprEnd = m.end()
val argTerm = m.group(1)!!
val modTerm = m.group(2)
val cookedArg = when (val typeTerm = m.group(3)) {
"%" -> "%"
"n" -> "\n"
else -> {
val argItem = args[when (argTerm) {
"" -> ++argAt
"<" -> argAt
else -> Integer.parseInt(argTerm.substring(0, argTerm.length - 1)) - 1
}]
if (typeTerm == "s" && argItem is Spanned) argItem else {
String.format(locale, "%$modTerm$typeTerm", argItem)
}
}
}
out.replace(i, exprEnd, cookedArg)
i += cookedArg.length
}
return SpannedString(out)
}
}

View File

@@ -0,0 +1,49 @@
package be.mygod.vpnhotspot.util
import android.annotation.SuppressLint
import android.net.wifi.SoftApConfiguration
import android.net.wifi.p2p.WifiP2pConfig
import android.os.Build
import androidx.annotation.RequiresApi
import java.lang.reflect.Method
/**
* The central object for accessing all the useful blocked APIs. Thanks Google!
*
* Lazy cannot be used directly as it will create inner classes.
*/
@SuppressLint("BlockedPrivateApi", "DiscouragedPrivateApi")
@Suppress("FunctionName")
object UnblockCentral {
/**
* Retrieve this property before doing dangerous shit.
*/
private val init by lazy {
if (Build.VERSION.SDK_INT < 28) return@lazy
// TODO: fix this not working when targeting API 30+
val getDeclaredMethod = Class::class.java.getDeclaredMethod("getDeclaredMethod",
String::class.java, arrayOf<Class<*>>()::class.java)
val clazz = Class.forName("dalvik.system.VMRuntime")
val setHiddenApiExemptions = getDeclaredMethod(clazz, "setHiddenApiExemptions",
arrayOf(Array<String>::class.java)) as Method
setHiddenApiExemptions(clazz.getDeclaredMethod("getRuntime")(null), arrayOf(""))
}
@RequiresApi(31)
fun setUserConfiguration(clazz: Class<*>) = init.let {
clazz.getDeclaredMethod("setUserConfiguration", Boolean::class.java)
}
@get:RequiresApi(31)
val SoftApConfiguration_BAND_TYPES get() = init.let {
SoftApConfiguration::class.java.getDeclaredField("BAND_TYPES").get(null) as IntArray
}
@RequiresApi(31)
fun getApInstanceIdentifier(clazz: Class<*>) = init.let { clazz.getDeclaredMethod("getApInstanceIdentifier") }
@get:RequiresApi(29)
val WifiP2pConfig_Builder_mNetworkName get() = init.let {
WifiP2pConfig.Builder::class.java.getDeclaredField("mNetworkName").apply { isAccessible = true }
}
}

View File

@@ -4,17 +4,13 @@ import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.content.*
import android.content.res.Resources
import android.net.InetAddresses
import android.net.LinkProperties
import android.net.RouteInfo
import android.net.*
import android.os.Build
import android.os.RemoteException
import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.*
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
@@ -39,6 +35,7 @@ import java.lang.reflect.Method
import java.net.InetAddress
import java.net.NetworkInterface
import java.net.SocketException
import java.util.*
tailrec fun Throwable.getRootCause(): Throwable {
if (this is InvocationTargetException || this is RemoteException) return (cause ?: return this).getRootCause()
@@ -56,6 +53,19 @@ fun Long.toPluralInt(): Int {
return (this % 1000000000).toInt() + 1000000000
}
@RequiresApi(26)
fun Method.matches(name: String, vararg classes: Class<*>) = this.name == name && parameterCount == classes.size &&
classes.indices.all { i -> parameters[i].type == classes[i] }
@RequiresApi(26)
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 Context.ensureReceiverUnregistered(receiver: BroadcastReceiver) {
try {
unregisterReceiver(receiver)
@@ -80,6 +90,61 @@ fun setVisibility(view: View, value: Boolean) {
view.isVisible = value
}
private val formatSequence = "%([0-9]+\\$|<?)([^a-zA-z%]*)([[a-zA-Z%]&&[^tT]]|[tT][a-zA-Z])".toPattern()
/**
* Version of [String.format] that works on [Spanned] strings to preserve rich text formatting.
* Both the `format` as well as any `%s args` can be Spanned and will have their formatting preserved.
* Due to the way [Spannable]s work, any argument's spans will can only be included **once** in the result.
* Any duplicates will appear as text only.
*
* See also: https://github.com/george-steel/android-utils/blob/289aff11e53593a55d780f9f5986e49343a79e55/src/org/oshkimaadziig/george/androidutils/SpanFormatter.java
*
* @param locale
* the locale to apply; `null` value means no localization.
* @param args
* the list of arguments passed to the formatter.
* @return the formatted string (with spans).
* @see String.format
* @author George T. Steel
*/
fun CharSequence.format(locale: Locale, vararg args: Any) = SpannableStringBuilder(this).apply {
var i = 0
var argAt = -1
while (i < length) {
val m = formatSequence.matcher(this)
if (!m.find(i)) break
i = m.start()
val exprEnd = m.end()
val argTerm = m.group(1)!!
val modTerm = m.group(2)
val cookedArg = when (val typeTerm = m.group(3)) {
"%" -> "%"
"n" -> "\n"
else -> {
val argItem = args[when (argTerm) {
"" -> ++argAt
"<" -> argAt
else -> Integer.parseInt(argTerm.substring(0, argTerm.length - 1)) - 1
}]
if (typeTerm == "s" && argItem is Spanned) argItem else {
String.format(locale, "%$modTerm$typeTerm", argItem)
}
}
}
replace(i, exprEnd, cookedArg)
i += cookedArg.length
}
}
fun <T> Iterable<T>.joinToSpanned(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "",
limit: Int = -1, truncated: CharSequence = "...",
transform: ((T) -> CharSequence)? = null) =
joinTo(SpannableStringBuilder(), separator, prefix, postfix, limit, truncated, transform)
fun <T> Sequence<T>.joinToSpanned(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "",
limit: Int = -1, truncated: CharSequence = "...",
transform: ((T) -> CharSequence)? = null) =
joinTo(SpannableStringBuilder(), separator, prefix, postfix, limit, truncated, transform)
fun makeIpSpan(ip: InetAddress) = ip.hostAddress.let {
// exclude all bogon IP addresses supported by Android APIs
if (!app.hasTouch || ip.isMulticastAddress || ip.isAnyLocalAddress || ip.isLoopbackAddress ||
@@ -184,6 +249,14 @@ fun InvocationHandler.callSuper(interfaceClass: Class<*>, proxy: Any, method: Me
}
}
fun globalNetworkRequestBuilder() = NetworkRequest.Builder().apply {
if (Build.VERSION.SDK_INT >= 31) setIncludeOtherUidNetworks(true) else if (Build.VERSION.SDK_INT == 23) {
// 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)

View File

@@ -37,6 +37,8 @@
android:layout_height="wrap_content"
android:descendantFocusability="beforeDescendants"
android:focusableInTouchMode="true"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
style="@style/wifi_item">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/ssid_wrapper"
@@ -119,8 +121,49 @@
android:inputType="number"
android:maxLength="19" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:id="@+id/band_wrapper"
android:id="@+id/band_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_divider" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_subhead"
android:text="@string/wifi_hotspot_ap_band_title" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
style="@style/wifi_item_label"
android:text="@string/wifi_ap_choose_2G" />
<Spinner
android:id="@+id/band_2G"
style="@style/wifi_item_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/touch_target_min"
android:prompt="@string/wifi_ap_choose_2G" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
style="@style/wifi_item_label"
android:text="@string/wifi_ap_choose_5G" />
<Spinner
android:id="@+id/band_5G"
style="@style/wifi_item_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/touch_target_min"
android:prompt="@string/wifi_ap_choose_5G" />
<LinearLayout
android:id="@+id/band_wrapper_6G"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
@@ -129,40 +172,66 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_label"
android:text="@string/wifi_hotspot_ap_band_title" />
android:text="@string/wifi_ap_choose_6G" />
<Spinner
android:id="@+id/band"
android:id="@+id/band_6G"
style="@style/wifi_item_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/touch_target_min"
android:prompt="@string/wifi_hotspot_ap_band_title" />
android:prompt="@string/wifi_ap_choose_6G" />
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/bssid_wrapper"
<LinearLayout
android:id="@+id/band_wrapper_60G"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
app:counterEnabled="true"
app:counterMaxLength="17"
app:errorEnabled="true"
android:hint="@string/wifi_advanced_mac_address_title">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/bssid"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_edit_content"
android:inputType="textNoSuggestions"
android:maxLength="17" />
</com.google.android.material.textfield.TextInputLayout>
style="@style/wifi_item_label"
android:text="@string/wifi_ap_choose_60G" />
<Spinner
android:id="@+id/band_60G"
style="@style/wifi_item_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/touch_target_min"
android:prompt="@string/wifi_ap_choose_60G" />
</LinearLayout>
<Switch
android:id="@+id/hidden_ssid"
android:id="@+id/bridged_mode"
style="@style/wifi_item_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
android:minHeight="@dimen/touch_target_min"
android:text="@string/wifi_hidden_network" />
android:text="@string/wifi_bridged_mode" />
<Switch
android:id="@+id/bridged_mode_opportunistic_shutdown"
style="@style/wifi_item_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
android:minHeight="@dimen/touch_target_min"
android:text="@string/wifi_bridged_mode_opportunistic_shutdown" />
</LinearLayout>
<LinearLayout
android:id="@+id/access_control_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_divider" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_subhead"
android:text="@string/wifi_hotspot_access_control_title" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/max_client_wrapper"
android:layout_width="match_parent"
@@ -217,5 +286,71 @@
android:inputType="textMultiLine|textNoSuggestions" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/advanced_ap_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_divider" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_subhead"
android:text="@string/wifi_hotspot_ap_advanced_title" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/bssid_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
app:counterEnabled="true"
app:counterMaxLength="17"
app:errorEnabled="true"
android:hint="@string/wifi_advanced_mac_address_title">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/bssid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_edit_content"
android:inputType="textNoSuggestions"
android:maxLength="17" />
</com.google.android.material.textfield.TextInputLayout>
<Switch
android:id="@+id/mac_randomization"
style="@style/wifi_item_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
android:minHeight="@dimen/touch_target_min"
android:text="@string/wifi_mac_randomization" />
<Switch
android:id="@+id/hidden_ssid"
style="@style/wifi_item_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
android:minHeight="@dimen/touch_target_min"
android:text="@string/wifi_hidden_network" />
<Switch
android:id="@+id/ieee_80211ax"
style="@style/wifi_item_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
android:minHeight="@dimen/touch_target_min"
android:text="@string/wifi_ieee_80211ax" />
<Switch
android:id="@+id/user_config"
style="@style/wifi_item_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
android:minHeight="@dimen/touch_target_min"
android:text="@string/wifi_user_config" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -61,7 +61,9 @@
<string name="wifi_hotspot_ap_band_title" msgid="1165801173359290681">"Диапазон частот Wi-Fi"</string>
<string name="wifi_ap_choose_auto" msgid="2677800651271769965">"Авто"</string>
<string name="wifi_ap_choose_2G" msgid="8724267386885036210">"2,4 ГГц"</string>
<string name="wifi_ap_choose_5G" msgid="8813128641914385634">"5,0 ГГц"</string>
<string name="wifi_ap_choose_5G" msgid="8813128641914385634">"5 ГГц"</string>
<string name="wifi_ap_choose_6G">"6 ГГц"</string>
<string name="wifi_ap_choose_60G">"60 ГГц"</string>
<string name="wifi_advanced_mac_address_title" msgid="6571335466330978393">"MAC-адрес"</string>
<string name="wifi_hidden_network" msgid="973162091800925000">"Скрытая сеть"</string>
<string name="wifi_save" msgid="3331121567988522826">"Сохранить"</string>

View File

@@ -61,9 +61,21 @@
<string name="tethering_manage_ncm">USB 网络共享 (NCM)</string>
<string name="tethering_manage_wigig">WiGig 热点</string>
<string name="tethering_manage_wifi_info">%1$d MHz, 频道 %2$d, 频宽 %3$s</string>
<string name="tethering_manage_wifi_info_timeout_enabled">%4$s: Wi\u2011Fi %5$d, %1$d MHz, 频道 %2$d, 频宽 %3$s,
关闭延迟 %6$s</string>
<string name="tethering_manage_wifi_info_timeout_disabled">%4$s: Wi\u2011Fi %5$d, %1$d MHz, 频道 %2$d, 频宽 %3$s,
不自动关闭</string>
<plurals name="tethering_manage_wifi_capabilities">
<item quantity="other">已连接 %1$s/%2$d 个设备\n支持功能%3$s</item>
</plurals>
<plurals name="tethering_manage_wifi_clients">
<item quantity="other">已连接 %d 个设备</item>
</plurals>
<string name="tethering_manage_wifi_supported_channels">\n支持频道: %s</string>
<string name="tethering_manage_wifi_feature_ap_mac_randomization">随机接入点 MAC</string>
<string name="tethering_manage_wifi_feature_bridged_ap_concurrency">桥接 AP 并发</string>
<string name="tethering_manage_wifi_feature_sta_ap_concurrency">STA/AP 并发</string>
<string name="tethering_manage_wifi_feature_sta_bridged_ap_concurrency">STA/桥接 AP 并发</string>
<string name="tethering_manage_wifi_no_features"></string>
<string name="tethering_manage_wifi_client_blocked">已屏蔽 %1$s%2$s</string>
<string name="tethering_manage_wifi_copy_mac">复制 MAC</string>
@@ -140,6 +152,7 @@
<string name="notification_tethering_title">VPN 共享已启用</string>
<string name="notification_channel_tethering">VPN 共享服务</string>
<string name="notification_channel_monitor">监视不活跃接口</string>
<plurals name="notification_connected_devices">
<item quantity="other">%d 个设备已连接到 %s</item>
</plurals>
@@ -166,16 +179,25 @@
<string name="wifi_hotspot_timeout">关闭延迟</string>
<string name="wifi_hotspot_timeout_default">默认延迟:%d 毫秒</string>
<string name="wifi_hotspot_ap_band_title" msgid="1165801173359290681">"AP 频段"</string>
<string name="wifi_ap_choose_disabled">Disabled</string>
<string name="wifi_ap_choose_auto" msgid="2677800651271769965">"自动"</string>
<string name="wifi_ap_choose_2G" msgid="8724267386885036210">"2.4 GHz 频段"</string>
<string name="wifi_ap_choose_5G" msgid="8813128641914385634">"5.0 GHz 频段"</string>
<string name="wifi_ap_choose_6G">6.0 GHz 频段</string>
<string name="wifi_ap_choose_5G" msgid="8813128641914385634">"5 GHz 频段"</string>
<string name="wifi_ap_choose_6G">6 GHz 频段</string>
<string name="wifi_ap_choose_60G">60 GHz 频段</string>
<string name="wifi_hotspot_access_control_title">访问控制</string>
<string name="wifi_hotspot_ap_advanced_title">高级接入点设置</string>
<string name="wifi_advanced_mac_address_title" msgid="6571335466330978393">"MAC 地址"</string>
<string name="wifi_hidden_network" msgid="973162091800925000">"隐藏的网络"</string>
<string name="wifi_max_clients">允许连接设备数上限</string>
<string name="wifi_client_user_control">过滤可以连接的设备</string>
<string name="wifi_blocked_list">设备黑名单</string>
<string name="wifi_allowed_list">设备白名单</string>
<string name="wifi_mac_randomization">随机生成 MAC 地址</string>
<string name="wifi_bridged_mode">启用无线接入点桥接模式</string>
<string name="wifi_bridged_mode_opportunistic_shutdown">启用桥接模式伺机关闭</string>
<string name="wifi_ieee_80211ax">启用 Wi\u2011Fi 6</string>
<string name="wifi_user_config">用户提供配置</string>
<string name="wifi_save" msgid="3331121567988522826">"保存"</string>
<!-- Based on: https://github.com/PrivacyApps/donations/blob/747d36a18433c7e9329691054122a8ad337a62d2/Donations/src/main/res/values-zh/donations__strings.xml -->

View File

@@ -71,6 +71,9 @@
<plurals name="tethering_manage_wifi_capabilities">
<item quantity="other">已連接 %1$s/%2$d 個設備\n支持功能%3$s</item>
</plurals>
<plurals name="tethering_manage_wifi_clients">
<item quantity="other">已連接 %d 個設備</item>
</plurals>
<string name="tethering_manage_wifi_no_features"></string>
<string name="tethering_manage_wifi_client_blocked">已隱藏 %1$s%2$s</string>
<string name="tethering_manage_wifi_copy_mac">複製 MAC</string>
@@ -173,16 +176,20 @@
<string name="wifi_hotspot_timeout">關閉延遲時間</string>
<string name="wifi_hotspot_timeout_default">默認延遲:%d 毫秒</string>
<string name="wifi_hotspot_ap_band_title" msgid="1165801173359290681">AP 頻帶</string>
<string name="wifi_ap_choose_disabled">停用</string>
<string name="wifi_ap_choose_auto" msgid="2677800651271769965">自動</string>
<string name="wifi_ap_choose_2G" msgid="8724267386885036210">2.4 GHz 頻帶</string>
<string name="wifi_ap_choose_5G" msgid="8813128641914385634">5.0 GHz 頻帶</string>
<string name="wifi_ap_choose_6G">6.0 GHz 頻帶</string>
<string name="wifi_ap_choose_5G" msgid="8813128641914385634">5 GHz 頻帶</string>
<string name="wifi_ap_choose_6G">6 GHz 頻帶</string>
<string name="wifi_ap_choose_60G">60 GHz 頻帶</string>
<string name="wifi_advanced_mac_address_title" msgid="6571335466330978393">"MAC 地址"</string>
<string name="wifi_hidden_network" msgid="973162091800925000">"隱藏的網路"</string>
<string name="wifi_max_clients">允許的連接裝置數量</string>
<string name="wifi_client_user_control">過濾可以連接的裝置</string>
<string name="wifi_blocked_list">裝置黑名單</string>
<string name="wifi_allowed_list">裝置白名單</string>
<string name="wifi_mac_randomization">隨機化 MAC 位址</string>
<string name="wifi_ieee_80211ax">啟用 Wi\u2011Fi 6</string>
<string name="wifi_save" msgid="3331121567988522826">儲存</string>
<!-- Based on: https://github.com/PrivacyApps/donations/blob/747d36a18433c7e9329691054122a8ad337a62d2/Donations/src/main/res/values-zh/donations__strings.xml -->

View File

@@ -73,10 +73,23 @@
<string name="tethering_manage_ncm">USB tethering (NCM)</string>
<string name="tethering_manage_wigig">WiGig hotspot</string>
<string name="tethering_manage_wifi_info">%1$d MHz, channel %2$d, width %3$s</string>
<string name="tethering_manage_wifi_info_timeout_enabled">%4$s: Wi\u2011Fi %5$d, %1$d MHz, channel %2$d,
width %3$s, idle timeout in %6$s</string>
<string name="tethering_manage_wifi_info_timeout_disabled">%4$s: Wi\u2011Fi %5$d, %1$d MHz, channel %2$d,
width %3$s, idle timeout disabled</string>
<plurals name="tethering_manage_wifi_capabilities">
<item quantity="one">%1$s/%2$d client connected\nSupported features: %3$s</item>
<item quantity="other">%1$s/%2$d clients connected\nSupported features: %3$s</item>
</plurals>
<plurals name="tethering_manage_wifi_clients">
<item quantity="one">%d client connected</item>
<item quantity="other">%1d clients connected</item>
</plurals>
<string name="tethering_manage_wifi_supported_channels">\nSupported channels: %s</string>
<string name="tethering_manage_wifi_feature_ap_mac_randomization">Randomized AP MAC</string>
<string name="tethering_manage_wifi_feature_bridged_ap_concurrency">Bridged AP concurrency</string>
<string name="tethering_manage_wifi_feature_sta_ap_concurrency">STA + AP concurrency</string>
<string name="tethering_manage_wifi_feature_sta_bridged_ap_concurrency">STA + Bridged AP concurrency</string>
<string name="tethering_manage_wifi_no_features">None</string>
<string name="tethering_manage_wifi_client_blocked">Blocked %1$s: %2$s</string>
<string name="tethering_manage_wifi_copy_mac">Copy MAC</string>
@@ -157,8 +170,9 @@
<string name="settings_restart_required">Restart this app to apply this setting.</string>
<string name="settings_exit_app">Exit</string>
<string name="notification_tethering_title">VPN tethering active</string>
<string name="notification_tethering_title">VPN tethering</string>
<string name="notification_channel_tethering">VPN Tethering Service</string>
<string name="notification_channel_monitor">Monitor Inactive Interfaces</string>
<plurals name="notification_connected_devices">
<item quantity="one">%d device connected to %s</item>
<item quantity="other">%d devices connected to %s</item>
@@ -188,16 +202,25 @@
<string name="wifi_hotspot_timeout">Inactive timeout</string>
<string name="wifi_hotspot_timeout_default">Default timeout: %dms</string>
<string name="wifi_hotspot_ap_band_title">AP Band</string>
<string name="wifi_ap_choose_disabled">Disabled</string>
<string name="wifi_ap_choose_auto">Auto</string>
<string name="wifi_ap_choose_2G">2.4 GHz Band</string>
<string name="wifi_ap_choose_5G">5.0 GHz Band</string>
<string name="wifi_ap_choose_6G">6.0 GHz Band</string>
<string name="wifi_ap_choose_5G">5 GHz Band</string>
<string name="wifi_ap_choose_6G">6 GHz Band</string>
<string name="wifi_ap_choose_60G">60 GHz Band</string>
<string name="wifi_hotspot_access_control_title">Access Control</string>
<string name="wifi_hotspot_ap_advanced_title">Advanced AP Options</string>
<string name="wifi_advanced_mac_address_title">MAC address</string>
<string name="wifi_hidden_network">Hidden network</string>
<string name="wifi_max_clients">Maximum number of clients</string>
<string name="wifi_client_user_control">Control which client can use hotspot</string>
<string name="wifi_blocked_list">Blocked list of clients</string>
<string name="wifi_allowed_list">Allowed list of clients</string>
<string name="wifi_mac_randomization">Use randomized MAC</string>
<string name="wifi_bridged_mode">Enable Bridged Access point (AP) concurrency</string>
<string name="wifi_bridged_mode_opportunistic_shutdown">Enable Bridged mode opportunistic shutdown</string>
<string name="wifi_ieee_80211ax">Enable Wi\u2011Fi 6</string>
<string name="wifi_user_config">User Supplied Configuration</string>
<string name="wifi_save">Save</string>
<!-- Based on: https://github.com/PrivacyApps/donations/blob/747d36a18433c7e9329691054122a8ad337a62d2/Donations/src/main/res/values/donations__strings.xml -->

View File

@@ -43,5 +43,16 @@
<item name="android:layout_marginStart">4dip</item>
<item name="android:textSize">18sp</item>
</style>
<style name="wifi_item_divider">
<item name="dividerInsetStart">8dip</item>
<item name="android:layout_marginTop">8dip</item>
</style>
<style name="wifi_item_subhead">
<item name="android:layout_marginTop">8dip</item>
<item name="android:paddingStart">8dip</item>
<item name="android:textAppearance">@style/TextAppearance.MaterialComponents.Subtitle1</item>
<item name="android:textColor">?attr/colorPrimary</item>
<item name="android:textStyle">bold</item>
</style>
</resources>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<data-extraction-rules>
<cloud-backup>
<include domain="file" path="nonexistent" />
</cloud-backup>
<device-transfer>
<include domain="file" path="nonexistent" />
</device-transfer>
</data-extraction-rules>