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._ _a.k.a. things that can go wrong if this app doesn't work._
This is a list of stuff that might impact this app's functionality if unavailable. This is a list of stuff that might impact this app's functionality if unavailable.
This is only meant to be an index. 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) 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/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$TetheringEventCallback;->onTetherableInterfaceRegexpsChanged(Landroid/net/TetheringManager$TetheringInterfaceRegexps;)V,blocked`
* (since API 30) `Landroid/net/TetheringManager;->TETHERING_WIGIG:I` * (since API 30) `Landroid/net/TetheringManager;->TETHERING_WIGIG:I,blocked`
* (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) * (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setUserConfiguration(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,blocked`
* (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 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_TYPES:[I,blocked`
* (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 31) `Landroid/net/wifi/SoftApInfo;->getApInstanceIdentifier()Ljava/lang/String;,blocked`
* (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 31) `Landroid/net/wifi/WifiClient;->getApInstanceIdentifier()Ljava/lang/String;,blocked`
* (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) * (prior to API 30) `Landroid/net/wifi/WifiConfiguration$KeyMgmt;->FT_PSK:I,lo-prio,max-target-o`
* (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) * (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;->apChannel:I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154102) * (since API 23, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_2GHZ:I,lo-prio,max-target-o`
* (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 23, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_5GHZ:I,lo-prio,max-target-o`
* (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 28, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_ANY:I,lo-prio,max-target-o`
* (since API 23, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->apBand:I,unsupported`
* (since API 23, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->apChannel:I,unsupported`
* (since API 28, prior to API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onNumClientsChanged(I)V,greylist-max-o`
* (since API 26) `Landroid/net/wifi/WifiManager;->cancelLocalOnlyHotspotRequest()V,unsupported`
* (prior to API 26) `Landroid/net/wifi/WifiManager;->setWifiApEnabled(Landroid/net/wifi/WifiConfiguration;Z)Z` * (prior to API 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/WifiP2pConfig$Builder;->MAC_ANY_ADDRESS:Landroid/net/MacAddress;,blocked`
* [`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 29) `Landroid/net/wifi/p2p/WifiP2pConfig$Builder;->mNetworkName:Ljava/lang/String;,blocked`
* (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) * `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`
* (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) * (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_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_bluetooth_regexs:I,max-target-q`
* (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) * (prior to API 30) `Lcom/android/internal/R$array;->config_tether_usb_regexs:I,max-target-q`
* (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) * (prior to API 30) `Lcom/android/internal/R$array;->config_tether_wifi_regexs:I,max-target-q`
* [`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) * (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 27) `Lcom/android/server/connectivity/tethering/OffloadHardwareInterface;->DEFAULT_TETHER_OFFLOAD_DISABLED:I`
* (since API 30) `Lcom/android/server/wifi/WifiContext;->ACTION_RESOURCES_APK:Ljava/lang/String;` * (since API 30) `Lcom/android/server/wifi/WifiContext;->ACTION_RESOURCES_APK:Ljava/lang/String;`
* (since API 29) `Lcom/android/server/wifi/p2p/WifiP2pServiceImpl;->ANONYMIZED_DEVICE_ADDRESS:Ljava/lang/String;` * (since API 29) `Lcom/android/server/wifi/p2p/WifiP2pServiceImpl;->ANONYMIZED_DEVICE_ADDRESS:Ljava/lang/String;`
* (since API 30) `Lcom/android/server/SystemServer;->TETHERING_CONNECTOR_CLASS:Ljava/lang/String;` * (since API 30) `Lcom/android/server/SystemServer;->TETHERING_CONNECTOR_CLASS:Ljava/lang/String;`
* (since API 26) [`Ljava/lang/invoke/MethodHandles$Lookup;-><init>(Ljava/lang/Class;I)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#370415) * (since API 29) `Ldalvik/system/VMRuntime;->getRuntime()Ldalvik/system/VMRuntime;,core-platform-api,unsupported`
* (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) * (since API 29) `Ldalvik/system/VMRuntime;->setHiddenApiExemptions([Ljava/lang/String;)V,blocked,core-platform-api`
* (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 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> <details>
<summary>Hidden whitelisted APIs: (same catch as above, however, things in this list are less likely to be broken)</summary> <summary>Hidden whitelisted APIs: (same catch as above, however, things in this list are less likely to be broken)</summary>
* (since API 24) [`Landroid/bluetooth/BluetoothPan;->isTetheringOn()Z,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#35264) * (since API 24) `Landroid/bluetooth/BluetoothPan;->isTetheringOn()Z,sdk,system-api,test-api`
* (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 24) `Landroid/bluetooth/BluetoothProfile;->PAN:I,sdk,system-api,test-api`
* (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 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,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;-><init>()V,sdk,system-api,test-api`
* (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;->onTetheringFailed()V,sdk,system-api,test-api`
* (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$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,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;->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,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#144408) * (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;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#146558) * `Landroid/net/LinkProperties;->getAllInterfaceNames()Ljava/util/List;,sdk,system-api,test-api`
* [`Landroid/net/LinkProperties;->getAllRoutes()Ljava/util/List;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#146560) * `Landroid/net/LinkProperties;->getAllRoutes()Ljava/util/List;,sdk,system-api,test-api`
* (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;->onTetheringFailed(I)V,sdk,system-api,test-api`
* (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$StartTetheringCallback;->onTetheringStarted()V,sdk,system-api,test-api`
* (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;->onClientsChanged(Ljava/util/Collection;)V,sdk,system-api,test-api`
* (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;->onError(Ljava/lang/String;I)V,sdk,system-api,test-api`
* (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;->onOffloadStatusChanged(I)V,sdk,system-api,test-api`
* (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;->onTetherableInterfacesChanged(Ljava/util/List;)V,sdk,system-api,test-api`
* (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;->onTetheredInterfacesChanged(Ljava/util/List;)V,sdk,system-api,test-api`
* (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;->onTetheringSupported(Z)V,sdk,system-api,test-api`
* (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$TetheringEventCallback;->onUpstreamChanged(Landroid/net/Network;)V,sdk,system-api,test-api`
* (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;-><init>(I)V,sdk,system-api,test-api`
* (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;->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;,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;->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;,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148917) * (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;,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148932) * `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;,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148935) * (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;,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148936) * `Landroid/net/TetheringManager;->EXTRA_ACTIVE_TETHER:Ljava/lang/String;,sdk,system-api,test-api`
* [`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) * `Landroid/net/TetheringManager;->EXTRA_ERRORED_TETHER:Ljava/lang/String;,sdk,system-api,test-api`
* (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 24) `Landroid/net/TetheringManager;->TETHERING_BLUETOOTH:I,sdk,system-api,test-api`
* (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_ETHERNET:I,sdk,system-api,test-api`
* (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 30) `Landroid/net/TetheringManager;->TETHERING_NCM:I,sdk,system-api,test-api`
* (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_USB:I,sdk,system-api,test-api`
* (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) * (since API 24) `Landroid/net/TetheringManager;->TETHERING_WIFI:I,sdk,system-api,test-api`
* [`Landroid/net/TetheringManager;->TETHER_ERROR_*:I,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148947) * `Landroid/net/TetheringManager;->TETHER_ERROR_*:I,sdk,system-api,test-api`
* (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_ERROR_NO_CHANGE_TETHERING_PERMISSION:I,sdk,system-api,test-api`
* (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_FAILED:I,sdk,system-api,test-api`
* (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_STARTED:I,sdk,system-api,test-api`
* (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;->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,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#149003) * (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,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#149009) * (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,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#149011) * (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,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#149014) * (since API 30) `Landroid/net/TetheringManager;->unregisterTetheringEventCallback(Landroid/net/TetheringManager$TetheringEventCallback;)V,sdk,system-api,test-api`
* (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 31) `Landroid/net/wifi/SoftApCapability;->SOFTAP_FEATURE_BAND_24G_SUPPORTED:J,sdk,system-api,test-api`
* (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 31) `Landroid/net/wifi/SoftApCapability;->SOFTAP_FEATURE_BAND_5G_SUPPORTED:J,sdk,system-api,test-api`
* (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 31) `Landroid/net/wifi/SoftApCapability;->SOFTAP_FEATURE_BAND_60G_SUPPORTED:J,sdk,system-api,test-api`
* (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 31) `Landroid/net/wifi/SoftApCapability;->SOFTAP_FEATURE_BAND_6G_SUPPORTED:J,sdk,system-api,test-api`
* (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/SoftApCapability;->SOFTAP_FEATURE_*:J,sdk,system-api,test-api`
* (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/SoftApCapability;->areFeaturesSupported(J)Z,sdk,system-api,test-api`
* (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/SoftApCapability;->getMaxSupportedClients()I,sdk,system-api,test-api`
* (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 31) `Landroid/net/wifi/SoftApCapability;->getSupportedChannelList(I)[I,sdk,system-api,test-api`
* (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;-><init>()V,sdk,system-api,test-api`
* (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;-><init>(Landroid/net/wifi/SoftApConfiguration;)V,sdk,system-api,test-api`
* (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;->build()Landroid/net/wifi/SoftApConfiguration;,sdk,system-api,test-api`
* (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;->setAllowedClientList(Ljava/util/List;)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;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153729) * (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setAutoShutdownEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setHiddenSsid(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153730) * (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;->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;->setBlockedClientList(Ljava/util/List;)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;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153732) * (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;->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;->setBssid(Landroid/net/MacAddress;)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;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153734) * (on API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setChannel(II)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (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 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;->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$Builder;->setClientControlByUserEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (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$Builder;->setHiddenSsid(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (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 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setIeee80211axEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (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 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;->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$Builder;->setMaxNumberOfClients(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (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$Builder;->setPassphrase(Ljava/lang/String;I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (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$Builder;->setShutdownTimeoutMillis(J)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (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$Builder;->setSsid(Ljava/lang/String;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (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;->BAND_2GHZ:I,sdk,system-api,test-api`
* (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;->BAND_5GHZ:I,sdk,system-api,test-api`
* (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 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_60GHZ:I,sdk,system-api,test-api`
* (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/SoftApConfiguration;->BAND_6GHZ:I,sdk,system-api,test-api`
* (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 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_*:I,sdk,system-api,test-api`
* (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 31) `Landroid/net/wifi/SoftApConfiguration;->RANDOMIZATION_NONE:I,sdk,system-api,test-api`
* (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 31) `Landroid/net/wifi/SoftApConfiguration;->RANDOMIZATION_PERSISTENT:I,sdk,system-api,test-api`
* (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) * (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getAllowedClientList()Ljava/util/List;,sdk,system-api,test-api`
* (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/SoftApConfiguration;->getBand()I,sdk,system-api,test-api`
* (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/SoftApConfiguration;->getBlockedClientList()Ljava/util/List;,sdk,system-api,test-api`
* (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/SoftApConfiguration;->getChannel()I,sdk,system-api,test-api`
* (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 31) `Landroid/net/wifi/SoftApConfiguration;->getChannels()Landroid/util/SparseIntArray;,sdk,system-api,test-api`
* (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 31) `Landroid/net/wifi/SoftApConfiguration;->getMacRandomizationSetting()I,sdk,system-api,test-api`
* (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/SoftApConfiguration;->getMaxNumberOfClients()I,sdk,system-api,test-api`
* (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 30) `Landroid/net/wifi/SoftApConfiguration;->getShutdownTimeoutMillis()J,sdk,system-api,test-api`
* (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 30) `Landroid/net/wifi/SoftApConfiguration;->isAutoShutdownEnabled()Z,sdk,system-api,test-api`
* (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 31) `Landroid/net/wifi/SoftApConfiguration;->isBridgedModeOpportunisticShutdownEnabled()Z,sdk,system-api,test-api`
* (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) * (since API 30) `Landroid/net/wifi/SoftApConfiguration;->isClientControlByUserEnabled()Z,sdk,system-api,test-api`
* (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 31) `Landroid/net/wifi/SoftApConfiguration;->isIeee80211axEnabled()Z,sdk,system-api,test-api`
* (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 31) `Landroid/net/wifi/SoftApConfiguration;->isUserConfiguration()Z,sdk,system-api,test-api`
* (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) * (since API 30) `Landroid/net/wifi/SoftApInfo;->CHANNEL_WIDTH_*:I,sdk,system-api,test-api`
* (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) * (on API 30) `Landroid/net/wifi/SoftApInfo;->CHANNEL_WIDTH_INVALID:I,sdk,system-api,test-api`
* (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) * (since API 31) `Landroid/net/wifi/SoftApInfo;->getAutoShutdownTimeoutMillis()J,sdk,system-api,test-api`
* [`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) * (since API 30) `Landroid/net/wifi/SoftApInfo;->getBandwidth()I,sdk,system-api,test-api`
* [`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) * (since API 31) `Landroid/net/wifi/SoftApInfo;->getBssid()Landroid/net/MacAddress;,sdk,system-api,test-api`
* [`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) * (since API 30) `Landroid/net/wifi/SoftApInfo;->getFrequency()I,sdk,system-api,test-api`
* [`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) * (since API 31) `Landroid/net/wifi/SoftApInfo;->getWifiStandard()I,sdk,system-api,test-api`
* [`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) * (since API 30) `Landroid/net/wifi/WifiClient;->getMacAddress()Landroid/net/MacAddress;,sdk,system-api,test-api`
* [`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) * (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> </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_p2p_regexs`
* (since API 30) `@com.android.networkstack.tethering:array/config_tether_wifi_regexs` * (since API 30) `@com.android.networkstack.tethering:array/config_tether_wifi_regexs`
* (since API 30) `@com.android.networkstack.tethering:array/config_tether_wigig_regexs` * (since API 30) `@com.android.networkstack.tethering:array/config_tether_wigig_regexs`
* (since API 30) `@com.android.wifi.resources:bool/config_wifi_p2p_mac_randomization_supported`
* (since API 30) `@com.android.wifi.resources:integer/config_wifiFrameworkSoftApShutDownTimeoutMilliseconds` * (since API 30) `@com.android.wifi.resources:integer/config_wifiFrameworkSoftApShutDownTimeoutMilliseconds`
Other: Other: Activity `com.android.settings/.Settings$TetherSettingsActivity` is assumed to be exported.
* (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.
For `ip rule` priorities, `RULE_PRIORITY_SECURE_VPN` and `RULE_PRIORITY_TETHERING` is assumed to be 12000 and 18000 respectively; For `ip rule` priorities, `RULE_PRIORITY_SECURE_VPN` and `RULE_PRIORITY_TETHERING` is assumed to be 12000 and 18000 respectively;
(prior to API 24) `RULE_PRIORITY_DEFAULT_NETWORK` is assumed to be 22000 (or at least > 18000). (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 { dependencies {
classpath(kotlin("gradle-plugin", "1.5.20")) classpath(kotlin("gradle-plugin", "1.5.21"))
classpath("com.android.tools.build:gradle:7.0.0-beta05") classpath("com.android.tools.build:gradle:7.0.0")
classpath("com.google.firebase:firebase-crashlytics-gradle:2.7.1") classpath("com.google.firebase:firebase-crashlytics-gradle:2.7.1")
classpath("com.google.android.gms:oss-licenses-plugin:0.10.4") classpath("com.google.android.gms:oss-licenses-plugin:0.10.4")
classpath("com.google.gms:google-services:4.3.8") classpath("com.google.gms:google-services:4.3.8")

View File

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

View File

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

View File

@@ -8,7 +8,6 @@ import android.os.RemoteException
import android.system.Os import android.system.Os
import android.system.OsConstants import android.system.OsConstants
import androidx.collection.LongSparseArray import androidx.collection.LongSparseArray
import androidx.collection.set
import androidx.collection.valueIterator import androidx.collection.valueIterator
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.* import kotlinx.coroutines.channels.*
@@ -102,6 +101,7 @@ class RootServer {
private fun readUnexpectedStderr(): String? { private fun readUnexpectedStderr(): String? {
if (!this::process.isInitialized) return null if (!this::process.isInitialized) return null
Logger.me.d("Attempting to read stderr")
var available = process.errorStream.available() var available = process.errorStream.available()
return if (available <= 0) null else String(ByteArrayOutputStream().apply { return if (available <= 0) null else String(ByteArrayOutputStream().apply {
try { try {
@@ -146,7 +146,9 @@ class RootServer {
try { try {
val token2 = UUID.randomUUID().toString() val token2 = UUID.randomUUID().toString()
val persistence = File(context.codeCacheDir, ".librootkotlinx-uuid") 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) } UUID.randomUUID().toString().also { persistence.writeText(it) }
} }
val (script, relocated) = AppProcess.relocateScript(uuid) val (script, relocated) = AppProcess.relocateScript(uuid)
@@ -218,9 +220,9 @@ class RootServer {
throw e throw e
} finally { } finally {
Logger.me.d("Waiting for exit") Logger.me.d("Waiting for exit")
errorReader.await() withContext(NonCancellable) { errorReader.await() }
process.waitFor() process.waitFor()
withContext(NonCancellable) { closeInternal(true) } closeInternal(true)
} }
} }
} }
@@ -246,7 +248,7 @@ class RootServer {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val callback = Callback.Ordinary(this, counter, classLoader, future as CompletableDeferred<Parcelable?>) val callback = Callback.Ordinary(this, counter, classLoader, future as CompletableDeferred<Parcelable?>)
if (active) { if (active) {
callbackLookup[counter] = callback callbackLookup.append(counter, callback)
sendLocked(command) sendLocked(command)
} else future.cancel() } else future.cancel()
callback callback
@@ -277,7 +279,7 @@ class RootServer {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val callback = Callback.Channel(this@RootServer, counter, classLoader, this as SendChannel<Parcelable?>) val callback = Callback.Channel(this@RootServer, counter, classLoader, this as SendChannel<Parcelable?>)
if (active) { if (active) {
callbackLookup[counter] = callback callbackLookup.append(counter, callback)
sendLocked(command) sendLocked(command)
} else callback.finish.cancel() } else callback.finish.cancel()
callback 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) { if (active) {
active = false active = false
Logger.me.d(if (fromWorker) "Shutting down from worker" else "Shutting down from client") Logger.me.d(if (fromWorker) "Shutting down from worker" else "Shutting down from client")
@@ -432,7 +434,7 @@ class RootServer {
} }
is RootCommand<*> -> { is RootCommand<*> -> {
val commandJob = Job() val commandJob = Job()
cancellables[callback] = { commandJob.cancel() } cancellables.append(callback) { commandJob.cancel() }
defaultWorker.launch(commandJob) { defaultWorker.launch(commandJob) {
val result = try { val result = try {
val result = command.execute(); val result = command.execute();
@@ -450,7 +452,7 @@ class RootServer {
val result = try { val result = try {
coroutineScope { coroutineScope {
command.create(this).also { command.create(this).also {
cancellables[callback] = { it.cancel() } cancellables.append(callback) { it.cancel() }
}.consumeEach { result -> }.consumeEach { result ->
withContext(callbackWorker) { output.pushResult(callback, 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.Fragment
import androidx.fragment.app.setFragmentResult import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.setFragmentResultListener
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
/** /**
@@ -44,7 +45,7 @@ abstract class AlertDialogFragment<Arg : Parcelable, Ret : Parcelable> :
fun key(resultKey: String = javaClass.name) = args().putString(KEY_RESULT, resultKey) fun key(resultKey: String = javaClass.name) = args().putString(KEY_RESULT, resultKey)
override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog = override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog =
AlertDialog.Builder(requireContext()).also { it.prepare(this) }.create() MaterialAlertDialogBuilder(requireContext()).also { it.prepare(this) }.create()
override fun onClick(dialog: DialogInterface?, which: Int) { override fun onClick(dialog: DialogInterface?, which: Int) {
setFragmentResult(resultKey ?: return, Bundle().apply { setFragmentResult(resultKey ?: return, Bundle().apply {

View File

@@ -62,6 +62,7 @@ class App : Application() {
} }
} }
Timber.plant(object : Timber.DebugTree() { Timber.plant(object : Timber.DebugTree() {
@SuppressLint("LogNotTimber")
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
if (t == null) { if (t == null) {
if (priority != Log.DEBUG || BuildConfig.DEBUG) Log.println(priority, tag, message) 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 android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.net.IpNeighbour import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
import be.mygod.vpnhotspot.net.wifi.WifiApManager import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.net.wifi.WifiApManager.wifiApState
import be.mygod.vpnhotspot.util.Services import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.StickyEvent1 import be.mygod.vpnhotspot.util.StickyEvent1
import be.mygod.vpnhotspot.util.broadcastReceiver
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.* import kotlinx.coroutines.*
import timber.log.Timber import timber.log.Timber
@@ -43,7 +40,8 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
null -> return // stopped null -> return // stopped
"" -> WifiApManager.cancelLocalOnlyHotspotRequest() "" -> WifiApManager.cancelLocalOnlyHotspotRequest()
} }
reservation?.close() ?: stopService() reservation?.close()
stopService()
} }
} }
@@ -56,24 +54,6 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
override val coroutineContext = dispatcher + Job() override val coroutineContext = dispatcher + Job()
private var routingManager: RoutingManager? = null private var routingManager: RoutingManager? = null
private var timeoutMonitor: TetherTimeoutMonitor? = null private var timeoutMonitor: TetherTimeoutMonitor? = null
private var receiverRegistered = false
private val receiver = broadcastReceiver { _, intent ->
val ifaces = (intent.localOnlyTetheredIfaces ?: return@broadcastReceiver).filter {
TetherType.ofInterface(it) != TetherType.WIFI_P2P
}
Timber.d("onTetherStateChangedLocked: $ifaces")
check(ifaces.size <= 1)
val iface = ifaces.singleOrNull()
binder.iface = iface
if (iface.isNullOrEmpty()) stopService() else launch {
val routingManager = routingManager
if (routingManager == null) {
this@LocalOnlyHotspotService.routingManager = RoutingManager.LocalOnly(this@LocalOnlyHotspotService,
iface).apply { start() }
IpNeighbourMonitor.registerCallback(this@LocalOnlyHotspotService)
} else check(iface == routingManager.downstream)
}
}
override val activeIfaces get() = binder.iface.let { if (it.isNullOrEmpty()) emptyList() else listOf(it) } override val activeIfaces get() = binder.iface.let { if (it.isNullOrEmpty()) emptyList() else listOf(it) }
override fun onBind(intent: Intent?) = binder override fun onBind(intent: Intent?) = binder
@@ -87,20 +67,36 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) { override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) {
if (reservation == null) onFailed(-2) else { if (reservation == null) onFailed(-2) else {
this@LocalOnlyHotspotService.reservation = reservation this@LocalOnlyHotspotService.reservation = reservation
if (!receiverRegistered) { val configuration = binder.configuration!!
val configuration = binder.configuration!! if (Build.VERSION.SDK_INT < 30 && configuration.isAutoShutdownEnabled) {
if (Build.VERSION.SDK_INT < 30 && configuration.isAutoShutdownEnabled) { timeoutMonitor = TetherTimeoutMonitor(configuration.shutdownTimeoutMillis,
timeoutMonitor = TetherTimeoutMonitor(configuration.shutdownTimeoutMillis, coroutineContext) { reservation.close() }
coroutineContext) { reservation.close() } }
// 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()
} }
registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED)) return stopService()
receiverRegistered = true }
binder.iface = iface
launch {
check(routingManager == null)
routingManager = RoutingManager.LocalOnly(
this@LocalOnlyHotspotService, iface).apply { start() }
IpNeighbourMonitor.registerCallback(this@LocalOnlyHotspotService)
} }
} }
} }
override fun onStopped() { override fun onStopped() {
Timber.d("LOHCallback.onStopped") Timber.d("LOHCallback.onStopped")
reservation?.close()
reservation = null reservation = null
} }
@@ -152,14 +148,10 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
} }
private fun unregisterReceiver(exit: Boolean = false) { private fun unregisterReceiver(exit: Boolean = false) {
if (receiverRegistered) { IpNeighbourMonitor.unregisterCallback(this)
unregisterReceiver(receiver) if (Build.VERSION.SDK_INT >= 28) {
IpNeighbourMonitor.unregisterCallback(this) timeoutMonitor?.close()
if (Build.VERSION.SDK_INT >= 28) { timeoutMonitor = null
timeoutMonitor?.close()
timeoutMonitor = null
}
receiverRegistered = false
} }
launch { launch {
routingManager?.stop() routingManager?.stop()

View File

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

View File

@@ -12,25 +12,29 @@ import be.mygod.vpnhotspot.App.Companion.app
import java.util.* import java.util.*
object ServiceNotification { object ServiceNotification {
private const val CHANNEL = "tethering" private const val CHANNEL_ACTIVE = "tethering"
private const val CHANNEL_ID = 1 private const val CHANNEL_INACTIVE = "tethering-inactive"
private const val NOTIFICATION_ID = 1
private val deviceCountsMap = WeakHashMap<Service, Map<String, Int>>() private val deviceCountsMap = WeakHashMap<Service, Map<String, Int>>()
private val inactiveMap = WeakHashMap<Service, List<String>>() private val inactiveMap = WeakHashMap<Service, List<String>>()
private val manager = app.getSystemService<NotificationManager>()!! private val manager = app.getSystemService<NotificationManager>()!!
private fun buildNotification(context: Context): Notification { private fun buildNotification(context: Context): Notification {
val builder = NotificationCompat.Builder(context, CHANNEL)
.setWhen(0)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setColor(ContextCompat.getColor(context, R.color.colorPrimary))
.setContentTitle(context.getText(R.string.notification_tethering_title))
.setSmallIcon(R.drawable.ic_quick_settings_tile_on)
.setContentIntent(PendingIntent.getActivity(context, 0,
Intent(context, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
val deviceCounts = deviceCountsMap.values.flatMap { it.entries }.sortedBy { it.key } val deviceCounts = deviceCountsMap.values.flatMap { it.entries }.sortedBy { it.key }
val inactive = inactiveMap.values.flatten() val inactive = inactiveMap.values.flatten()
val isInactive = inactive.isNotEmpty() && deviceCounts.isEmpty()
val builder = 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) -> var lines = deviceCounts.map { (dev, size) ->
context.resources.getQuantityString(R.plurals.notification_connected_devices, size, size, dev) context.resources.getQuantityString(R.plurals.notification_connected_devices, size, size, dev)
} }
@@ -54,23 +58,28 @@ object ServiceNotification {
synchronized(this) { synchronized(this) {
deviceCountsMap[service] = deviceCounts deviceCountsMap[service] = deviceCounts
if (inactive.isEmpty()) inactiveMap.remove(service) else inactiveMap[service] = inactive if (inactive.isEmpty()) inactiveMap.remove(service) else inactiveMap[service] = inactive
service.startForeground(CHANNEL_ID, buildNotification(service)) service.startForeground(NOTIFICATION_ID, buildNotification(service))
} }
} }
fun stopForeground(service: Service) = synchronized(this) { fun stopForeground(service: Service) = synchronized(this) {
deviceCountsMap.remove(service) deviceCountsMap.remove(service) ?: return@synchronized
if (deviceCountsMap.isEmpty()) service.stopForeground(true) else { val shutdown = deviceCountsMap.isEmpty()
service.stopForeground(false) service.stopForeground(shutdown)
manager.notify(CHANNEL_ID, buildNotification(service)) if (!shutdown) manager.notify(NOTIFICATION_ID, buildNotification(service))
}
} }
fun updateNotificationChannels() { fun updateNotificationChannels() {
if (Build.VERSION.SDK_INT >= 26) @TargetApi(26) { if (Build.VERSION.SDK_INT >= 26) @TargetApi(26) {
val tethering = NotificationChannel(CHANNEL, NotificationChannel(CHANNEL_ACTIVE,
app.getText(R.string.notification_channel_tethering), NotificationManager.IMPORTANCE_LOW) app.getText(R.string.notification_channel_tethering), NotificationManager.IMPORTANCE_LOW).apply {
tethering.lockscreenVisibility = Notification.VISIBILITY_PUBLIC lockscreenVisibility = Notification.VISIBILITY_PUBLIC
manager.createNotificationChannel(tethering) 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 // remove old service channels
manager.deleteNotificationChannel("hotspot") manager.deleteNotificationChannel("hotspot")
manager.deleteNotificationChannel("repeater") 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.IpMonitor
import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
import be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreferenceDialogFragment import be.mygod.vpnhotspot.preference.AutoCompleteNetworkPreferenceDialogFragment
import be.mygod.vpnhotspot.preference.SharedPreferenceDataStore import be.mygod.vpnhotspot.preference.SharedPreferenceDataStore
import be.mygod.vpnhotspot.preference.SummaryFallbackProvider import be.mygod.vpnhotspot.preference.SummaryFallbackProvider
import be.mygod.vpnhotspot.root.Dump import be.mygod.vpnhotspot.root.Dump
import be.mygod.vpnhotspot.root.RootManager import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.util.Services import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.allInterfaceNames
import be.mygod.vpnhotspot.util.launchUrl import be.mygod.vpnhotspot.util.launchUrl
import be.mygod.vpnhotspot.util.showAllowingStateLoss import be.mygod.vpnhotspot.util.showAllowingStateLoss
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
@@ -144,16 +143,12 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
} }
} }
override fun onDisplayPreferenceDialog(preference: Preference) { override fun onDisplayPreferenceDialog(preference: Preference) = when (preference.key) {
when (preference.key) { UpstreamMonitor.KEY, FallbackUpstreamMonitor.KEY ->
UpstreamMonitor.KEY, FallbackUpstreamMonitor.KEY -> AutoCompleteNetworkPreferenceDialogFragment().apply {
AlwaysAutoCompleteEditTextPreferenceDialogFragment().apply { setArguments(preference.key)
setArguments(preference.key, Services.connectivity.allNetworks.mapNotNull { setTargetFragment(this@SettingsPreferenceFragment, 0)
Services.connectivity.getLinkProperties(it)?.allInterfaceNames }.showAllowingStateLoss(parentFragmentManager, preference.key)
}.flatten().toTypedArray()) else -> super.onDisplayPreferenceDialog(preference)
setTargetFragment(this@SettingsPreferenceFragment, 0)
}.showAllowingStateLoss(parentFragmentManager, preference.key)
else -> super.onDisplayPreferenceDialog(preference)
}
} }
} }

View File

@@ -4,7 +4,10 @@ import android.content.ComponentName
import android.content.IntentFilter import android.content.IntentFilter
import android.content.ServiceConnection import android.content.ServiceConnection
import android.net.wifi.p2p.WifiP2pDevice import android.net.wifi.p2p.WifiP2pDevice
import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.Parcelable
import androidx.annotation.RequiresApi
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@@ -13,13 +16,19 @@ import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.RepeaterService import be.mygod.vpnhotspot.RepeaterService
import be.mygod.vpnhotspot.net.IpNeighbour import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.MacAddressCompat import be.mygod.vpnhotspot.net.MacAddressCompat
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
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor 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 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 var tetheredInterfaces = emptySet<String>()
private val receiver = broadcastReceiver { _, intent -> private val receiver = broadcastReceiver { _, intent ->
tetheredInterfaces = (intent.tetheredIfaces ?: return@broadcastReceiver).toSet() + tetheredInterfaces = (intent.tetheredIfaces ?: return@broadcastReceiver).toSet() +
@@ -29,6 +38,7 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
private var repeater: RepeaterService.Binder? = null private var repeater: RepeaterService.Binder? = null
private var p2p: Collection<WifiP2pDevice> = emptyList() private var p2p: Collection<WifiP2pDevice> = emptyList()
private var wifiAp = emptyList<Pair<String, MacAddressCompat>>()
private var neighbours: Collection<IpNeighbour> = emptyList() private var neighbours: Collection<IpNeighbour> = emptyList()
val clients = MutableLiveData<List<Client>>() val clients = MutableLiveData<List<Client>>()
val fullMode = object : DefaultLifecycleObserver { val fullMode = object : DefaultLifecycleObserver {
@@ -42,11 +52,18 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
private fun populateClients() { private fun populateClients() {
val clients = HashMap<Pair<String, MacAddressCompat>, Client>() val clients = HashMap<Pair<String, MacAddressCompat>, Client>()
val group = repeater?.group repeater?.group?.`interface`?.let { p2pInterface ->
val p2pInterface = group?.`interface` for (client in p2p) {
if (p2pInterface != null) { val addr = MacAddressCompat.fromString(client.deviceAddress!!)
for (client in p2p) clients[p2pInterface to MacAddressCompat.fromString(client.deviceAddress)] = clients[p2pInterface to addr] = object : Client(addr, p2pInterface) {
WifiP2pClient(p2pInterface, client) 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) { for (neighbour in neighbours) {
val key = neighbour.dev to neighbour.lladdr val key = neighbour.dev to neighbour.lladdr
@@ -70,8 +87,10 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
override fun onStart(owner: LifecycleOwner) { override fun onStart(owner: LifecycleOwner) {
app.registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED)) app.registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
IpNeighbourMonitor.registerCallback(this, false) IpNeighbourMonitor.registerCallback(this, false)
if (Build.VERSION.SDK_INT >= 31) WifiApCommands.registerSoftApCallback(this)
} }
override fun onStop(owner: LifecycleOwner) { override fun onStop(owner: LifecycleOwner) {
if (Build.VERSION.SDK_INT >= 31) WifiApCommands.unregisterSoftApCallback(this)
IpNeighbourMonitor.unregisterCallback(this) IpNeighbourMonitor.unregisterCallback(this)
app.unregisterReceiver(receiver) app.unregisterReceiver(receiver)
} }
@@ -94,4 +113,12 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
this.neighbours = neighbours this.neighbours = neighbours
populateClients() 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.AppDatabase
import be.mygod.vpnhotspot.room.ClientStats import be.mygod.vpnhotspot.room.ClientStats
import be.mygod.vpnhotspot.room.TrafficRecord 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.showAllowingStateLoss
import be.mygod.vpnhotspot.util.toPluralInt import be.mygod.vpnhotspot.util.toPluralInt
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
@@ -82,10 +82,11 @@ class ClientsFragment : Fragment() {
data class StatsArg(val title: CharSequence, val stats: ClientStats) : Parcelable data class StatsArg(val title: CharSequence, val stats: ClientStats) : Parcelable
class StatsDialogFragment : AlertDialogFragment<StatsArg, Empty>() { class StatsDialogFragment : AlertDialogFragment<StatsArg, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) { override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setTitle(SpanFormatter.format(getText(R.string.clients_stats_title), arg.title))
val context = context val context = context
val resources = resources 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( setMessage("%s\n%s\n%s".format(
resources.getQuantityString(R.plurals.clients_stats_message_1, arg.stats.count.toPluralInt(), resources.getQuantityString(R.plurals.clients_stats_message_1, arg.stats.count.toPluralInt(),
format.format(arg.stats.count), DateUtils.formatDateTime(context, arg.stats.timestamp, 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 package be.mygod.vpnhotspot.manage
import android.annotation.SuppressLint
import android.annotation.TargetApi import android.annotation.TargetApi
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothProfile import android.bluetooth.BluetoothProfile
@@ -9,7 +10,6 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.os.BuildCompat
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.util.broadcastReceiver import be.mygod.vpnhotspot.util.broadcastReceiver
@@ -18,7 +18,7 @@ import be.mygod.vpnhotspot.widget.SmartSnackbar
import timber.log.Timber import timber.log.Timber
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
class BluetoothTethering(context: Context, val stateListener: () -> Unit) : class BluetoothTethering(context: Context, private val adapter: BluetoothAdapter, val stateListener: () -> Unit) :
BluetoothProfile.ServiceListener, AutoCloseable { BluetoothProfile.ServiceListener, AutoCloseable {
companion object : BroadcastReceiver() { companion object : BroadcastReceiver() {
/** /**
@@ -26,17 +26,9 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
*/ */
private const val PAN = 5 private const val PAN = 5
private val clazz by lazy { Class.forName("android.bluetooth.BluetoothPan") } private val clazz by lazy { Class.forName("android.bluetooth.BluetoothPan") }
private val constructor by lazy {
clazz.getDeclaredConstructor(Context::class.java, BluetoothProfile.ServiceListener::class.java).apply {
isAccessible = true
}
}
private val isTetheringOn by lazy { clazz.getDeclaredMethod("isTetheringOn") } private val isTetheringOn by lazy { clazz.getDeclaredMethod("isTetheringOn") }
fun pan(context: Context, serviceListener: BluetoothProfile.ServiceListener) = private val BluetoothProfile.isTetheringOn get() = isTetheringOn(this) as Boolean
constructor.newInstance(context, serviceListener) as BluetoothProfile
val BluetoothProfile.isTetheringOn get() = isTetheringOn(this) as Boolean
fun BluetoothProfile.closePan() = BluetoothAdapter.getDefaultAdapter()!!.closeProfileProxy(PAN, this)
private fun registerBluetoothStateListener(receiver: BroadcastReceiver) = private fun registerBluetoothStateListener(receiver: BroadcastReceiver) =
app.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)) app.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
@@ -58,28 +50,12 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
pendingCallback = null pendingCallback = null
app.unregisterReceiver(this) app.unregisterReceiver(this)
} }
/**
* https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/TetherSettings.java#384
*/
@RequiresApi(24)
fun start(callback: TetheringManager.StartTetheringCallback) {
if (pendingCallback != null) return
val adapter = BluetoothAdapter.getDefaultAdapter()
try {
if (adapter?.state == BluetoothAdapter.STATE_OFF) {
registerBluetoothStateListener(this)
pendingCallback = callback
adapter.enable()
} else TetheringManager.startTethering(TetheringManager.TETHERING_BLUETOOTH, true, callback)
} catch (e: SecurityException) {
SmartSnackbar.make(e.readableMessage).shortToast().show()
}
}
} }
private var proxyCreated = false
private var connected = false private var connected = false
private var pan: BluetoothProfile? = null private var pan: BluetoothProfile? = null
private var stoppedByUser = false
var activeFailureCause: Throwable? = null var activeFailureCause: Throwable? = null
/** /**
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/78d5efd/src/com/android/settings/TetherSettings.java * Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/78d5efd/src/com/android/settings/TetherSettings.java
@@ -88,7 +64,7 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
val pan = pan ?: return null val pan = pan ?: return null
if (!connected) return null if (!connected) return null
activeFailureCause = null activeFailureCause = null
return BluetoothAdapter.getDefaultAdapter()?.state == BluetoothAdapter.STATE_ON && try { val on = adapter.state == BluetoothAdapter.STATE_ON && try {
pan.isTetheringOn pan.isTetheringOn
} catch (e: InvocationTargetException) { } catch (e: InvocationTargetException) {
activeFailureCause = e.cause ?: e activeFailureCause = e.cause ?: e
@@ -96,16 +72,21 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
else Timber.w(e) else Timber.w(e)
return null return null
} }
return if (stoppedByUser) {
if (!on) stoppedByUser = false
false
} else on
} }
private val receiver = broadcastReceiver { _, _ -> stateListener() } private val receiver = broadcastReceiver { _, _ -> stateListener() }
fun ensureInit(context: Context) { fun ensureInit(context: Context) {
if (pan == null && BluetoothAdapter.getDefaultAdapter() != null) try { activeFailureCause = null
pan = pan(context, this) if (!proxyCreated) try {
} catch (e: ReflectiveOperationException) { check(adapter.getProfileProxy(context, this, PAN))
if (e.cause is SecurityException && BuildCompat.isAtLeastS()) Timber.d(e.readableMessage) proxyCreated = true
else Timber.w(e) } catch (e: SecurityException) {
if (Build.VERSION.SDK_INT >= 31) Timber.d(e.readableMessage) else Timber.w(e)
activeFailureCause = e activeFailureCause = e
} }
} }
@@ -116,13 +97,39 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
override fun onServiceDisconnected(profile: Int) { override fun onServiceDisconnected(profile: Int) {
connected = false connected = false
stoppedByUser = false
} }
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) { override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
pan = proxy
connected = true connected = true
stateListener() stateListener()
} }
/**
* https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/TetherSettings.java#384
*/
@SuppressLint("MissingPermission")
@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() { override fun close() {
app.unregisterReceiver(receiver) app.unregisterReceiver(receiver)
pan?.closePan() adapter.closeProfileProxy(PAN, pan)
} }
} }

View File

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

View File

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

View File

@@ -2,40 +2,41 @@ package be.mygod.vpnhotspot.manage
import android.Manifest import android.Manifest
import android.annotation.TargetApi import android.annotation.TargetApi
import android.content.ClipData import android.bluetooth.BluetoothAdapter
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.MacAddress
import android.os.Build import android.os.Build
import android.os.Parcelable
import android.provider.Settings import android.provider.Settings
import android.text.SpannableStringBuilder
import android.text.format.DateUtils
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.os.BuildCompat
import androidx.core.view.updatePaddingRelative import androidx.core.view.updatePaddingRelative
import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.BuildConfig
import be.mygod.vpnhotspot.MainActivity import be.mygod.vpnhotspot.MainActivity
import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat import be.mygod.vpnhotspot.net.wifi.*
import be.mygod.vpnhotspot.net.wifi.WifiApManager import be.mygod.vpnhotspot.net.wifi.WifiApManager.wifiApState
import be.mygod.vpnhotspot.root.WifiApCommands import be.mygod.vpnhotspot.root.WifiApCommands
import be.mygod.vpnhotspot.util.readableMessage import be.mygod.vpnhotspot.util.*
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.util.*
sealed class TetherManager(protected val parent: TetheringFragment) : Manager(), sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
TetheringManager.StartTetheringCallback { TetheringManager.StartTetheringCallback {
@@ -63,12 +64,16 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
} catch (e: RuntimeException) { } catch (e: RuntimeException) {
app.logEvent("manage_write_settings") { param("message", e.toString()) } app.logEvent("manage_write_settings") { param("message", e.toString()) }
} }
if (manager.isStarted) try { when (manager.isStarted) {
manager.stop() true -> try {
} catch (e: InvocationTargetException) { manager.stop()
if (e.targetException !is SecurityException) Timber.w(e) } catch (e: InvocationTargetException) {
manager.onException(e) if (e.targetException !is SecurityException) Timber.w(e)
} else manager.start() manager.onException(e)
}
false -> manager.start()
null -> manager.onClickNull()
}
} }
} }
@@ -79,13 +84,13 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
override val icon get() = tetherType.icon override val icon get() = tetherType.icon
override val title get() = this@TetherManager.title override val title get() = this@TetherManager.title
override val text get() = this@TetherManager.text override val text get() = this@TetherManager.text
override val active get() = isStarted override val active get() = isStarted == true
} }
val data = Data() val data = Data()
abstract val title: CharSequence abstract val title: CharSequence
abstract val tetherType: TetherType abstract val tetherType: TetherType
open val isStarted get() = parent.enabledTypes.contains(tetherType) open val isStarted: Boolean? get() = parent.enabledTypes.contains(tetherType)
protected open val text: CharSequence get() = baseError ?: "" protected open val text: CharSequence get() = baseError ?: ""
protected var baseError: String? = null protected var baseError: String? = null
@@ -93,6 +98,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
protected abstract fun start() protected abstract fun start()
protected abstract fun stop() protected abstract fun stop()
protected open fun onClickNull(): Unit = throw UnsupportedOperationException()
override fun onTetheringStarted() = data.notifyChange() override fun onTetheringStarted() = data.notifyChange()
override fun onTetheringFailed(error: Int?) { override fun onTetheringFailed(error: Int?) {
@@ -119,11 +125,13 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
(viewHolder as ViewHolder).manager = this (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 } val interested = errored.filter { TetherType.ofInterface(it) == tetherType }
baseError = if (interested.isEmpty()) null else interested.joinToString("\n") { iface -> baseError = if (interested.isEmpty()) null else interested.joinToString("\n") { iface ->
"$iface: " + try { "$iface: " + try {
TetheringManager.tetherErrorLookup(TetheringManager.getLastTetherError(iface)) TetheringManager.tetherErrorLookup(if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
TetheringManager.getLastTetherError(iface)
} else lastErrors[iface] ?: 0)
} catch (e: InvocationTargetException) { } catch (e: InvocationTargetException) {
if (Build.VERSION.SDK_INT !in 24..25 || e.cause !is SecurityException) Timber.w(e) else Timber.d(e) if (Build.VERSION.SDK_INT !in 24..25 || e.cause !is SecurityException) Timber.w(e) else Timber.d(e)
e.readableMessage e.readableMessage
@@ -135,77 +143,156 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
@RequiresApi(24) @RequiresApi(24)
class Wifi(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver, class Wifi(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver,
WifiApManager.SoftApCallbackCompat { WifiApManager.SoftApCallbackCompat {
private val receiver = broadcastReceiver { _, intent ->
failureReason = if (intent.wifiApState == WifiApManager.WIFI_AP_STATE_FAILED) {
intent.getIntExtra(WifiApManager.EXTRA_WIFI_AP_FAILURE_REASON, 0)
} else null
data.notifyChange()
}
private var failureReason: Int? = null private var failureReason: Int? = null
private var numClients: Int? = null private var numClients: Int? = null
private var frequency = 0 private var info = emptyList<Parcelable>()
private var bandwidth = WifiApManager.CHANNEL_WIDTH_INVALID private var capability: Parcelable? = null
private var capability: Pair<Int, Long>? = null
init { 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) { 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) { 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) { override fun onStateChanged(state: Int, failureReason: Int) {
if (state < 10 || state > 14) { if (!WifiApManager.checkWifiApState(state)) return
Timber.w(Exception("Unknown state $state, $failureReason")) this.failureReason = if (state == WifiApManager.WIFI_AP_STATE_FAILED) failureReason else null
return
}
this.failureReason = if (state == 14) failureReason else null // WIFI_AP_STATE_FAILED
data.notifyChange() data.notifyChange()
} }
override fun onNumClientsChanged(numClients: Int) { override fun onNumClientsChanged(numClients: Int) {
this.numClients = numClients 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() data.notifyChange()
} }
override fun onCapabilityChanged(maxSupportedClients: Int, supportedFeatures: Long) { override fun onInfoChanged(info: List<Parcelable>) {
capability = maxSupportedClients to supportedFeatures this.info = info
data.notifyChange() data.notifyChange()
} }
override fun onBlockedClientConnecting(client: MacAddress, blockedReason: Int) { override fun onCapabilityChanged(capability: Parcelable) {
val reason = WifiApManager.clientBlockLookup(blockedReason, true) this.capability = capability
Timber.i("$client blocked from connecting: $reason ($blockedReason)") data.notifyChange()
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 val title get() = parent.getString(R.string.tethering_manage_wifi) override val title get() = parent.getString(R.string.tethering_manage_wifi)
override val tetherType get() = TetherType.WIFI override val tetherType get() = TetherType.WIFI
override val type get() = VIEW_TYPE_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) { @TargetApi(30)
parent.getString(R.string.tethering_manage_wifi_info, frequency, private fun formatCapability(locale: Locale) = capability?.let { parcel ->
SoftApConfigurationCompat.frequencyToChannel(frequency), val capability = SoftApCapability(parcel)
WifiApManager.channelWidthLookup(bandwidth, true)) val numClients = numClients
} else null, val maxClients = capability.maxSupportedClients
capability?.let { (maxSupportedClients, supportedFeatures) -> var features = capability.supportedFeatures
app.resources.getQuantityString(R.plurals.tethering_manage_wifi_capabilities, maxSupportedClients, if (Build.VERSION.SDK_INT >= 31) for ((flag, band) in arrayOf(
numClients ?: "?", maxSupportedClients, sequence { SoftApCapability.SOFTAP_FEATURE_BAND_24G_SUPPORTED to SoftApConfigurationCompat.BAND_2GHZ,
var features = supportedFeatures SoftApCapability.SOFTAP_FEATURE_BAND_5G_SUPPORTED to SoftApConfigurationCompat.BAND_5GHZ,
if (features == 0L) yield(parent.getString(R.string.tethering_manage_wifi_no_features)) SoftApCapability.SOFTAP_FEATURE_BAND_6G_SUPPORTED to SoftApConfigurationCompat.BAND_6GHZ,
else while (features != 0L) { SoftApCapability.SOFTAP_FEATURE_BAND_60G_SUPPORTED to SoftApConfigurationCompat.BAND_60GHZ,
val bit = features.takeLowestOneBit() )) {
yield(WifiApManager.featureLookup(bit, true)) if (capability.getSupportedChannelList(band).isEmpty()) continue
features = features and bit.inv() // 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(SoftApCapability.featureLookup(bit, true))
features = features and bit.inv()
}
}.joinToSpanned().let {
if (it.isEmpty()) parent.getText(R.string.tethering_manage_wifi_no_features) else it
})
if (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
} }
}.joinToString()) pending?.let {
}).joinToString("\n") 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 start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI, this::onException) override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI, this::onException)
@@ -220,25 +307,24 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_USB, this::onException) override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_USB, this::onException)
} }
@RequiresApi(24) @RequiresApi(24)
class Bluetooth(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver { class Bluetooth(parent: TetheringFragment, adapter: BluetoothAdapter) :
companion object { TetherManager(parent), DefaultLifecycleObserver {
// TODO: migrate to framework Manifest.permission when stable private val tethering = BluetoothTethering(parent.requireContext(), adapter) { data.notifyChange() }
private const val BLUETOOTH_CONNECT = "android.permission.BLUETOOTH_CONNECT"
}
private val tethering = BluetoothTethering(parent.requireContext()) { data.notifyChange() }
init { init {
parent.viewLifecycleOwner.lifecycle.addObserver(this) parent.viewLifecycleOwner.lifecycle.addObserver(this)
} }
fun ensureInit(context: Context) = tethering.ensureInit(context) fun ensureInit(context: Context) {
tethering.ensureInit(context)
onTetheringStarted() // force flush
}
override fun onResume(owner: LifecycleOwner) { override fun onResume(owner: LifecycleOwner) {
if (!BuildCompat.isAtLeastS() || parent.requireContext().checkSelfPermission(BLUETOOTH_CONNECT) == if (Build.VERSION.SDK_INT < 31 || parent.requireContext().checkSelfPermission(
PackageManager.PERMISSION_GRANTED) { Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
ensureInit(parent.requireContext()) tethering.ensureInit(parent.requireContext())
} else if (parent.shouldShowRequestPermissionRationale(BLUETOOTH_CONNECT)) { } else if (parent.shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_CONNECT)) {
parent.requestBluetooth.launch(BLUETOOTH_CONNECT) parent.requestBluetooth.launch(Manifest.permission.BLUETOOTH_CONNECT)
} }
} }
override fun onDestroy(owner: LifecycleOwner) = tethering.close() 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 title get() = parent.getString(R.string.tethering_manage_bluetooth)
override val tetherType get() = TetherType.BLUETOOTH override val tetherType get() = TetherType.BLUETOOTH
override val type get() = VIEW_TYPE_BLUETOOTH override val type get() = VIEW_TYPE_BLUETOOTH
override val isStarted get() = tethering.active == true override val isStarted get() = tethering.active
override val text get() = listOfNotNull( override val text get() = listOfNotNull(
if (tethering.active == null) tethering.activeFailureCause?.readableMessage else null, if (tethering.active == null) tethering.activeFailureCause?.readableMessage else null,
baseError).joinToString("\n") baseError).joinToString("\n")
override fun start() = BluetoothTethering.start(this) override fun start() = tethering.start(this)
override fun stop() { override fun stop() {
TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, this::onException) tethering.stop(this::onException)
Thread.sleep(1) // give others a room to breathe
onTetheringStarted() // force flush state onTetheringStarted() // force flush state
} }
override fun onClickNull() = ManageBar.start(parent.requireContext())
} }
@RequiresApi(30) @RequiresApi(30)
class Ethernet(parent: TetheringFragment) : TetherManager(parent) { class Ethernet(parent: TetheringFragment) : TetherManager(parent) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.net.wifi.SoftApConfiguration
import android.os.Build import android.os.Build
import android.os.Parcelable import android.os.Parcelable
import android.util.SparseIntArray
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.net.MacAddressCompat import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toCompat import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toCompat
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
import be.mygod.vpnhotspot.util.ConstantLookup
import be.mygod.vpnhotspot.util.UnblockCentral
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber
@Parcelize @Parcelize
data class SoftApConfigurationCompat( data class SoftApConfigurationCompat(
@@ -19,13 +23,18 @@ data class SoftApConfigurationCompat(
var bssidAddr: Long? = null, var bssidAddr: Long? = null,
var passphrase: String? = null, var passphrase: String? = null,
var isHiddenSsid: Boolean = false, var isHiddenSsid: Boolean = false,
/**
* To read legacy band/channel pair, use [requireSingleBand]. For easy access, see [getChannel].
*
* You should probably set or modify this field directly only when you want to use bridged AP,
* see also [android.net.wifi.WifiManager.isBridgedApConcurrencySupported].
* Otherwise, use [optimizeChannels] or [setChannel].
*/
@TargetApi(23) @TargetApi(23)
var band: Int = BAND_2GHZ, var channels: SparseIntArray = SparseIntArray(1).apply { append(BAND_2GHZ, 0) },
@TargetApi(23) var securityType: Int = SoftApConfiguration.SECURITY_TYPE_OPEN,
var channel: Int = 0,
@TargetApi(30) @TargetApi(30)
var maxNumberOfClients: Int = 0, var maxNumberOfClients: Int = 0,
var securityType: Int = SoftApConfiguration.SECURITY_TYPE_OPEN,
@TargetApi(28) @TargetApi(28)
var isAutoShutdownEnabled: Boolean = true, var isAutoShutdownEnabled: Boolean = true,
@TargetApi(28) @TargetApi(28)
@@ -36,12 +45,41 @@ data class SoftApConfigurationCompat(
var blockedClientList: List<MacAddress> = emptyList(), var blockedClientList: List<MacAddress> = emptyList(),
@RequiresApi(30) @RequiresApi(30)
var allowedClientList: List<MacAddress> = emptyList(), 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 { var underlying: Parcelable? = null) : Parcelable {
companion object { companion object {
const val BAND_2GHZ = 1 const val BAND_2GHZ = 1
const val BAND_5GHZ = 2 const val BAND_5GHZ = 2
@TargetApi(30)
const val BAND_6GHZ = 4 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] * [android.net.wifi.WifiConfiguration.KeyMgmt.WPA2_PSK]
*/ */
@@ -53,7 +91,8 @@ data class SoftApConfigurationCompat(
/** /**
* Based on: * 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 * https://cs.android.com/android/platform/superproject/+/master:frameworks/base/wifi/java/android/net/wifi/ScanResult.java;l=624;drc=f7ccda05642b55700d67a288462bada488fc7f5e
*/ */
fun channelToFrequency(band: Int, chan: Int) = when (band) { fun channelToFrequency(band: Int, chan: Int) = when (band) {
@@ -67,18 +106,24 @@ data class SoftApConfigurationCompat(
in 1..Int.MAX_VALUE -> 5000 + chan * 5 in 1..Int.MAX_VALUE -> 5000 + chan * 5
else -> throw IllegalArgumentException("Invalid 5GHz channel $chan") else -> throw IllegalArgumentException("Invalid 5GHz channel $chan")
} }
BAND_6GHZ -> if (chan in 1..253) { BAND_6GHZ -> when (chan) {
5940 + chan * 5 2 -> 5935
} else throw IllegalArgumentException("Invalid 6GHz channel $chan") in 1..233 -> 5950 + chan * 5
// BAND_60GHZ -> if (chan in 1 until 7) 56160 + chan * 2160 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") else -> throw IllegalArgumentException("Invalid band $band")
} }
fun frequencyToChannel(freq: Int) = when (freq) { fun frequencyToChannel(freq: Int) = when (freq) {
2484 -> 14 2484 -> 14
in Int.MIN_VALUE until 2484 -> (freq - 2407) / 5 in Int.MIN_VALUE until 2484 -> (freq - 2407) / 5
in 4910..4980 -> (freq - 4000) / 5 in 4910..4980 -> (freq - 4000) / 5
in Int.MIN_VALUE until 5945 -> (freq - 5000) / 5 in Int.MIN_VALUE until 5925 -> (freq - 5000) / 5
in Int.MIN_VALUE..45000 -> (freq - 5940) / 5 5935 -> 2
in Int.MIN_VALUE..45000 -> (freq - 5950) / 5
in 58320..70200 -> (freq - 56160) / 2160 in 58320..70200 -> (freq - 56160) / 2160
else -> throw IllegalArgumentException("Invalid frequency $freq") else -> throw IllegalArgumentException("Invalid frequency $freq")
} }
@@ -122,6 +167,14 @@ data class SoftApConfigurationCompat(
private val getChannel by lazy @TargetApi(30) { private val getChannel by lazy @TargetApi(30) {
SoftApConfiguration::class.java.getDeclaredMethod("getChannel") 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) @get:RequiresApi(30)
private val getMaxNumberOfClients by lazy @TargetApi(30) { private val getMaxNumberOfClients by lazy @TargetApi(30) {
SoftApConfiguration::class.java.getDeclaredMethod("getMaxNumberOfClients") SoftApConfiguration::class.java.getDeclaredMethod("getMaxNumberOfClients")
@@ -134,10 +187,22 @@ data class SoftApConfigurationCompat(
private val isAutoShutdownEnabled by lazy @TargetApi(30) { private val isAutoShutdownEnabled by lazy @TargetApi(30) {
SoftApConfiguration::class.java.getDeclaredMethod("isAutoShutdownEnabled") SoftApConfiguration::class.java.getDeclaredMethod("isAutoShutdownEnabled")
} }
@get:RequiresApi(31)
private val isBridgedModeOpportunisticShutdownEnabled by lazy @TargetApi(31) {
SoftApConfiguration::class.java.getDeclaredMethod("isBridgedModeOpportunisticShutdownEnabled")
}
@get:RequiresApi(30) @get:RequiresApi(30)
private val isClientControlByUserEnabled by lazy @TargetApi(30) { private val isClientControlByUserEnabled by lazy @TargetApi(30) {
SoftApConfiguration::class.java.getDeclaredMethod("isClientControlByUserEnabled") 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) @get:RequiresApi(30)
private val classBuilder by lazy { Class.forName("android.net.wifi.SoftApConfiguration\$Builder") } private val classBuilder by lazy { Class.forName("android.net.wifi.SoftApConfiguration\$Builder") }
@@ -159,6 +224,10 @@ data class SoftApConfigurationCompat(
private val setBlockedClientList by lazy { private val setBlockedClientList by lazy {
classBuilder.getDeclaredMethod("setBlockedClientList", java.util.List::class.java) 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) @get:RequiresApi(30)
private val setBssid by lazy @TargetApi(30) { private val setBssid by lazy @TargetApi(30) {
classBuilder.getDeclaredMethod("setBssid", MacAddress::class.java) classBuilder.getDeclaredMethod("setBssid", MacAddress::class.java)
@@ -167,12 +236,24 @@ data class SoftApConfigurationCompat(
private val setChannel by lazy { private val setChannel by lazy {
classBuilder.getDeclaredMethod("setChannel", Int::class.java, Int::class.java) 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) @get:RequiresApi(30)
private val setClientControlByUserEnabled by lazy { private val setClientControlByUserEnabled by lazy {
classBuilder.getDeclaredMethod("setClientControlByUserEnabled", Boolean::class.java) classBuilder.getDeclaredMethod("setClientControlByUserEnabled", Boolean::class.java)
} }
@get:RequiresApi(30) @get:RequiresApi(30)
private val setHiddenSsid by lazy { classBuilder.getDeclaredMethod("setHiddenSsid", Boolean::class.java) } private val setHiddenSsid by lazy { 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) @get:RequiresApi(30)
private val setMaxNumberOfClients by lazy { private val setMaxNumberOfClients by lazy {
classBuilder.getDeclaredMethod("setMaxNumberOfClients", Int::class.java) classBuilder.getDeclaredMethod("setMaxNumberOfClients", Int::class.java)
@@ -187,6 +268,8 @@ data class SoftApConfigurationCompat(
} }
@get:RequiresApi(30) @get:RequiresApi(30)
private val setSsid by lazy { classBuilder.getDeclaredMethod("setSsid", String::class.java) } 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") @Deprecated("Class deprecated in framework")
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@@ -196,14 +279,14 @@ data class SoftApConfigurationCompat(
preSharedKey, preSharedKey,
hiddenSSID, hiddenSSID,
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/wifi/java/android/net/wifi/SoftApConfToXmlMigrationUtil.java;l=87;drc=aa6527cf41671d1ed417b8ebdb6b3aa614f62344 // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/wifi/java/android/net/wifi/SoftApConfToXmlMigrationUtil.java;l=87;drc=aa6527cf41671d1ed417b8ebdb6b3aa614f62344
if (Build.VERSION.SDK_INT >= 23) when (val band = apBand.getInt(this)) { SparseIntArray(1).also {
0 -> BAND_2GHZ if (Build.VERSION.SDK_INT >= 23) it.append(when (val band = apBand.getInt(this)) {
1 -> BAND_5GHZ 0 -> BAND_2GHZ
-1 -> BAND_2GHZ or BAND_5GHZ 1 -> BAND_5GHZ
else -> throw IllegalArgumentException("Unexpected band $band") -1 -> BAND_LEGACY
} else BAND_ANY, else -> throw IllegalArgumentException("Unexpected band $band")
if (Build.VERSION.SDK_INT >= 23) apChannel.getInt(this) else 0, }, apChannel.getInt(this)) else it.append(BAND_LEGACY, 0)
0, },
allowedKeyManagement.nextSetBit(0).let { selected -> allowedKeyManagement.nextSetBit(0).let { selected ->
require(allowedKeyManagement.nextSetBit(selected + 1) < 0) { require(allowedKeyManagement.nextSetBit(selected + 1) < 0) {
"More than 1 key managements supplied: $allowedKeyManagement" "More than 1 key managements supplied: $allowedKeyManagement"
@@ -224,26 +307,32 @@ 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) underlying = this)
@RequiresApi(30) @RequiresApi(30)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun SoftApConfiguration.toCompat() = SoftApConfigurationCompat( fun SoftApConfiguration.toCompat() = SoftApConfigurationCompat(
ssid, ssid,
bssid?.toCompat()?.addr, bssid?.toCompat()?.addr,
passphrase, passphrase,
isHiddenSsid, isHiddenSsid,
getBand(this) as Int, if (Build.VERSION.SDK_INT >= 31) getChannels(this) as SparseIntArray else SparseIntArray(1).also {
getChannel(this) as Int, it.append(getBand(this) as Int, getChannel(this) as Int)
getMaxNumberOfClients(this) as Int, },
securityType, securityType,
isAutoShutdownEnabled(this) as Boolean, getMaxNumberOfClients(this) as Int,
getShutdownTimeoutMillis(this) as Long, isAutoShutdownEnabled(this) as Boolean,
isClientControlByUserEnabled(this) as Boolean, getShutdownTimeoutMillis(this) as Long,
getBlockedClientList(this) as List<MacAddress>, isClientControlByUserEnabled(this) as Boolean,
getAllowedClientList(this) as List<MacAddress>, getBlockedClientList(this) as List<MacAddress>,
this) getAllowedClientList(this) as List<MacAddress>,
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") @Suppress("DEPRECATION")
@@ -253,6 +342,47 @@ data class SoftApConfigurationCompat(
bssidAddr = value?.addr 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: * Based on:
* https://android.googlesource.com/platform/packages/apps/Settings/+/android-5.0.0_r1/src/com/android/settings/wifi/WifiApDialog.java#88 * https://android.googlesource.com/platform/packages/apps/Settings/+/android-5.0.0_r1/src/com/android/settings/wifi/WifiApDialog.java#88
@@ -263,6 +393,7 @@ data class SoftApConfigurationCompat(
@Deprecated("Class deprecated in framework, use toPlatform().toWifiConfiguration()") @Deprecated("Class deprecated in framework, use toPlatform().toWifiConfiguration()")
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
fun toWifiConfiguration(): android.net.wifi.WifiConfiguration { fun toWifiConfiguration(): android.net.wifi.WifiConfiguration {
val (band, channel) = requireSingleBand()
val wc = underlying as? android.net.wifi.WifiConfiguration val wc = underlying as? android.net.wifi.WifiConfiguration
val result = if (wc == null) android.net.wifi.WifiConfiguration() else android.net.wifi.WifiConfiguration(wc) val result = if (wc == null) android.net.wifi.WifiConfiguration() else android.net.wifi.WifiConfiguration(wc)
val original = wc?.toCompat() val original = wc?.toCompat()
@@ -273,11 +404,14 @@ data class SoftApConfigurationCompat(
apBand.setInt(result, when (band) { apBand.setInt(result, when (band) {
BAND_2GHZ -> 0 BAND_2GHZ -> 0
BAND_5GHZ -> 1 BAND_5GHZ -> 1
BAND_2GHZ or BAND_5GHZ, BAND_ANY -> -1 else -> {
else -> throw IllegalArgumentException("Convert fail, unsupported band setting :$band") 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) 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) { if (original?.securityType != securityType) {
result.allowedKeyManagement.clear() result.allowedKeyManagement.clear()
result.allowedKeyManagement.set(when (securityType) { result.allowedKeyManagement.set(when (securityType) {
@@ -304,7 +438,10 @@ data class SoftApConfigurationCompat(
setSsid(builder, ssid) setSsid(builder, ssid)
setPassphrase(builder, if (securityType == SoftApConfiguration.SECURITY_TYPE_OPEN) null else passphrase, setPassphrase(builder, if (securityType == SoftApConfiguration.SECURITY_TYPE_OPEN) null else passphrase,
securityType) securityType)
if (channel == 0) setBand(builder, band) else setChannel(builder, channel, band) 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()) setBssid(builder, bssid?.toPlatform())
setMaxNumberOfClients(builder, maxNumberOfClients) setMaxNumberOfClients(builder, maxNumberOfClients)
setShutdownTimeoutMillis(builder, shutdownTimeoutMillis) setShutdownTimeoutMillis(builder, shutdownTimeoutMillis)
@@ -313,6 +450,16 @@ data class SoftApConfigurationCompat(
setHiddenSsid(builder, isHiddenSsid) setHiddenSsid(builder, isHiddenSsid)
setAllowedClientList(builder, allowedClientList) setAllowedClientList(builder, allowedClientList)
setBlockedClientList(builder, blockedClientList) 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 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.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.util.Base64 import android.util.Base64
import android.util.SparseIntArray
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Spinner
import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
@@ -30,8 +33,8 @@ import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
import be.mygod.vpnhotspot.util.QRCodeDialog import be.mygod.vpnhotspot.util.QRCodeDialog
import be.mygod.vpnhotspot.util.readableMessage import be.mygod.vpnhotspot.util.readableMessage
import be.mygod.vpnhotspot.util.showAllowingStateLoss import be.mygod.vpnhotspot.util.showAllowingStateLoss
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.parcelize.Parcelize 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 * 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 * Related: https://android.googlesource.com/platform/packages/apps/Settings/+/defb1183ecb00d6231bac7d934d07f58f90261ea
*/ */
class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiApDialogFragment.Arg>(), TextWatcher, class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiApDialogFragment.Arg>(), TextWatcher,
Toolbar.OnMenuItemClickListener { Toolbar.OnMenuItemClickListener, AdapterView.OnItemSelectedListener {
companion object { companion object {
private const val BASE64_FLAGS = Base64.NO_PADDING or Base64.NO_WRAP private const val BASE64_FLAGS = Base64.NO_PADDING or Base64.NO_WRAP
private val nonMacChars = "[^0-9a-fA-F:]+".toRegex() private val nonMacChars = "[^0-9a-fA-F:]+".toRegex()
private val channels by lazy { private val baseOptions by lazy { listOf(ChannelOption.Disabled, ChannelOption.Auto) }
val list = ArrayList<BandOption.Channel>() private val channels2G by lazy {
for (chan in 1..14) list.add(BandOption.Channel(SoftApConfigurationCompat.BAND_2GHZ, chan)) baseOptions + (1..14).map { ChannelOption(it, SoftApConfigurationCompat.BAND_2GHZ) }
for (chan in 1..196) list.add(BandOption.Channel(SoftApConfigurationCompat.BAND_5GHZ, chan)) }
if (Build.VERSION.SDK_INT >= 30) { private val channels5G by lazy {
for (chan in 1..253) list.add(BandOption.Channel(SoftApConfigurationCompat.BAND_6GHZ, chan)) baseOptions + (1..196).map { ChannelOption(it, SoftApConfigurationCompat.BAND_5GHZ) }
} }
list @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 * Source: https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/c2fc6a1/service/java/com/android/server/wifi/p2p/SupplicantP2pIfaceHal.java#1396
*/ */
private val p2pChannels by lazy { private val p2pChannels by lazy {
(1..165).map { baseOptions + (15..165).map { ChannelOption(it, SoftApConfigurationCompat.BAND_5GHZ) }
val band = if (it <= 14) SoftApConfigurationCompat.BAND_2GHZ else SoftApConfigurationCompat.BAND_5GHZ
BandOption.Channel(band, it)
}
} }
} }
@@ -73,35 +79,20 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
*/ */
val p2pMode: Boolean = false) : Parcelable val p2pMode: Boolean = false) : Parcelable
private sealed class BandOption { private open class ChannelOption(val channel: Int = 0, private val band: Int = 0) {
open val band get() = SoftApConfigurationCompat.BAND_ANY object Disabled : ChannelOption(-1) {
open val channel get() = 0 override fun toString() = app.getString(R.string.wifi_ap_choose_disabled)
}
object BandAny : BandOption() { object Auto : ChannelOption() {
override fun toString() = app.getString(R.string.wifi_ap_choose_auto) override fun toString() = app.getString(R.string.wifi_ap_choose_auto)
} }
object Band2GHz : BandOption() { override fun toString() = "${SoftApConfigurationCompat.channelToFrequency(band, channel)} MHz ($channel)"
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 dialogView: DialogWifiApBinding
private lateinit var bandOptions: MutableList<BandOption>
private lateinit var base: SoftApConfigurationCompat private lateinit var base: SoftApConfigurationCompat
private var started = false private var started = false
private val currentChannels5G get() = if (arg.p2pMode && !RepeaterService.safeMode) p2pChannels else channels5G
override val ret get() = Arg(generateConfig()) override val ret get() = Arg(generateConfig())
private fun generateConfig(full: Boolean = true) = base.copy( 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 (text.isNullOrEmpty()) 0 else text.toString().toLong()
} }
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) { if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) {
val bandOption = dialogView.band.selectedItem as BandOption val channels = SparseIntArray(4)
band = bandOption.band for ((band, spinner) in arrayOf(SoftApConfigurationCompat.BAND_2GHZ to dialogView.band2G,
channel = bandOption.channel 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) { bssid = if (dialogView.bssid.length() != 0) {
MacAddressCompat.fromString(dialogView.bssid.text.toString()) MacAddressCompat.fromString(dialogView.bssid.text.toString())
@@ -132,6 +131,10 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
.filter { it.isNotEmpty() }.map { MacAddressCompat.fromString(it).toPlatform() } .filter { it.isNotEmpty() }.map { MacAddressCompat.fromString(it).toPlatform() }
blockedClientList = (dialogView.blockedList.text ?: "").split(nonMacChars) blockedClientList = (dialogView.blockedList.text ?: "").split(nonMacChars)
.filter { it.isNotEmpty() }.map { MacAddressCompat.fromString(it).toPlatform() } .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) TetherTimeoutMonitor.defaultTimeout)
dialogView.timeout.addTextChangedListener(this@WifiApDialogFragment) dialogView.timeout.addTextChangedListener(this@WifiApDialogFragment)
} else dialogView.timeoutWrapper.isGone = true } else dialogView.timeoutWrapper.isGone = true
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) dialogView.band.apply { fun Spinner.configure(options: List<ChannelOption>) {
bandOptions = mutableListOf<BandOption>().apply { adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0, options).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 {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
} }
} else dialogView.bandWrapper.isGone = true onItemSelectedListener = this@WifiApDialogFragment
dialogView.bssid.addTextChangedListener(this@WifiApDialogFragment) }
if (arg.p2pMode) dialogView.hiddenSsid.isGone = true if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) {
if (arg.p2pMode || Build.VERSION.SDK_INT < 30) { dialogView.band2G.configure(channels2G)
dialogView.maxClientWrapper.isGone = true dialogView.band5G.configure(currentChannels5G)
dialogView.clientUserControl.isGone = true if (Build.VERSION.SDK_INT >= 30 && !arg.p2pMode) dialogView.band6G.configure(channels6G)
dialogView.blockedListWrapper.isGone = true else dialogView.bandWrapper6G.isGone = true
dialogView.allowedListWrapper.isGone = true if (Build.VERSION.SDK_INT >= 31 && !arg.p2pMode) dialogView.band60G.configure(channels60G) else {
} 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.maxClient.addTextChangedListener(this@WifiApDialogFragment)
dialogView.blockedList.addTextChangedListener(this@WifiApDialogFragment) dialogView.blockedList.addTextChangedListener(this@WifiApDialogFragment)
dialogView.allowedList.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 base = arg.configuration
populateFromConfiguration() 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() { private fun populateFromConfiguration() {
dialogView.ssid.setText(base.ssid) dialogView.ssid.setText(base.ssid)
if (!arg.p2pMode) dialogView.security.setSelection(base.securityType) if (!arg.p2pMode) dialogView.security.setSelection(base.securityType)
@@ -208,10 +237,13 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
dialogView.autoShutdown.isChecked = base.isAutoShutdownEnabled dialogView.autoShutdown.isChecked = base.isAutoShutdownEnabled
dialogView.timeout.setText(base.shutdownTimeoutMillis.let { if (it == 0L) "" else it.toString() }) dialogView.timeout.setText(base.shutdownTimeoutMillis.let { if (it == 0L) "" else it.toString() })
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) { if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) {
val selection = if (base.channel != 0) { dialogView.band2G.setSelection(locate(SoftApConfigurationCompat.BAND_2GHZ, channels2G))
bandOptions.indexOfFirst { it.channel == base.channel } dialogView.band5G.setSelection(locate(SoftApConfigurationCompat.BAND_5GHZ, currentChannels5G))
} else bandOptions.indexOfFirst { it.band == base.band } dialogView.band6G.setSelection(locate(SoftApConfigurationCompat.BAND_6GHZ, channels6G))
dialogView.band.setSelection(if (selection == -1) 0 else selection) dialogView.band60G.setSelection(locate(SoftApConfigurationCompat.BAND_60GHZ, channels60G))
userBridgedMode = base.channels.size() > 1
dialogView.bridgedMode.isChecked = userBridgedMode
setBridgedMode()
} }
dialogView.bssid.setText(base.bssid?.toString()) dialogView.bssid.setText(base.bssid?.toString())
dialogView.hiddenSsid.isChecked = base.isHiddenSsid dialogView.hiddenSsid.isChecked = base.isHiddenSsid
@@ -219,6 +251,11 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
dialogView.clientUserControl.isChecked = base.isClientControlByUserEnabled dialogView.clientUserControl.isChecked = base.isClientControlByUserEnabled
dialogView.blockedList.setText(base.blockedClientList.joinToString("\n")) dialogView.blockedList.setText(base.blockedClientList.joinToString("\n"))
dialogView.allowedList.setText(base.allowedClientList.joinToString("\n")) dialogView.allowedList.setText(base.allowedClientList.joinToString("\n"))
dialogView.macRandomization.isChecked =
base.macRandomizationSetting == SoftApConfigurationCompat.RANDOMIZATION_PERSISTENT
dialogView.bridgedModeOpportunisticShutdown.isChecked = base.isBridgedModeOpportunisticShutdownEnabled
dialogView.ieee80211ax.isChecked = base.isIeee80211axEnabled
dialogView.userConfig.isChecked = base.isUserConfiguration
} }
override fun onStart() { override fun onStart() {
@@ -254,6 +291,29 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
} }
} }
dialogView.timeoutWrapper.error = timeoutError 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 dialogView.bssidWrapper.error = null
val bssidValid = dialogView.bssid.length() == 0 || try { val bssidValid = dialogView.bssid.length() == 0 || try {
MacAddressCompat.fromString(dialogView.bssid.text.toString()) 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 && val canCopy = timeoutError == null && bssidValid && maxClientError == null && blockedListError == null &&
allowedListError == null allowedListError == null
(dialog as? AlertDialog)?.getButton(DialogInterface.BUTTON_POSITIVE)?.isEnabled = (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 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 beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { }
override fun afterTextChanged(editable: Editable) = validate() 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 { override fun onMenuItemClick(item: MenuItem?): Boolean {
return when (item?.itemId) { return when (item?.itemId) {
android.R.id.copy -> { android.R.id.copy -> {
@@ -318,7 +381,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
} }
true true
} catch (e: RuntimeException) { } catch (e: RuntimeException) {
SmartSnackbar.make(e).show() Toast.makeText(context, e.readableMessage, Toast.LENGTH_LONG).show()
false false
} }
R.id.share_qr -> { R.id.share_qr -> {

View File

@@ -3,18 +3,16 @@ package be.mygod.vpnhotspot.net.wifi
import android.annotation.TargetApi import android.annotation.TargetApi
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.MacAddress import android.content.res.Resources
import android.net.wifi.SoftApConfiguration import android.net.wifi.SoftApConfiguration
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Parcelable
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
import be.mygod.vpnhotspot.util.ConstantLookup import be.mygod.vpnhotspot.util.*
import be.mygod.vpnhotspot.util.LongConstantLookup
import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.callSuper
import timber.log.Timber import timber.log.Timber
import java.lang.reflect.InvocationHandler import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method import java.lang.reflect.Method
@@ -27,6 +25,8 @@ object WifiApManager {
*/ */
@RequiresApi(30) @RequiresApi(30)
private const val ACTION_RESOURCES_APK = "com.android.server.wifi.intent.action.SERVICE_WIFI_RESOURCES_APK" 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 * 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), val resolvedActivity get() = app.packageManager.queryIntentActivities(Intent(ACTION_RESOURCES_APK),
PackageManager.MATCH_SYSTEM_ONLY).single() 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") } private val getWifiApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getWifiApConfiguration") }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
private val setWifiApConfiguration by lazy { private val setWifiApConfiguration by lazy {
@@ -63,40 +172,65 @@ object WifiApManager {
/** /**
* Called when soft AP state changes. * Called when soft AP state changes.
* *
* @param state new new AP state. One of {@link #WIFI_AP_STATE_DISABLED}, * @param state the new AP state. One of [WIFI_AP_STATE_DISABLED], [WIFI_AP_STATE_DISABLING],
* {@link #WIFI_AP_STATE_DISABLING}, {@link #WIFI_AP_STATE_ENABLED}, * [WIFI_AP_STATE_ENABLED], [WIFI_AP_STATE_ENABLING], [WIFI_AP_STATE_FAILED]
* {@link #WIFI_AP_STATE_ENABLING}, {@link #WIFI_AP_STATE_FAILED}
* @param failureReason reason when in failed state. One of * @param failureReason reason when in failed state. One of
* {@link #SAP_START_FAILURE_GENERAL}, {@link #SAP_START_FAILURE_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) { } fun onStateChanged(state: Int, failureReason: Int) { }
/** /**
* Called when number of connected clients to soft AP changes. * 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 * @param numClients number of connected clients
*/ */
@Deprecated("onConnectedClientsChanged")
fun onNumClientsChanged(numClients: Int) { } fun onNumClientsChanged(numClients: Int) { }
/**
* Called when the connected clients to soft AP changes.
*
* @param clients the currently connected clients
*/
@RequiresApi(30) @RequiresApi(30)
fun onConnectedClientsChanged(clients: List<MacAddress>) { fun onConnectedClientsChanged(clients: List<Parcelable>) = onNumClientsChanged(clients.size)
@Suppress("DEPRECATION")
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) @RequiresApi(30)
fun onInfoChanged(frequency: Int, bandwidth: Int) { } fun onInfoChanged(info: List<Parcelable>) { }
/**
* Called when capability of softap changes.
*
* @param capability is the softap capability. [SoftApCapability]
*/
@RequiresApi(30) @RequiresApi(30)
fun onCapabilityChanged(maxSupportedClients: Int, supportedFeatures: Long) { } 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) @RequiresApi(30)
fun onBlockedClientConnecting(client: MacAddress, blockedReason: Int) { } fun onBlockedClientConnecting(client: Parcelable, blockedReason: Int) { }
} }
@RequiresApi(28) @RequiresApi(23)
val failureReasonLookup = ConstantLookup<WifiManager>("SAP_START_FAILURE_", val failureReasonLookup = ConstantLookup<WifiManager>("SAP_START_FAILURE_", "GENERAL", "NO_CHANNEL")
"SAP_START_FAILURE_GENERAL", "SAP_START_FAILURE_NO_CHANNEL")
@get:RequiresApi(30) @get:RequiresApi(30)
val clientBlockLookup by lazy { ConstantLookup<WifiManager>("SAP_CLIENT_") } val clientBlockLookup by lazy { ConstantLookup<WifiManager>("SAP_CLIENT_") }
@@ -111,25 +245,6 @@ object WifiApManager {
WifiManager::class.java.getDeclaredMethod("unregisterSoftApCallback", interfaceSoftApCallback) 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) @RequiresApi(28)
fun registerSoftApCallback(callback: SoftApCallbackCompat, executor: Executor): Any { fun registerSoftApCallback(callback: SoftApCallbackCompat, executor: Executor): Any {
val proxy = Proxy.newProxyInstance(interfaceSoftApCallback.classLoader, val proxy = Proxy.newProxyInstance(interfaceSoftApCallback.classLoader,
@@ -141,49 +256,37 @@ object WifiApManager {
} else invokeActual(proxy, method, args) } else invokeActual(proxy, method, args)
private fun invokeActual(proxy: Any, method: Method, args: Array<out Any?>?): Any? { private fun invokeActual(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
val noArgs = args?.size ?: 0 return when {
return when (val name = method.name) { method.matches("onStateChanged", Integer.TYPE, Integer.TYPE) -> {
"onStateChanged" -> {
if (noArgs != 2) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
callback.onStateChanged(args!![0] as Int, args[1] as Int) callback.onStateChanged(args!![0] as Int, args[1] as Int)
} }
"onNumClientsChanged" -> @Suppress("DEPRECATION") { method.matches("onNumClientsChanged", Integer.TYPE) -> @Suppress("DEPRECATION") {
if (Build.VERSION.SDK_INT >= 30) Timber.w(Exception("Unexpected onNumClientsChanged")) if (Build.VERSION.SDK_INT >= 30) Timber.w(Exception("Unexpected onNumClientsChanged"))
if (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
callback.onNumClientsChanged(args!![0] as Int) callback.onNumClientsChanged(args!![0] as Int)
} }
"onConnectedClientsChanged" -> @TargetApi(30) { method.matches1<java.util.List<*>>("onConnectedClientsChanged") -> @TargetApi(30) {
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onConnectedClientsChanged")) if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onConnectedClientsChanged"))
if (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}") @Suppress("UNCHECKED_CAST")
callback.onConnectedClientsChanged((args!![0] as? Iterable<*> ?: return null) callback.onConnectedClientsChanged(args!![0] as List<Parcelable>)
.map { getMacAddress(it) as MacAddress })
} }
"onInfoChanged" -> @TargetApi(30) { method.matches1<java.util.List<*>>("onInfoChanged") -> @TargetApi(31) {
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onInfoChanged")) if (Build.VERSION.SDK_INT < 31) Timber.w(Exception("Unexpected onInfoChanged API 31+"))
if (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}") @Suppress("UNCHECKED_CAST")
val softApInfo = args!![0] callback.onInfoChanged(args!![0] as List<Parcelable>)
if (softApInfo != null && classSoftApInfo.isAssignableFrom(softApInfo.javaClass)) {
callback.onInfoChanged(getFrequency(softApInfo) as Int, getBandwidth(softApInfo) as Int)
} else null
} }
"onCapabilityChanged" -> @TargetApi(30) { Build.VERSION.SDK_INT >= 30 && method.matches("onInfoChanged", SoftApInfo.clazz) -> {
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onCapabilityChanged")) if (Build.VERSION.SDK_INT >= 31) return null // ignore old version calls
if (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}") val arg = args!![0]
val softApCapability = args!![0] val info = SoftApInfo(arg as Parcelable)
var supportedFeatures = 0L callback.onInfoChanged( // check for legacy empty info with CHANNEL_WIDTH_INVALID
var probe = 1L if (info.frequency == 0 && info.bandwidth == 0) emptyList() else listOf(arg))
while (probe != 0L) {
if (areFeaturesSupported(softApCapability, probe) as Boolean) {
supportedFeatures = supportedFeatures or probe
}
probe += probe
}
callback.onCapabilityChanged(getMaxSupportedClients(softApCapability) as Int, supportedFeatures)
} }
"onBlockedClientConnecting" -> @TargetApi(30) { Build.VERSION.SDK_INT >= 30 && method.matches("onCapabilityChanged", SoftApCapability.clazz) -> {
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onBlockedClientConnecting")) callback.onCapabilityChanged(args!![0] as Parcelable)
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) 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.App.Companion.app
import be.mygod.vpnhotspot.net.MacAddressCompat import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.util.callSuper import be.mygod.vpnhotspot.util.callSuper
import be.mygod.vpnhotspot.util.matchesCompat
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import timber.log.Timber
import java.lang.reflect.InvocationHandler import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method import java.lang.reflect.Method
import java.lang.reflect.Proxy import java.lang.reflect.Proxy
@@ -96,12 +96,11 @@ object WifiP2pManagerHelper {
return result.future.await() return result.future.await()
} }
private val interfacePersistentGroupInfoListener by lazy @SuppressLint("PrivateApi") { private val interfacePersistentGroupInfoListener by lazy {
Class.forName("android.net.wifi.p2p.WifiP2pManager\$PersistentGroupInfoListener") Class.forName("android.net.wifi.p2p.WifiP2pManager\$PersistentGroupInfoListener")
} }
private val getGroupList by lazy @SuppressLint("PrivateApi") { private val classWifiP2pGroupList by lazy { Class.forName("android.net.wifi.p2p.WifiP2pGroupList") }
Class.forName("android.net.wifi.p2p.WifiP2pGroupList").getDeclaredMethod("getGroupList") private val getGroupList by lazy { classWifiP2pGroupList.getDeclaredMethod("getGroupList") }
}
private val requestPersistentGroupInfo by lazy { private val requestPersistentGroupInfo by lazy {
WifiP2pManager::class.java.getDeclaredMethod("requestPersistentGroupInfo", WifiP2pManager::class.java.getDeclaredMethod("requestPersistentGroupInfo",
WifiP2pManager.Channel::class.java, interfacePersistentGroupInfoListener) WifiP2pManager.Channel::class.java, interfacePersistentGroupInfoListener)
@@ -112,15 +111,13 @@ object WifiP2pManagerHelper {
* Requires one of NETWORK_SETTING, NETWORK_STACK, or READ_WIFI_CREDENTIAL permission since API 30. * 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 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> { suspend fun WifiP2pManager.requestPersistentGroupInfo(c: WifiP2pManager.Channel): Collection<WifiP2pGroup> {
val result = CompletableDeferred<Collection<WifiP2pGroup>>() val result = CompletableDeferred<Collection<WifiP2pGroup>>()
requestPersistentGroupInfo(this, c, Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader, requestPersistentGroupInfo(this, c, Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader,
arrayOf(interfacePersistentGroupInfoListener), object : InvocationHandler { arrayOf(interfacePersistentGroupInfoListener), object : InvocationHandler {
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? = when (method.name) { override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? = when {
"onPersistentGroupInfoAvailable" -> { method.matchesCompat("onPersistentGroupInfoAvailable", args, classWifiP2pGroupList) -> {
if (args?.size != 1) Timber.w(IllegalArgumentException("Unexpected args: $args"))
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
result.complete(getGroupList(args!![0]) as Collection<WifiP2pGroup>) result.complete(getGroupList(args!![0]) as Collection<WifiP2pGroup>)
} }

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,18 @@
package be.mygod.vpnhotspot.root 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 android.os.Parcelable
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import be.mygod.librootkotlinx.ParcelableBoolean import be.mygod.librootkotlinx.ParcelableBoolean
import be.mygod.librootkotlinx.RootCommand import be.mygod.librootkotlinx.RootCommand
import be.mygod.librootkotlinx.RootCommandChannel import be.mygod.librootkotlinx.RootCommandChannel
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
import be.mygod.vpnhotspot.net.wifi.WifiApManager import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.net.wifi.WifiClient
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.* import kotlinx.coroutines.channels.*
@@ -26,32 +31,29 @@ object WifiApCommands {
} }
@Parcelize @Parcelize
data class OnNumClientsChanged(val numClients: Int) : SoftApCallbackParcel() { data class OnNumClientsChanged(val numClients: Int) : SoftApCallbackParcel() {
@Suppress("DEPRECATION")
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) = override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
callback.onNumClientsChanged(numClients) callback.onNumClientsChanged(numClients)
} }
@Parcelize @Parcelize
@RequiresApi(30) @RequiresApi(30)
data class OnConnectedClientsChanged(val clients: List<MacAddress>) : SoftApCallbackParcel() { data class OnConnectedClientsChanged(val clients: List<Parcelable>) : SoftApCallbackParcel() {
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) = override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
callback.onConnectedClientsChanged(clients) callback.onConnectedClientsChanged(clients)
} }
@Parcelize @Parcelize
@RequiresApi(30) @RequiresApi(30)
data class OnInfoChanged(val frequency: Int, val bandwidth: Int) : SoftApCallbackParcel() { data class OnInfoChanged(val info: List<Parcelable>) : SoftApCallbackParcel() {
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) = override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) = callback.onInfoChanged(info)
callback.onInfoChanged(frequency, bandwidth)
} }
@Parcelize @Parcelize
@RequiresApi(30) @RequiresApi(30)
data class OnCapabilityChanged(val maxSupportedClients: Int, data class OnCapabilityChanged(val capability: Parcelable) : SoftApCallbackParcel() {
val supportedFeatures: Long) : SoftApCallbackParcel() {
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) = override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
callback.onCapabilityChanged(maxSupportedClients, supportedFeatures) callback.onCapabilityChanged(capability)
} }
@Parcelize @Parcelize
@RequiresApi(30) @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) = override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
callback.onBlockedClientConnecting(client, blockedReason) callback.onBlockedClientConnecting(client, blockedReason)
} }
@@ -60,7 +62,7 @@ object WifiApCommands {
@Parcelize @Parcelize
@RequiresApi(28) @RequiresApi(28)
class RegisterSoftApCallback : RootCommandChannel<SoftApCallbackParcel> { 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 finish = CompletableDeferred<Unit>()
val key = WifiApManager.registerSoftApCallback(object : WifiApManager.SoftApCallbackCompat { val key = WifiApManager.registerSoftApCallback(object : WifiApManager.SoftApCallbackCompat {
private fun push(parcel: SoftApCallbackParcel) { private fun push(parcel: SoftApCallbackParcel) {
@@ -71,20 +73,18 @@ object WifiApCommands {
override fun onStateChanged(state: Int, failureReason: Int) = override fun onStateChanged(state: Int, failureReason: Int) =
push(SoftApCallbackParcel.OnStateChanged(state, failureReason)) push(SoftApCallbackParcel.OnStateChanged(state, failureReason))
@Suppress("OverridingDeprecatedMember")
override fun onNumClientsChanged(numClients: Int) = override fun onNumClientsChanged(numClients: Int) =
push(SoftApCallbackParcel.OnNumClientsChanged(numClients)) push(SoftApCallbackParcel.OnNumClientsChanged(numClients))
@RequiresApi(30) @RequiresApi(30)
override fun onConnectedClientsChanged(clients: List<MacAddress>) = override fun onConnectedClientsChanged(clients: List<Parcelable>) =
push(SoftApCallbackParcel.OnConnectedClientsChanged(clients)) push(SoftApCallbackParcel.OnConnectedClientsChanged(clients))
@RequiresApi(30) @RequiresApi(30)
override fun onInfoChanged(frequency: Int, bandwidth: Int) = override fun onInfoChanged(info: List<Parcelable>) = push(SoftApCallbackParcel.OnInfoChanged(info))
push(SoftApCallbackParcel.OnInfoChanged(frequency, bandwidth))
@RequiresApi(30) @RequiresApi(30)
override fun onCapabilityChanged(maxSupportedClients: Int, supportedFeatures: Long) = override fun onCapabilityChanged(capability: Parcelable) =
push(SoftApCallbackParcel.OnCapabilityChanged(maxSupportedClients, supportedFeatures)) push(SoftApCallbackParcel.OnCapabilityChanged(capability))
@RequiresApi(30) @RequiresApi(30)
override fun onBlockedClientConnecting(client: MacAddress, blockedReason: Int) = override fun onBlockedClientConnecting(client: Parcelable, blockedReason: Int) =
push(SoftApCallbackParcel.OnBlockedClientConnecting(client, blockedReason)) push(SoftApCallbackParcel.OnBlockedClientConnecting(client, blockedReason))
}) { }) {
scope.launch { scope.launch {
@@ -125,8 +125,21 @@ object WifiApCommands {
} }
is SoftApCallbackParcel.OnInfoChanged -> synchronized(callbacks) { lastCallback.info = parcel } is SoftApCallbackParcel.OnInfoChanged -> synchronized(callbacks) { lastCallback.info = parcel }
is SoftApCallbackParcel.OnCapabilityChanged -> synchronized(callbacks) { lastCallback.capability = parcel } is SoftApCallbackParcel.OnCapabilityChanged -> synchronized(callbacks) { lastCallback.capability = parcel }
is SoftApCallbackParcel.OnBlockedClientConnecting -> @TargetApi(30) { // passively consume events
val client = WifiClient(parcel.client)
val macAddress = client.macAddress
var name = macAddress.toString()
if (Build.VERSION.SDK_INT >= 31) client.apInstanceIdentifier?.let { name += "%$it" }
val reason = WifiApManager.clientBlockLookup(parcel.blockedReason, true)
Timber.i("$name blocked from connecting: $reason (${parcel.blockedReason})")
SmartSnackbar.make(app.getString(R.string.tethering_manage_wifi_client_blocked, name, reason)).apply {
action(R.string.tethering_manage_wifi_copy_mac) {
app.clipboard.setPrimaryClip(ClipData.newPlainText(null, macAddress.toString()))
}
}.show()
}
} }
for (callback in synchronized(callbacks) { callbacks }) parcel.dispatch(callback) for (callback in synchronized(callbacks) { callbacks.toList() }) parcel.dispatch(callback)
} }
@RequiresApi(28) @RequiresApi(28)
fun registerSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) { fun registerSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) {
@@ -142,8 +155,8 @@ object WifiApCommands {
SmartSnackbar.make(e).show() SmartSnackbar.make(e).show()
} }
} }
lastCallback null
} else null } else lastCallback
}?.toSequence()?.forEach { it?.dispatch(callback) } }?.toSequence()?.forEach { it?.dispatch(callback) }
@RequiresApi(28) @RequiresApi(28)
fun unregisterSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) { 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 be.mygod.vpnhotspot.R
import timber.log.Timber import timber.log.Timber
class ConstantLookup(private val prefix: String, private val lookup29: Array<out String>, class ConstantLookup(private val prefix: String, private val lookup29: Array<out String?>,
private val clazz: () -> Class<*>) { private val clazz: () -> Class<*>) {
private val lookup by lazy { private val lookup by lazy {
SparseArrayCompat<String>().apply { SparseArrayCompat<String>().apply {
for (field in clazz().declaredFields) try { for (field in clazz().declaredFields) try {
if (field.name.startsWith(prefix)) put(field.getInt(null), field.name) if (field.type == Int::class.java && field.name.startsWith(prefix)) put(field.getInt(null), field.name)
} catch (e: Exception) { } catch (e: Exception) {
Timber.w(e) Timber.w(e)
} }
@@ -25,22 +25,22 @@ class ConstantLookup(private val prefix: String, private val lookup29: Array<out
} catch (e: ReflectiveOperationException) { } catch (e: ReflectiveOperationException) {
Timber.w(e) 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) ?: app.getString(R.string.failure_reason_unknown, reason)
} }
} }
@Suppress("FunctionName") @Suppress("FunctionName")
fun ConstantLookup(prefix: String, vararg lookup29: String, clazz: () -> Class<*>) = fun ConstantLookup(prefix: String, vararg lookup29: String?, clazz: () -> Class<*>) =
ConstantLookup(prefix, lookup29, clazz) ConstantLookup(prefix, lookup29, clazz)
@Suppress("FunctionName") @Suppress("FunctionName")
inline fun <reified T> ConstantLookup(prefix: String, vararg lookup29: String) = inline fun <reified T> ConstantLookup(prefix: String, vararg lookup29: String?) =
ConstantLookup(prefix, lookup29) { T::class.java } ConstantLookup(prefix, lookup29) { T::class.java }
class LongConstantLookup(private val clazz: Class<*>, private val prefix: String) { class LongConstantLookup(private val clazz: Class<*>, private val prefix: String) {
private val lookup = LongSparseArray<String>().apply { private val lookup = LongSparseArray<String>().apply {
for (field in clazz.declaredFields) try { for (field in clazz.declaredFields) try {
if (field.name.startsWith(prefix)) put(field.getLong(null), field.name) if (field.type == Long::class.java && field.name.startsWith(prefix)) put(field.getLong(null), field.name)
} catch (e: Exception) { } catch (e: Exception) {
Timber.w(e) Timber.w(e)
} }

View File

@@ -1,15 +1,12 @@
package be.mygod.vpnhotspot.util package be.mygod.vpnhotspot.util
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import android.net.wifi.p2p.WifiP2pManager import android.net.wifi.p2p.WifiP2pManager
import android.util.Log
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import timber.log.Timber import timber.log.Timber
@SuppressLint("LogNotTimber")
object Services { object Services {
private lateinit var contextInit: () -> Context private lateinit var contextInit: () -> Context
val context by lazy { contextInit() } val context by lazy { contextInit() }
@@ -22,7 +19,7 @@ object Services {
try { try {
context.getSystemService<WifiP2pManager>() context.getSystemService<WifiP2pManager>()
} catch (e: RuntimeException) { } catch (e: RuntimeException) {
if (android.os.Process.myUid() == 0) Log.w("WifiP2pManager", e) else Timber.w(e) Timber.w(e)
null 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.annotation.TargetApi
import android.content.* import android.content.*
import android.content.res.Resources import android.content.res.Resources
import android.net.InetAddresses import android.net.*
import android.net.LinkProperties
import android.net.RouteInfo
import android.os.Build import android.os.Build
import android.os.RemoteException import android.os.RemoteException
import android.system.ErrnoException import android.system.ErrnoException
import android.system.Os import android.system.Os
import android.system.OsConstants import android.system.OsConstants
import android.text.Spannable import android.text.*
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
@@ -39,6 +35,7 @@ import java.lang.reflect.Method
import java.net.InetAddress import java.net.InetAddress
import java.net.NetworkInterface import java.net.NetworkInterface
import java.net.SocketException import java.net.SocketException
import java.util.*
tailrec fun Throwable.getRootCause(): Throwable { tailrec fun Throwable.getRootCause(): Throwable {
if (this is InvocationTargetException || this is RemoteException) return (cause ?: return this).getRootCause() if (this is InvocationTargetException || this is RemoteException) return (cause ?: return this).getRootCause()
@@ -56,6 +53,19 @@ fun Long.toPluralInt(): Int {
return (this % 1000000000).toInt() + 1000000000 return (this % 1000000000).toInt() + 1000000000
} }
@RequiresApi(26)
fun Method.matches(name: String, vararg classes: Class<*>) = this.name == name && parameterCount == classes.size &&
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) { fun Context.ensureReceiverUnregistered(receiver: BroadcastReceiver) {
try { try {
unregisterReceiver(receiver) unregisterReceiver(receiver)
@@ -80,6 +90,61 @@ fun setVisibility(view: View, value: Boolean) {
view.isVisible = value 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 { fun makeIpSpan(ip: InetAddress) = ip.hostAddress.let {
// exclude all bogon IP addresses supported by Android APIs // exclude all bogon IP addresses supported by Android APIs
if (!app.hasTouch || ip.isMulticastAddress || ip.isAnyLocalAddress || ip.isLoopbackAddress || 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") @Suppress("FunctionName")
fun if_nametoindex(ifname: String) = if (Build.VERSION.SDK_INT >= 26) { fun if_nametoindex(ifname: String) = if (Build.VERSION.SDK_INT >= 26) {
Os.if_nametoindex(ifname) Os.if_nametoindex(ifname)

View File

@@ -37,6 +37,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:descendantFocusability="beforeDescendants" android:descendantFocusability="beforeDescendants"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
style="@style/wifi_item"> style="@style/wifi_item">
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/ssid_wrapper" android:id="@+id/ssid_wrapper"
@@ -119,103 +121,236 @@
android:inputType="number" android:inputType="number"
android:maxLength="19" /> android:maxLength="19" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<LinearLayout <LinearLayout
android:id="@+id/band_wrapper" android:id="@+id/band_group"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dip"
android:orientation="vertical"> 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 <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
style="@style/wifi_item_label" style="@style/wifi_item_subhead"
android:text="@string/wifi_hotspot_ap_band_title" /> 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 <Spinner
android:id="@+id/band" android:id="@+id/band_2G"
style="@style/wifi_item_content" style="@style/wifi_item_content"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="@dimen/touch_target_min" android:minHeight="@dimen/touch_target_min"
android:prompt="@string/wifi_hotspot_ap_band_title" /> 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"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_label"
android:text="@string/wifi_ap_choose_6G" />
<Spinner
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_ap_choose_6G" />
</LinearLayout>
<LinearLayout
android:id="@+id/band_wrapper_60G"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
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/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_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>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/bssid_wrapper" <LinearLayout
android:id="@+id/access_control_group"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dip" android:orientation="vertical">
app:counterEnabled="true" <com.google.android.material.divider.MaterialDivider
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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
style="@style/wifi_item_edit_content" style="@style/wifi_item_divider" />
android:inputType="textNoSuggestions" <TextView
android:maxLength="17" />
</com.google.android.material.textfield.TextInputLayout>
<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" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/max_client_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
app:counterEnabled="true"
app:counterMaxLength="10"
app:errorEnabled="true"
android:hint="@string/wifi_max_clients">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/max_client"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
style="@style/wifi_item_edit_content" style="@style/wifi_item_subhead"
android:inputType="number" android:text="@string/wifi_hotspot_access_control_title" />
android:maxLength="10" /> <com.google.android.material.textfield.TextInputLayout
</com.google.android.material.textfield.TextInputLayout> android:id="@+id/max_client_wrapper"
<Switch
android:id="@+id/client_user_control"
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_client_user_control" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/blocked_list_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
android:hint="@string/wifi_blocked_list"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/blocked_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
style="@style/wifi_item_edit_content" android:layout_marginTop="8dip"
android:inputType="textMultiLine|textNoSuggestions" /> app:counterEnabled="true"
</com.google.android.material.textfield.TextInputLayout> app:counterMaxLength="10"
<com.google.android.material.textfield.TextInputLayout app:errorEnabled="true"
android:id="@+id/allowed_list_wrapper" android:hint="@string/wifi_max_clients">
android:layout_width="match_parent" <com.google.android.material.textfield.TextInputEditText
android:layout_height="wrap_content" android:id="@+id/max_client"
android:layout_marginTop="8dip" android:layout_width="match_parent"
android:hint="@string/wifi_allowed_list" android:layout_height="wrap_content"
app:errorEnabled="true"> style="@style/wifi_item_edit_content"
<com.google.android.material.textfield.TextInputEditText android:inputType="number"
android:id="@+id/allowed_list" android:maxLength="10" />
</com.google.android.material.textfield.TextInputLayout>
<Switch
android:id="@+id/client_user_control"
style="@style/wifi_item_label"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
style="@style/wifi_item_edit_content" android:layout_marginTop="8dip"
android:inputType="textMultiLine|textNoSuggestions" /> android:minHeight="@dimen/touch_target_min"
</com.google.android.material.textfield.TextInputLayout> android:text="@string/wifi_client_user_control" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/blocked_list_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
android:hint="@string/wifi_blocked_list"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/blocked_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_edit_content"
android:inputType="textMultiLine|textNoSuggestions" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/allowed_list_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
android:hint="@string/wifi_allowed_list"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/allowed_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_edit_content"
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> </LinearLayout>
</ScrollView> </ScrollView>
</LinearLayout> </LinearLayout>

View File

@@ -61,7 +61,9 @@
<string name="wifi_hotspot_ap_band_title" msgid="1165801173359290681">"Диапазон частот Wi-Fi"</string> <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_auto" msgid="2677800651271769965">"Авто"</string>
<string name="wifi_ap_choose_2G" msgid="8724267386885036210">"2,4 ГГц"</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_advanced_mac_address_title" msgid="6571335466330978393">"MAC-адрес"</string>
<string name="wifi_hidden_network" msgid="973162091800925000">"Скрытая сеть"</string> <string name="wifi_hidden_network" msgid="973162091800925000">"Скрытая сеть"</string>
<string name="wifi_save" msgid="3331121567988522826">"Сохранить"</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_ncm">USB 网络共享 (NCM)</string>
<string name="tethering_manage_wigig">WiGig 热点</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">%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"> <plurals name="tethering_manage_wifi_capabilities">
<item quantity="other">已连接 %1$s/%2$d 个设备\n支持功能%3$s</item> <item quantity="other">已连接 %1$s/%2$d 个设备\n支持功能%3$s</item>
</plurals> </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_no_features"></string>
<string name="tethering_manage_wifi_client_blocked">已屏蔽 %1$s%2$s</string> <string name="tethering_manage_wifi_client_blocked">已屏蔽 %1$s%2$s</string>
<string name="tethering_manage_wifi_copy_mac">复制 MAC</string> <string name="tethering_manage_wifi_copy_mac">复制 MAC</string>
@@ -140,6 +152,7 @@
<string name="notification_tethering_title">VPN 共享已启用</string> <string name="notification_tethering_title">VPN 共享已启用</string>
<string name="notification_channel_tethering">VPN 共享服务</string> <string name="notification_channel_tethering">VPN 共享服务</string>
<string name="notification_channel_monitor">监视不活跃接口</string>
<plurals name="notification_connected_devices"> <plurals name="notification_connected_devices">
<item quantity="other">%d 个设备已连接到 %s</item> <item quantity="other">%d 个设备已连接到 %s</item>
</plurals> </plurals>
@@ -166,16 +179,25 @@
<string name="wifi_hotspot_timeout">关闭延迟</string> <string name="wifi_hotspot_timeout">关闭延迟</string>
<string name="wifi_hotspot_timeout_default">默认延迟:%d 毫秒</string> <string name="wifi_hotspot_timeout_default">默认延迟:%d 毫秒</string>
<string name="wifi_hotspot_ap_band_title" msgid="1165801173359290681">"AP 频段"</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_auto" msgid="2677800651271769965">"自动"</string>
<string name="wifi_ap_choose_2G" msgid="8724267386885036210">"2.4 GHz 频段"</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_5G" msgid="8813128641914385634">"5 GHz 频段"</string>
<string name="wifi_ap_choose_6G">6.0 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_advanced_mac_address_title" msgid="6571335466330978393">"MAC 地址"</string>
<string name="wifi_hidden_network" msgid="973162091800925000">"隐藏的网络"</string> <string name="wifi_hidden_network" msgid="973162091800925000">"隐藏的网络"</string>
<string name="wifi_max_clients">允许连接设备数上限</string> <string name="wifi_max_clients">允许连接设备数上限</string>
<string name="wifi_client_user_control">过滤可以连接的设备</string> <string name="wifi_client_user_control">过滤可以连接的设备</string>
<string name="wifi_blocked_list">设备黑名单</string> <string name="wifi_blocked_list">设备黑名单</string>
<string name="wifi_allowed_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> <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 --> <!-- 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"> <plurals name="tethering_manage_wifi_capabilities">
<item quantity="other">已連接 %1$s/%2$d 個設備\n支持功能%3$s</item> <item quantity="other">已連接 %1$s/%2$d 個設備\n支持功能%3$s</item>
</plurals> </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_no_features"></string>
<string name="tethering_manage_wifi_client_blocked">已隱藏 %1$s%2$s</string> <string name="tethering_manage_wifi_client_blocked">已隱藏 %1$s%2$s</string>
<string name="tethering_manage_wifi_copy_mac">複製 MAC</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">關閉延遲時間</string>
<string name="wifi_hotspot_timeout_default">默認延遲:%d 毫秒</string> <string name="wifi_hotspot_timeout_default">默認延遲:%d 毫秒</string>
<string name="wifi_hotspot_ap_band_title" msgid="1165801173359290681">AP 頻帶</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_auto" msgid="2677800651271769965">自動</string>
<string name="wifi_ap_choose_2G" msgid="8724267386885036210">2.4 GHz 頻帶</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_5G" msgid="8813128641914385634">5 GHz 頻帶</string>
<string name="wifi_ap_choose_6G">6.0 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_advanced_mac_address_title" msgid="6571335466330978393">"MAC 地址"</string>
<string name="wifi_hidden_network" msgid="973162091800925000">"隱藏的網路"</string> <string name="wifi_hidden_network" msgid="973162091800925000">"隱藏的網路"</string>
<string name="wifi_max_clients">允許的連接裝置數量</string> <string name="wifi_max_clients">允許的連接裝置數量</string>
<string name="wifi_client_user_control">過濾可以連接的裝置</string> <string name="wifi_client_user_control">過濾可以連接的裝置</string>
<string name="wifi_blocked_list">裝置黑名單</string> <string name="wifi_blocked_list">裝置黑名單</string>
<string name="wifi_allowed_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> <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 --> <!-- 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_ncm">USB tethering (NCM)</string>
<string name="tethering_manage_wigig">WiGig hotspot</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">%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"> <plurals name="tethering_manage_wifi_capabilities">
<item quantity="one">%1$s/%2$d client connected\nSupported features: %3$s</item> <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> <item quantity="other">%1$s/%2$d clients connected\nSupported features: %3$s</item>
</plurals> </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_no_features">None</string>
<string name="tethering_manage_wifi_client_blocked">Blocked %1$s: %2$s</string> <string name="tethering_manage_wifi_client_blocked">Blocked %1$s: %2$s</string>
<string name="tethering_manage_wifi_copy_mac">Copy MAC</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_restart_required">Restart this app to apply this setting.</string>
<string name="settings_exit_app">Exit</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_tethering">VPN Tethering Service</string>
<string name="notification_channel_monitor">Monitor Inactive Interfaces</string>
<plurals name="notification_connected_devices"> <plurals name="notification_connected_devices">
<item quantity="one">%d device connected to %s</item> <item quantity="one">%d device connected to %s</item>
<item quantity="other">%d devices 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">Inactive timeout</string>
<string name="wifi_hotspot_timeout_default">Default timeout: %dms</string> <string name="wifi_hotspot_timeout_default">Default timeout: %dms</string>
<string name="wifi_hotspot_ap_band_title">AP Band</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_auto">Auto</string>
<string name="wifi_ap_choose_2G">2.4 GHz Band</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_5G">5 GHz Band</string>
<string name="wifi_ap_choose_6G">6.0 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_advanced_mac_address_title">MAC address</string>
<string name="wifi_hidden_network">Hidden network</string> <string name="wifi_hidden_network">Hidden network</string>
<string name="wifi_max_clients">Maximum number of clients</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_client_user_control">Control which client can use hotspot</string>
<string name="wifi_blocked_list">Blocked list of clients</string> <string name="wifi_blocked_list">Blocked list of clients</string>
<string name="wifi_allowed_list">Allowed 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> <string name="wifi_save">Save</string>
<!-- Based on: https://github.com/PrivacyApps/donations/blob/747d36a18433c7e9329691054122a8ad337a62d2/Donations/src/main/res/values/donations__strings.xml --> <!-- 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:layout_marginStart">4dip</item>
<item name="android:textSize">18sp</item> <item name="android:textSize">18sp</item>
</style> </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> </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>