diff --git a/README.md b/README.md index 3a6eaa96..82a59b9e 100644 --- a/README.md +++ b/README.md @@ -145,140 +145,180 @@ You can also use WPS to connect your 2.4GHz-only device to force the repeater to _a.k.a. things that can go wrong if this app doesn't work._ This is a list of stuff that might impact this app's functionality if unavailable. -This is only meant to be an index. You can read more in the source code. +This is only meant to be an index. +You can read more in the source code. +API restrictions are updated up to [SHA-256 checksum `156715dfa705a048926dca876d731d72604df32e8bcac055af32866b50bc2cc8`](https://dl.google.com/developers/android/sc/non-sdk/hiddenapi-flags.csv). Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded or implicitly used) -* [`Landroid/net/ConnectivityManager;->getLastTetherError(Ljava/lang/String;)I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#144306) +* (prior to API 30) `Landroid/net/ConnectivityManager;->getLastTetherError(Ljava/lang/String;)I,max-target-r` * (since API 30) `Landroid/net/ConnectivityModuleConnector;->IN_PROCESS_SUFFIX:Ljava/lang/String;` -* (since API 30) [`Landroid/net/TetheringManager$TetheringEventCallback;->onTetherableInterfaceRegexpsChanged(Landroid/net/TetheringManager$TetheringInterfaceRegexps;)V,blacklist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148899) -* (since API 30) `Landroid/net/TetheringManager;->TETHERING_WIGIG:I` -* (prior to API 30) [`Landroid/net/wifi/WifiConfiguration$KeyMgmt;->FT_PSK:I,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153923) -* (prior to API 30) [`Landroid/net/wifi/WifiConfiguration$KeyMgmt;->WPA_PSK_SHA256:I,blacklist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153936) -* (since API 23, prior to API 30) [`Landroid/net/wifi/WifiConfiguration;->AP_BAND_2GHZ:I,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154057) -* (since API 23, prior to API 30) [`Landroid/net/wifi/WifiConfiguration;->AP_BAND_5GHZ:I,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154058) -* (since API 23, prior to API 30) [`Landroid/net/wifi/WifiConfiguration;->AP_BAND_ANY:I,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154059) -* (since API 23, prior to API 30) [`Landroid/net/wifi/WifiConfiguration;->apBand:I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154101) -* (since API 23, prior to API 30) [`Landroid/net/wifi/WifiConfiguration;->apChannel:I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154102) -* (since API 28, prior to API 30) [`Landroid/net/wifi/WifiManager$SoftApCallback;->onNumClientsChanged(I)V,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#132005) -* (since API 26) [`Landroid/net/wifi/WifiManager;->cancelLocalOnlyHotspotRequest()V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154947s) +* (since API 30) `Landroid/net/TetheringManager$TetheringEventCallback;->onTetherableInterfaceRegexpsChanged(Landroid/net/TetheringManager$TetheringInterfaceRegexps;)V,blocked` +* (since API 30) `Landroid/net/TetheringManager;->TETHERING_WIGIG:I,blocked` +* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setUserConfiguration(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,blocked` +* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_TYPES:[I,blocked` +* (since API 31) `Landroid/net/wifi/SoftApInfo;->getApInstanceIdentifier()Ljava/lang/String;,blocked` +* (since API 31) `Landroid/net/wifi/WifiClient;->getApInstanceIdentifier()Ljava/lang/String;,blocked` +* (prior to API 30) `Landroid/net/wifi/WifiConfiguration$KeyMgmt;->FT_PSK:I,lo-prio,max-target-o` +* (prior to API 30) `Landroid/net/wifi/WifiConfiguration$KeyMgmt;->WPA_PSK_SHA256:I,blocked` +* (since API 23, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_2GHZ:I,lo-prio,max-target-o` +* (since API 23, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_5GHZ:I,lo-prio,max-target-o` +* (since API 28, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_ANY:I,lo-prio,max-target-o` +* (since API 23, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->apBand:I,unsupported` +* (since API 23, prior to API 30) `Landroid/net/wifi/WifiConfiguration;->apChannel:I,unsupported` +* (since API 28, prior to API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onNumClientsChanged(I)V,greylist-max-o` +* (since API 26) `Landroid/net/wifi/WifiManager;->cancelLocalOnlyHotspotRequest()V,unsupported` * (prior to API 26) `Landroid/net/wifi/WifiManager;->setWifiApEnabled(Landroid/net/wifi/WifiConfiguration;Z)Z` -* [`Landroid/net/wifi/p2p/WifiP2pConfig$Builder;->MAC_ANY_ADDRESS:Landroid/net/MacAddress;,blacklist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#157883) -* [`Landroid/net/wifi/p2p/WifiP2pManager;->startWps(Landroid/net/wifi/p2p/WifiP2pManager$Channel;Landroid/net/wifi/WpsInfo;Landroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#158332) -* (since API 28) [`Landroid/provider/Settings$Global;->SOFT_AP_TIMEOUT_ENABLED:Ljava/lang/String;,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#183735) -* (prior to API 30) [`Lcom/android/internal/R$array;->config_tether_bluetooth_regexs:I,greylist-max-q`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#272546) -* (prior to API 30) [`Lcom/android/internal/R$array;->config_tether_usb_regexs:I,greylist-max-q`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#272549) -* (prior to API 30) [`Lcom/android/internal/R$array;->config_tether_wifi_regexs:I,greylist-max-q`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#272551) -* (since API 28, prior to API 30) [`Lcom/android/internal/R$integer;->config_wifi_framework_soft_ap_timeout_delay:I,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#245007) -* [`Lcom/android/internal/R$string;->config_ethernet_iface_regex:I,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#276573) +* `Landroid/net/wifi/p2p/WifiP2pConfig$Builder;->MAC_ANY_ADDRESS:Landroid/net/MacAddress;,blocked` +* (since API 29) `Landroid/net/wifi/p2p/WifiP2pConfig$Builder;->mNetworkName:Ljava/lang/String;,blocked` +* `Landroid/net/wifi/p2p/WifiP2pManager;->startWps(Landroid/net/wifi/p2p/WifiP2pManager$Channel;Landroid/net/wifi/WpsInfo;Landroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,max-target-r` +* (since API 28, prior to API 30) `Landroid/provider/Settings$Global;->SOFT_AP_TIMEOUT_ENABLED:Ljava/lang/String;,lo-prio,max-target-o` +* (prior to API 30) `Lcom/android/internal/R$array;->config_tether_bluetooth_regexs:I,max-target-q` +* (prior to API 30) `Lcom/android/internal/R$array;->config_tether_usb_regexs:I,max-target-q` +* (prior to API 30) `Lcom/android/internal/R$array;->config_tether_wifi_regexs:I,max-target-q` +* (on API 29) `Lcom/android/internal/R$bool;->config_wifi_p2p_mac_randomization_supported:I,blacklist` +* (since API 28, prior to API 30) `Lcom/android/internal/R$integer;->config_wifi_framework_soft_ap_timeout_delay:I,greylist-max-o` +* `Lcom/android/internal/R$string;->config_ethernet_iface_regex:I,lo-prio,max-target-o` * (since API 27) `Lcom/android/server/connectivity/tethering/OffloadHardwareInterface;->DEFAULT_TETHER_OFFLOAD_DISABLED:I` * (since API 30) `Lcom/android/server/wifi/WifiContext;->ACTION_RESOURCES_APK:Ljava/lang/String;` * (since API 29) `Lcom/android/server/wifi/p2p/WifiP2pServiceImpl;->ANONYMIZED_DEVICE_ADDRESS:Ljava/lang/String;` * (since API 30) `Lcom/android/server/SystemServer;->TETHERING_CONNECTOR_CLASS:Ljava/lang/String;` -* (since API 26) [`Ljava/lang/invoke/MethodHandles$Lookup;->(Ljava/lang/Class;I)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#370415) -* (since API 26) [`Ljava/lang/invoke/MethodHandles$Lookup;->ALL_MODES:I,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#370416) -* (prior to API 29) [`Ljava/net/InetAddress;->parseNumericAddress(Ljava/lang/String;)Ljava/net/InetAddress;,core-platform-api,greylist-max-p`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#372578) +* (since API 29) `Ldalvik/system/VMRuntime;->getRuntime()Ldalvik/system/VMRuntime;,core-platform-api,unsupported` +* (since API 29) `Ldalvik/system/VMRuntime;->setHiddenApiExemptions([Ljava/lang/String;)V,blocked,core-platform-api` +* (since API 26) `Ljava/lang/invoke/MethodHandles$Lookup;->(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`
Hidden whitelisted APIs: (same catch as above, however, things in this list are less likely to be broken) -* (since API 24) [`Landroid/bluetooth/BluetoothPan;->isTetheringOn()Z,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#35264) -* (since API 24) [`Landroid/bluetooth/BluetoothProfile;->PAN:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#35361) -* (since API 30) [`Landroid/content/Context;->TETHERING_SERVICE:Ljava/lang/String;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#41038) -* (since API 24, prior to API 30) [`Landroid/net/ConnectivityManager$OnStartTetheringCallback;->()V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#144095) -* (since API 24, prior to API 30) [`Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringFailed()V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#144096) -* (since API 24, prior to API 30) [`Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringStarted()V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#144097) -* (since API 24, prior to API 30) [`Landroid/net/ConnectivityManager;->startTethering(IZLandroid/net/ConnectivityManager$OnStartTetheringCallback;Landroid/os/Handler;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#144406) -* (since API 24, prior to API 30) [`Landroid/net/ConnectivityManager;->stopTethering(I)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#144408) -* [`Landroid/net/LinkProperties;->getAllInterfaceNames()Ljava/util/List;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#146558) -* [`Landroid/net/LinkProperties;->getAllRoutes()Ljava/util/List;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#146560) -* (since API 30) [`Landroid/net/TetheringManager$StartTetheringCallback;->onTetheringFailed(I)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148881) -* (since API 30) [`Landroid/net/TetheringManager$StartTetheringCallback;->onTetheringStarted()V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148882) -* (since API 30) [`Landroid/net/TetheringManager$TetheringEventCallback;->onClientsChanged(Ljava/util/Collection;)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148896) -* (since API 30) [`Landroid/net/TetheringManager$TetheringEventCallback;->onError(Ljava/lang/String;I)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148897) -* (since API 30) [`Landroid/net/TetheringManager$TetheringEventCallback;->onOffloadStatusChanged(I)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148898) -* (since API 30) [`Landroid/net/TetheringManager$TetheringEventCallback;->onTetherableInterfacesChanged(Ljava/util/List;)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148900) -* (since API 30) [`Landroid/net/TetheringManager$TetheringEventCallback;->onTetheredInterfacesChanged(Ljava/util/List;)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148901) -* (since API 30) [`Landroid/net/TetheringManager$TetheringEventCallback;->onTetheringSupported(Z)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148902) -* (since API 30) [`Landroid/net/TetheringManager$TetheringEventCallback;->onUpstreamChanged(Landroid/net/Network;)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148903) -* (since API 30) [`Landroid/net/TetheringManager$TetheringRequest$Builder;->(I)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148913) -* (since API 30) [`Landroid/net/TetheringManager$TetheringRequest$Builder;->build()Landroid/net/TetheringManager$TetheringRequest;,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148914) -* (since API 30) [`Landroid/net/TetheringManager$TetheringRequest$Builder;->setExemptFromEntitlementCheck(Z)Landroid/net/TetheringManager$TetheringRequest$Builder;,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148916) -* (since API 30) [`Landroid/net/TetheringManager$TetheringRequest$Builder;->setShouldShowEntitlementUi(Z)Landroid/net/TetheringManager$TetheringRequest$Builder;,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148917) -* [`Landroid/net/TetheringManager;->ACTION_TETHER_STATE_CHANGED:Ljava/lang/String;,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148932) -* (since API 26) [`Landroid/net/TetheringManager;->EXTRA_ACTIVE_LOCAL_ONLY:Ljava/lang/String;,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148935) -* [`Landroid/net/TetheringManager;->EXTRA_ACTIVE_TETHER:Ljava/lang/String;,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148936) -* [`Landroid/net/TetheringManager;->EXTRA_ERRORED_TETHER:Ljava/lang/String;,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148938) -* (since API 24) [`Landroid/net/TetheringManager;->TETHERING_BLUETOOTH:I,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148940) -* (since API 30) [`Landroid/net/TetheringManager;->TETHERING_ETHERNET:I,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148941) -* (since API 30) [`Landroid/net/TetheringManager;->TETHERING_NCM:I,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148943) -* (since API 24) [`Landroid/net/TetheringManager;->TETHERING_USB:I,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148944) -* (since API 24) [`Landroid/net/TetheringManager;->TETHERING_WIFI:I,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148945) -* [`Landroid/net/TetheringManager;->TETHER_ERROR_*:I,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148947) -* (since API 30) [`Landroid/net/TetheringManager;->TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION:I,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148954) -* (since API 30) [`Landroid/net/TetheringManager;->TETHER_HARDWARE_OFFLOAD_FAILED:I,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148964) -* (since API 30) [`Landroid/net/TetheringManager;->TETHER_HARDWARE_OFFLOAD_STARTED:I,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148965) -* (since API 30) [`Landroid/net/TetheringManager;->TETHER_HARDWARE_OFFLOAD_STOPPED:I,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#148966) -* (since API 30) [`Landroid/net/TetheringManager;->registerTetheringEventCallback(Ljava/util/concurrent/Executor;Landroid/net/TetheringManager$TetheringEventCallback;)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#149003) -* (since API 30) [`Landroid/net/TetheringManager;->startTethering(Landroid/net/TetheringManager$TetheringRequest;Ljava/util/concurrent/Executor;Landroid/net/TetheringManager$StartTetheringCallback;)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#149009) -* (since API 30) [`Landroid/net/TetheringManager;->stopTethering(I)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#149011) -* (since API 30) [`Landroid/net/TetheringManager;->unregisterTetheringEventCallback(Landroid/net/TetheringManager$TetheringEventCallback;)V,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#149014) -* (since API 30) [`Landroid/net/wifi/SoftApCapability;->SOFTAP_FEATURE_*:J,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153656) -* (since API 30) [`Landroid/net/wifi/SoftApCapability;->areFeaturesSupported(J)Z,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153660) -* (since API 30) [`Landroid/net/wifi/SoftApCapability;->getMaxSupportedClients()I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153663) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->()V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153707) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->(Landroid/net/wifi/SoftApConfiguration;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153708) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->build()Landroid/net/wifi/SoftApConfiguration;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153709) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setAllowedClientList(Ljava/util/List;)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153723) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setAutoShutdownEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153724) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setBand(I)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153725) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setBlockedClientList(Ljava/util/List;)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153726) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setBssid(Landroid/net/MacAddress;)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153727) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setChannel(II)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153728) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setClientControlByUserEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153729) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setHiddenSsid(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153730) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setMaxNumberOfClients(I)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153731) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setPassphrase(Ljava/lang/String;I)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153732) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setShutdownTimeoutMillis(J)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153733) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration$Builder;->setSsid(Ljava/lang/String;)Landroid/net/wifi/SoftApConfiguration$Builder;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153734) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->BAND_2GHZ:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153738) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->BAND_5GHZ:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153739) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->BAND_6GHZ:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153740) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->BAND_ANY:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153741) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->getAllowedClientList()Ljava/util/List;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153773) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->getBand()I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153774) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->getBlockedClientList()Ljava/util/List;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153775) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->getChannel()I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153777) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->getMaxNumberOfClients()I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153778) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->getShutdownTimeoutMillis()J,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153781) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->isAutoShutdownEnabled()Z,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153784) -* (since API 30) [`Landroid/net/wifi/SoftApConfiguration;->isClientControlByUserEnabled()Z,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153787) -* (since API 30) [`Landroid/net/wifi/SoftApInfo;->CHANNEL_WIDTH_*:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153813) -* (since API 30) [`Landroid/net/wifi/SoftApInfo;->CHANNEL_WIDTH_INVALID:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153819) -* (since API 30) [`Landroid/net/wifi/SoftApInfo;->getBandwidth()I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153825) -* (since API 30) [`Landroid/net/wifi/SoftApInfo;->getFrequency()I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153826) -* (since API 30) [`Landroid/net/wifi/WifiClient;->getMacAddress()Landroid/net/MacAddress;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153881) -* (prior to API 30) [`Landroid/net/wifi/WifiConfiguration$KeyMgmt;->WPA2_PSK:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#153932) -* (since API 30) [`Landroid/net/wifi/WifiManager$SoftApCallback;->onBlockedClientConnecting(Landroid/net/wifi/WifiClient;I)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154681) -* (since API 30) [`Landroid/net/wifi/WifiManager$SoftApCallback;->onCapabilityChanged(Landroid/net/wifi/SoftApCapability;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154682) -* (since API 30) [`Landroid/net/wifi/WifiManager$SoftApCallback;->onConnectedClientsChanged(Ljava/util/List;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154683) -* (since API 30) [`Landroid/net/wifi/WifiManager$SoftApCallback;->onInfoChanged(Landroid/net/wifi/SoftApInfo;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154684) -* (since API 28) [`Landroid/net/wifi/WifiManager$SoftApCallback;->onStateChanged(II)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154685) -* (since API 30) [`Landroid/net/wifi/WifiManager;->SAP_CLIENT_BLOCK_REASON_CODE_*:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154840) -* (since API 28) [`Landroid/net/wifi/WifiManager;->SAP_START_FAILURE_*:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154843) -* (since API 28) [`Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_FAILED:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154868) -* (since API 30) [`Landroid/net/wifi/WifiManager;->getSoftApConfiguration()Landroid/net/wifi/SoftApConfiguration;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154985) -* (prior to API 30) [`Landroid/net/wifi/WifiManager;->getWifiApConfiguration()Landroid/net/wifi/WifiConfiguration;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#154989) -* (since API 28) [`Landroid/net/wifi/WifiManager;->registerSoftApCallback(Ljava/util/concurrent/Executor;Landroid/net/wifi/WifiManager$SoftApCallback;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#155042) -* (since API 30) [`Landroid/net/wifi/WifiManager;->setSoftApConfiguration(Landroid/net/wifi/SoftApConfiguration;)Z,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#155063) -* (prior to API 30) [`Landroid/net/wifi/WifiManager;->setWifiApConfiguration(Landroid/net/wifi/WifiConfiguration;)Z,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#155067) -* (since API 28) [`Landroid/net/wifi/WifiManager;->unregisterSoftApCallback(Landroid/net/wifi/WifiManager$SoftApCallback;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#155087) -* [`Landroid/net/wifi/p2p/WifiP2pGroupList;->getGroupList()Ljava/util/List;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#158079) -* [`Landroid/net/wifi/p2p/WifiP2pManager$PersistentGroupInfoListener;->onPersistentGroupInfoAvailable(Landroid/net/wifi/p2p/WifiP2pGroupList;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#158156) -* [`Landroid/net/wifi/p2p/WifiP2pManager;->deletePersistentGroup(Landroid/net/wifi/p2p/WifiP2pManager$Channel;ILandroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#158296) -* [`Landroid/net/wifi/p2p/WifiP2pManager;->requestPersistentGroupInfo(Landroid/net/wifi/p2p/WifiP2pManager$Channel;Landroid/net/wifi/p2p/WifiP2pManager$PersistentGroupInfoListener;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#158320) -* [`Landroid/net/wifi/p2p/WifiP2pManager;->setWifiP2pChannels(Landroid/net/wifi/p2p/WifiP2pManager$Channel;IILandroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#158330) -* [`Landroid/provider/Settings$Global;->TETHER_OFFLOAD_DISABLED:Ljava/lang/String;,system-api,test-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/4601d91/appcompat/hiddenapi-flags.csv#183757) +* (since API 24) `Landroid/bluetooth/BluetoothPan;->isTetheringOn()Z,sdk,system-api,test-api` +* (since API 24) `Landroid/bluetooth/BluetoothProfile;->PAN:I,sdk,system-api,test-api` +* (since API 30) `Landroid/content/Context;->TETHERING_SERVICE:Ljava/lang/String;,sdk,system-api,test-api` +* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;->()V,sdk,system-api,test-api` +* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringFailed()V,sdk,system-api,test-api` +* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringStarted()V,sdk,system-api,test-api` +* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager;->startTethering(IZLandroid/net/ConnectivityManager$OnStartTetheringCallback;Landroid/os/Handler;)V,sdk,system-api,test-api` +* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager;->stopTethering(I)V,sdk,system-api,test-api` +* `Landroid/net/LinkProperties;->getAllInterfaceNames()Ljava/util/List;,sdk,system-api,test-api` +* `Landroid/net/LinkProperties;->getAllRoutes()Ljava/util/List;,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager$StartTetheringCallback;->onTetheringFailed(I)V,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager$StartTetheringCallback;->onTetheringStarted()V,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager$TetheringEventCallback;->onClientsChanged(Ljava/util/Collection;)V,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager$TetheringEventCallback;->onError(Ljava/lang/String;I)V,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager$TetheringEventCallback;->onOffloadStatusChanged(I)V,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager$TetheringEventCallback;->onTetherableInterfacesChanged(Ljava/util/List;)V,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager$TetheringEventCallback;->onTetheredInterfacesChanged(Ljava/util/List;)V,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager$TetheringEventCallback;->onTetheringSupported(Z)V,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager$TetheringEventCallback;->onUpstreamChanged(Landroid/net/Network;)V,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager$TetheringRequest$Builder;->(I)V,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager$TetheringRequest$Builder;->build()Landroid/net/TetheringManager$TetheringRequest;,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager$TetheringRequest$Builder;->setExemptFromEntitlementCheck(Z)Landroid/net/TetheringManager$TetheringRequest$Builder;,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager$TetheringRequest$Builder;->setShouldShowEntitlementUi(Z)Landroid/net/TetheringManager$TetheringRequest$Builder;,sdk,system-api,test-api` +* `Landroid/net/TetheringManager;->ACTION_TETHER_STATE_CHANGED:Ljava/lang/String;,sdk,system-api,test-api` +* (since API 26) `Landroid/net/TetheringManager;->EXTRA_ACTIVE_LOCAL_ONLY:Ljava/lang/String;,sdk,system-api,test-api` +* `Landroid/net/TetheringManager;->EXTRA_ACTIVE_TETHER:Ljava/lang/String;,sdk,system-api,test-api` +* `Landroid/net/TetheringManager;->EXTRA_ERRORED_TETHER:Ljava/lang/String;,sdk,system-api,test-api` +* (since API 24) `Landroid/net/TetheringManager;->TETHERING_BLUETOOTH:I,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager;->TETHERING_ETHERNET:I,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager;->TETHERING_NCM:I,sdk,system-api,test-api` +* (since API 24) `Landroid/net/TetheringManager;->TETHERING_USB:I,sdk,system-api,test-api` +* (since API 24) `Landroid/net/TetheringManager;->TETHERING_WIFI:I,sdk,system-api,test-api` +* `Landroid/net/TetheringManager;->TETHER_ERROR_*:I,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager;->TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION:I,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager;->TETHER_HARDWARE_OFFLOAD_FAILED:I,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager;->TETHER_HARDWARE_OFFLOAD_STARTED:I,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager;->TETHER_HARDWARE_OFFLOAD_STOPPED:I,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager;->registerTetheringEventCallback(Ljava/util/concurrent/Executor;Landroid/net/TetheringManager$TetheringEventCallback;)V,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager;->startTethering(Landroid/net/TetheringManager$TetheringRequest;Ljava/util/concurrent/Executor;Landroid/net/TetheringManager$StartTetheringCallback;)V,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager;->stopTethering(I)V,sdk,system-api,test-api` +* (since API 30) `Landroid/net/TetheringManager;->unregisterTetheringEventCallback(Landroid/net/TetheringManager$TetheringEventCallback;)V,sdk,system-api,test-api` +* (since API 31) `Landroid/net/wifi/SoftApCapability;->SOFTAP_FEATURE_BAND_24G_SUPPORTED:J,sdk,system-api,test-api` +* (since API 31) `Landroid/net/wifi/SoftApCapability;->SOFTAP_FEATURE_BAND_5G_SUPPORTED:J,sdk,system-api,test-api` +* (since API 31) `Landroid/net/wifi/SoftApCapability;->SOFTAP_FEATURE_BAND_60G_SUPPORTED:J,sdk,system-api,test-api` +* (since API 31) `Landroid/net/wifi/SoftApCapability;->SOFTAP_FEATURE_BAND_6G_SUPPORTED:J,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApCapability;->SOFTAP_FEATURE_*:J,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApCapability;->areFeaturesSupported(J)Z,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApCapability;->getMaxSupportedClients()I,sdk,system-api,test-api` +* (since API 31) `Landroid/net/wifi/SoftApCapability;->getSupportedChannelList(I)[I,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->()V,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->(Landroid/net/wifi/SoftApConfiguration;)V,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->build()Landroid/net/wifi/SoftApConfiguration;,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setAllowedClientList(Ljava/util/List;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setAutoShutdownEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` +* (on API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBand(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBlockedClientList(Ljava/util/List;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` +* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBridgedModeOpportunisticShutdownEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setBssid(Landroid/net/MacAddress;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` +* (on API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setChannel(II)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` +* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setChannels(Landroid/util/SparseIntArray;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setClientControlByUserEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setHiddenSsid(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` +* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setIeee80211axEnabled(Z)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` +* (since API 31) `Landroid/net/wifi/SoftApConfiguration$Builder;->setMacRandomizationSetting(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setMaxNumberOfClients(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setPassphrase(Ljava/lang/String;I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setShutdownTimeoutMillis(J)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setSsid(Ljava/lang/String;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->BAND_2GHZ:I,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->BAND_5GHZ:I,sdk,system-api,test-api` +* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_60GHZ:I,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->BAND_6GHZ:I,sdk,system-api,test-api` +* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_*:I,sdk,system-api,test-api` +* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->RANDOMIZATION_NONE:I,sdk,system-api,test-api` +* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->RANDOMIZATION_PERSISTENT:I,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getAllowedClientList()Ljava/util/List;,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getBand()I,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getBlockedClientList()Ljava/util/List;,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getChannel()I,sdk,system-api,test-api` +* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->getChannels()Landroid/util/SparseIntArray;,sdk,system-api,test-api` +* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->getMacRandomizationSetting()I,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getMaxNumberOfClients()I,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->getShutdownTimeoutMillis()J,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->isAutoShutdownEnabled()Z,sdk,system-api,test-api` +* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->isBridgedModeOpportunisticShutdownEnabled()Z,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->isClientControlByUserEnabled()Z,sdk,system-api,test-api` +* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->isIeee80211axEnabled()Z,sdk,system-api,test-api` +* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->isUserConfiguration()Z,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApInfo;->CHANNEL_WIDTH_*:I,sdk,system-api,test-api` +* (on API 30) `Landroid/net/wifi/SoftApInfo;->CHANNEL_WIDTH_INVALID:I,sdk,system-api,test-api` +* (since API 31) `Landroid/net/wifi/SoftApInfo;->getAutoShutdownTimeoutMillis()J,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApInfo;->getBandwidth()I,sdk,system-api,test-api` +* (since API 31) `Landroid/net/wifi/SoftApInfo;->getBssid()Landroid/net/MacAddress;,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/SoftApInfo;->getFrequency()I,sdk,system-api,test-api` +* (since API 31) `Landroid/net/wifi/SoftApInfo;->getWifiStandard()I,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/WifiClient;->getMacAddress()Landroid/net/MacAddress;,sdk,system-api,test-api` +* (prior to API 30) `Landroid/net/wifi/WifiConfiguration$KeyMgmt;->WPA2_PSK:I,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onBlockedClientConnecting(Landroid/net/wifi/WifiClient;I)V,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onCapabilityChanged(Landroid/net/wifi/SoftApCapability;)V,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onConnectedClientsChanged(Ljava/util/List;)V,sdk,system-api,test-api` +* (on API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onInfoChanged(Landroid/net/wifi/SoftApInfo;)V,sdk,system-api,test-api` +* (since API 31) `Landroid/net/wifi/WifiManager$SoftApCallback;->onInfoChanged(Ljava/util/List;)V,sdk,system-api,test-api` +* (since API 28) `Landroid/net/wifi/WifiManager$SoftApCallback;->onStateChanged(II)V,sdk,system-api,test-api` +* (since API 23) `Landroid/net/wifi/WifiManager;->EXTRA_WIFI_AP_FAILURE_REASON:Ljava/lang/String;,sdk,system-api,test-api` +* (since API 26) `Landroid/net/wifi/WifiManager;->EXTRA_WIFI_AP_INTERFACE_NAME:Ljava/lang/String;,sdk,system-api,test-api` +* (since API 23) `Landroid/net/wifi/WifiManager;->EXTRA_WIFI_AP_STATE:Ljava/lang/String;,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/WifiManager;->SAP_CLIENT_BLOCK_REASON_CODE_*:I,sdk,system-api,test-api` +* (since API 23) `Landroid/net/wifi/WifiManager;->SAP_START_FAILURE_*:I,sdk,system-api,test-api` +* (since API 23) `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_CHANGED_ACTION:Ljava/lang/String;,sdk,system-api,test-api` +* (since API 23) `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_DISABLED:I,sdk,system-api,test-api` +* (since API 23) `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_DISABLING:I,sdk,system-api,test-api` +* (since API 23) `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_ENABLED:I,sdk,system-api,test-api` +* (since API 23) `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_ENABLING:I,sdk,system-api,test-api` +* (since API 23) `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_FAILED:I,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/WifiManager;->getSoftApConfiguration()Landroid/net/wifi/SoftApConfiguration;,sdk,system-api,test-api` +* (prior to API 30) `Landroid/net/wifi/WifiManager;->getWifiApConfiguration()Landroid/net/wifi/WifiConfiguration;,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/WifiManager;->isApMacRandomizationSupported()Z,sdk,system-api,test-api` +* (since API 28) `Landroid/net/wifi/WifiManager;->registerSoftApCallback(Ljava/util/concurrent/Executor;Landroid/net/wifi/WifiManager$SoftApCallback;)V,sdk,system-api,test-api` +* (since API 30) `Landroid/net/wifi/WifiManager;->setSoftApConfiguration(Landroid/net/wifi/SoftApConfiguration;)Z,sdk,system-api,test-api` +* (prior to API 30) `Landroid/net/wifi/WifiManager;->setWifiApConfiguration(Landroid/net/wifi/WifiConfiguration;)Z,sdk,system-api,test-api` +* (since API 28) `Landroid/net/wifi/WifiManager;->unregisterSoftApCallback(Landroid/net/wifi/WifiManager$SoftApCallback;)V,sdk,system-api,test-api` +* `Landroid/net/wifi/p2p/WifiP2pGroupList;->getGroupList()Ljava/util/List;,sdk,system-api,test-api` +* `Landroid/net/wifi/p2p/WifiP2pManager$PersistentGroupInfoListener;->onPersistentGroupInfoAvailable(Landroid/net/wifi/p2p/WifiP2pGroupList;)V,sdk,system-api,test-api` +* `Landroid/net/wifi/p2p/WifiP2pManager;->deletePersistentGroup(Landroid/net/wifi/p2p/WifiP2pManager$Channel;ILandroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,sdk,system-api,test-api` +* `Landroid/net/wifi/p2p/WifiP2pManager;->requestPersistentGroupInfo(Landroid/net/wifi/p2p/WifiP2pManager$Channel;Landroid/net/wifi/p2p/WifiP2pManager$PersistentGroupInfoListener;)V,sdk,system-api,test-api` +* `Landroid/net/wifi/p2p/WifiP2pManager;->setWifiP2pChannels(Landroid/net/wifi/p2p/WifiP2pManager$Channel;IILandroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,sdk,system-api,test-api` +* `Landroid/provider/Settings$Global;->TETHER_OFFLOAD_DISABLED:Ljava/lang/String;,sdk,system-api,test-api`
@@ -290,12 +330,10 @@ Nonexported system resources: * (since API 30) `@com.android.networkstack.tethering:array/config_tether_wifi_p2p_regexs` * (since API 30) `@com.android.networkstack.tethering:array/config_tether_wifi_regexs` * (since API 30) `@com.android.networkstack.tethering:array/config_tether_wigig_regexs` +* (since API 30) `@com.android.wifi.resources:bool/config_wifi_p2p_mac_randomization_supported` * (since API 30) `@com.android.wifi.resources:integer/config_wifiFrameworkSoftApShutDownTimeoutMilliseconds` -Other: - -* (since API 29) `android.net.wifi.p2p.WifiP2pConfig` needs to be parcelized in a very specific order, except for possible extra fields at the end. (used only for safe mode) -* Activity `com.android.settings/.Settings$TetherSettingsActivity` is assumed to be exported. +Other: Activity `com.android.settings/.Settings$TetherSettingsActivity` is assumed to be exported. For `ip rule` priorities, `RULE_PRIORITY_SECURE_VPN` and `RULE_PRIORITY_TETHERING` is assumed to be 12000 and 18000 respectively; (prior to API 24) `RULE_PRIORITY_DEFAULT_NETWORK` is assumed to be 22000 (or at least > 18000). diff --git a/build.gradle.kts b/build.gradle.kts index 9a4b13e0..f80fd139 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,8 +9,8 @@ buildscript { } dependencies { - classpath(kotlin("gradle-plugin", "1.5.20")) - classpath("com.android.tools.build:gradle:7.0.0-beta05") + classpath(kotlin("gradle-plugin", "1.5.21")) + classpath("com.android.tools.build:gradle:7.0.0") classpath("com.google.firebase:firebase-crashlytics-gradle:2.7.1") classpath("com.google.android.gms:oss-licenses-plugin:0.10.4") classpath("com.google.gms:google-services:4.3.8") diff --git a/mobile/build.gradle.kts b/mobile/build.gradle.kts index bee3ee8e..005b6a3f 100644 --- a/mobile/build.gradle.kts +++ b/mobile/build.gradle.kts @@ -9,23 +9,23 @@ plugins { } android { - val javaVersion = JavaVersion.VERSION_1_8 + val javaVersion = JavaVersion.VERSION_11 val targetSdk = 29 - buildToolsVersion = "30.0.3" + buildToolsVersion = "31.0.0" compileOptions { isCoreLibraryDesugaringEnabled = true sourceCompatibility = javaVersion targetCompatibility = javaVersion } - compileSdk = 30 + compileSdk = 31 kotlinOptions.jvmTarget = javaVersion.toString() defaultConfig { applicationId = "be.mygod.vpnhotspot" minSdk = 21 this.targetSdk = targetSdk resourceConfigurations.addAll(arrayOf("it", "ru", "zh-rCN", "zh-rTW")) - versionCode = 262 - versionName = "2.11.9" + versionCode = 277 + versionName = "2.12.6" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" javaCompileOptions.annotationProcessorOptions.arguments.apply { put("room.expandProjection", "true") @@ -70,11 +70,11 @@ dependencies { coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5") kapt("androidx.room:room-compiler:$roomVersion") implementation(kotlin("stdlib-jdk8")) - implementation("androidx.appcompat:appcompat:1.3.0") // https://issuetracker.google.com/issues/151603528 + implementation("androidx.appcompat:appcompat:1.3.1") // https://issuetracker.google.com/issues/151603528 implementation("androidx.browser:browser:1.3.0") implementation("androidx.core:core-ktx:1.6.0") implementation("androidx.emoji:emoji:1.1.0") - implementation("androidx.fragment:fragment-ktx:1.3.5") + implementation("androidx.fragment:fragment-ktx:1.3.6") implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion") @@ -83,9 +83,9 @@ dependencies { implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation("com.android.billingclient:billing-ktx:4.0.0") implementation("com.google.android.gms:play-services-oss-licenses:17.0.0") - implementation("com.google.android.material:material:1.4.0") + implementation("com.google.android.material:material:1.5.0-alpha01") implementation("com.google.firebase:firebase-analytics-ktx:19.0.0") - implementation("com.google.firebase:firebase-crashlytics:18.1.0") + implementation("com.google.firebase:firebase-crashlytics:18.2.0") implementation("com.google.zxing:core:3.4.1") implementation("com.jakewharton.timber:timber:4.7.1") implementation("com.linkedin.dexmaker:dexmaker:2.28.1") diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index c16a50a7..0dc395d0 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -9,9 +9,6 @@ - @@ -24,6 +21,9 @@ + @@ -52,12 +52,15 @@ - + + @@ -100,6 +104,7 @@ + + + + + + + + android:enabled="false" + android:exported="true"> diff --git a/mobile/src/main/java/be/mygod/librootkotlinx/RootServer.kt b/mobile/src/main/java/be/mygod/librootkotlinx/RootServer.kt index f6f5fc4e..47559b39 100644 --- a/mobile/src/main/java/be/mygod/librootkotlinx/RootServer.kt +++ b/mobile/src/main/java/be/mygod/librootkotlinx/RootServer.kt @@ -8,7 +8,6 @@ import android.os.RemoteException import android.system.Os import android.system.OsConstants import androidx.collection.LongSparseArray -import androidx.collection.set import androidx.collection.valueIterator import kotlinx.coroutines.* import kotlinx.coroutines.channels.* @@ -102,6 +101,7 @@ class RootServer { private fun readUnexpectedStderr(): String? { if (!this::process.isInitialized) return null + Logger.me.d("Attempting to read stderr") var available = process.errorStream.available() return if (available <= 0) null else String(ByteArrayOutputStream().apply { try { @@ -146,7 +146,9 @@ class RootServer { try { val token2 = UUID.randomUUID().toString() val persistence = File(context.codeCacheDir, ".librootkotlinx-uuid") - val uuid = context.packageName + '@' + if (persistence.canRead()) persistence.readText() else { + val uuid = context.packageName + '@' + try { + persistence.readText() + } catch (_: FileNotFoundException) { UUID.randomUUID().toString().also { persistence.writeText(it) } } val (script, relocated) = AppProcess.relocateScript(uuid) @@ -218,9 +220,9 @@ class RootServer { throw e } finally { Logger.me.d("Waiting for exit") - errorReader.await() + withContext(NonCancellable) { errorReader.await() } process.waitFor() - withContext(NonCancellable) { closeInternal(true) } + closeInternal(true) } } } @@ -246,7 +248,7 @@ class RootServer { @Suppress("UNCHECKED_CAST") val callback = Callback.Ordinary(this, counter, classLoader, future as CompletableDeferred) if (active) { - callbackLookup[counter] = callback + callbackLookup.append(counter, callback) sendLocked(command) } else future.cancel() callback @@ -277,7 +279,7 @@ class RootServer { @Suppress("UNCHECKED_CAST") val callback = Callback.Channel(this@RootServer, counter, classLoader, this as SendChannel) if (active) { - callbackLookup[counter] = callback + callbackLookup.append(counter, callback) sendLocked(command) } else callback.finish.cancel() callback @@ -290,7 +292,7 @@ class RootServer { } } - private suspend fun closeInternal(fromWorker: Boolean = false) = synchronized(callbackLookup) { + private fun closeInternal(fromWorker: Boolean = false) = synchronized(callbackLookup) { if (active) { active = false Logger.me.d(if (fromWorker) "Shutting down from worker" else "Shutting down from client") @@ -432,7 +434,7 @@ class RootServer { } is RootCommand<*> -> { val commandJob = Job() - cancellables[callback] = { commandJob.cancel() } + cancellables.append(callback) { commandJob.cancel() } defaultWorker.launch(commandJob) { val result = try { val result = command.execute(); @@ -450,7 +452,7 @@ class RootServer { val result = try { coroutineScope { command.create(this).also { - cancellables[callback] = { it.cancel() } + cancellables.append(callback) { it.cancel() } }.consumeEach { result -> withContext(callbackWorker) { output.pushResult(callback, result) } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/AlertDialogFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/AlertDialogFragment.kt index c20b1ace..0f607081 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/AlertDialogFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/AlertDialogFragment.kt @@ -10,6 +10,7 @@ import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.setFragmentResult import androidx.fragment.app.setFragmentResultListener +import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.parcelize.Parcelize /** @@ -44,7 +45,7 @@ abstract class AlertDialogFragment : fun key(resultKey: String = javaClass.name) = args().putString(KEY_RESULT, resultKey) override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog = - AlertDialog.Builder(requireContext()).also { it.prepare(this) }.create() + MaterialAlertDialogBuilder(requireContext()).also { it.prepare(this) }.create() override fun onClick(dialog: DialogInterface?, which: Int) { setFragmentResult(resultKey ?: return, Bundle().apply { diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/App.kt b/mobile/src/main/java/be/mygod/vpnhotspot/App.kt index 78b808f3..9e2eca05 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/App.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/App.kt @@ -62,6 +62,7 @@ class App : Application() { } } Timber.plant(object : Timber.DebugTree() { + @SuppressLint("LogNotTimber") override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { if (t == null) { if (priority != Log.DEBUG || BuildConfig.DEBUG) Log.println(priority, tag, message) diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt index 7c80c71b..95ead806 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt @@ -6,16 +6,13 @@ import android.net.wifi.WifiManager import android.os.Build import androidx.annotation.RequiresApi import be.mygod.vpnhotspot.net.IpNeighbour -import be.mygod.vpnhotspot.net.TetherType -import be.mygod.vpnhotspot.net.TetheringManager -import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat import be.mygod.vpnhotspot.net.wifi.WifiApManager +import be.mygod.vpnhotspot.net.wifi.WifiApManager.wifiApState import be.mygod.vpnhotspot.util.Services import be.mygod.vpnhotspot.util.StickyEvent1 -import be.mygod.vpnhotspot.util.broadcastReceiver import be.mygod.vpnhotspot.widget.SmartSnackbar import kotlinx.coroutines.* import timber.log.Timber @@ -43,7 +40,8 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope { null -> return // stopped "" -> WifiApManager.cancelLocalOnlyHotspotRequest() } - reservation?.close() ?: stopService() + reservation?.close() + stopService() } } @@ -56,24 +54,6 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope { override val coroutineContext = dispatcher + Job() private var routingManager: RoutingManager? = null private var timeoutMonitor: TetherTimeoutMonitor? = null - private var receiverRegistered = false - private val receiver = broadcastReceiver { _, intent -> - val ifaces = (intent.localOnlyTetheredIfaces ?: return@broadcastReceiver).filter { - TetherType.ofInterface(it) != TetherType.WIFI_P2P - } - Timber.d("onTetherStateChangedLocked: $ifaces") - check(ifaces.size <= 1) - val iface = ifaces.singleOrNull() - binder.iface = iface - if (iface.isNullOrEmpty()) stopService() else launch { - val routingManager = routingManager - if (routingManager == null) { - this@LocalOnlyHotspotService.routingManager = RoutingManager.LocalOnly(this@LocalOnlyHotspotService, - iface).apply { start() } - IpNeighbourMonitor.registerCallback(this@LocalOnlyHotspotService) - } else check(iface == routingManager.downstream) - } - } override val activeIfaces get() = binder.iface.let { if (it.isNullOrEmpty()) emptyList() else listOf(it) } override fun onBind(intent: Intent?) = binder @@ -87,20 +67,36 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope { override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) { if (reservation == null) onFailed(-2) else { this@LocalOnlyHotspotService.reservation = reservation - if (!receiverRegistered) { - val configuration = binder.configuration!! - if (Build.VERSION.SDK_INT < 30 && configuration.isAutoShutdownEnabled) { - timeoutMonitor = TetherTimeoutMonitor(configuration.shutdownTimeoutMillis, - coroutineContext) { reservation.close() } + val configuration = binder.configuration!! + if (Build.VERSION.SDK_INT < 30 && configuration.isAutoShutdownEnabled) { + timeoutMonitor = TetherTimeoutMonitor(configuration.shutdownTimeoutMillis, + 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)) - receiverRegistered = true + return stopService() + } + binder.iface = iface + launch { + check(routingManager == null) + routingManager = RoutingManager.LocalOnly( + this@LocalOnlyHotspotService, iface).apply { start() } + IpNeighbourMonitor.registerCallback(this@LocalOnlyHotspotService) } } } override fun onStopped() { Timber.d("LOHCallback.onStopped") + reservation?.close() reservation = null } @@ -152,14 +148,10 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope { } private fun unregisterReceiver(exit: Boolean = false) { - if (receiverRegistered) { - unregisterReceiver(receiver) - IpNeighbourMonitor.unregisterCallback(this) - if (Build.VERSION.SDK_INT >= 28) { - timeoutMonitor?.close() - timeoutMonitor = null - } - receiverRegistered = false + IpNeighbourMonitor.unregisterCallback(this) + if (Build.VERSION.SDK_INT >= 28) { + timeoutMonitor?.close() + timeoutMonitor = null } launch { routingManager?.stop() diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt index abe09ae3..bf2950bb 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt @@ -15,7 +15,6 @@ import android.provider.Settings import androidx.annotation.RequiresApi import androidx.annotation.StringRes import androidx.core.content.edit -import be.mygod.librootkotlinx.useParcel import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.net.MacAddressCompat import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor @@ -53,14 +52,10 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene private const val KEY_AUTO_SHUTDOWN = "service.repeater.autoShutdown" private const val KEY_SHUTDOWN_TIMEOUT = "service.repeater.shutdownTimeout" private const val KEY_DEVICE_ADDRESS = "service.repeater.mac" - /** - * Placeholder for bypassing networkName check. - */ - private const val PLACEHOLDER_NETWORK_NAME = "DIRECT-00-VPNHotspot" var persistentSupported = false - @delegate:TargetApi(29) + @get:RequiresApi(29) private val hasP2pValidateName by lazy { val array = Build.VERSION.SECURITY_PATCH.split('-', limit = 3) val y = array.getOrNull(0)?.toIntOrNull() @@ -70,6 +65,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene val safeModeConfigurable get() = Build.VERSION.SDK_INT >= 29 && hasP2pValidateName val safeMode get() = Build.VERSION.SDK_INT >= 29 && (!hasP2pValidateName || app.pref.getBoolean(KEY_SAFE_MODE, true)) + private val mNetworkName by lazy { UnblockCentral.WifiP2pConfig_Builder_mNetworkName } var networkName: String? get() = app.pref.getString(KEY_NETWORK_NAME, null) @@ -79,7 +75,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene set(value) = app.pref.edit { putString(KEY_PASSPHRASE, value) } var operatingBand: Int @SuppressLint("InlinedApi") - get() = app.pref.getInt(KEY_OPERATING_BAND, SoftApConfigurationCompat.BAND_ANY) + get() = app.pref.getInt(KEY_OPERATING_BAND, SoftApConfigurationCompat.BAND_LEGACY) set(value) = app.pref.edit { putInt(KEY_OPERATING_BAND, value) } var operatingChannel: Int get() { @@ -398,59 +394,39 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene } val networkName = networkName val passphrase = passphrase - try { - if (!safeMode || networkName == null || passphrase == null) { - persistNextGroup = true - p2pManager.createGroup(channel, listener) - } else @TargetApi(29) { - p2pManager.createGroup(channel, WifiP2pConfig.Builder().apply { - setNetworkName(PLACEHOLDER_NETWORK_NAME) - setPassphrase(passphrase) - when (val oc = operatingChannel) { - 0 -> setGroupOperatingBand(when (val band = operatingBand) { - SoftApConfigurationCompat.BAND_ANY -> WifiP2pConfig.GROUP_OWNER_BAND_AUTO - SoftApConfigurationCompat.BAND_2GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_2GHZ - SoftApConfigurationCompat.BAND_5GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_5GHZ - else -> throw IllegalArgumentException("Unknown band $band") - }) + @SuppressLint("MissingPermission") // missing permission will simply leading to returning ERROR + if (!safeMode || networkName == null || passphrase == null) { + persistNextGroup = true + p2pManager.createGroup(channel, listener) + } else @TargetApi(29) { + p2pManager.createGroup(channel, WifiP2pConfig.Builder().apply { + try { + mNetworkName.set(this, networkName) // bypass networkName check + } catch (e: ReflectiveOperationException) { + Timber.w(e) + try { + setNetworkName(networkName) + } catch (e: IllegalArgumentException) { + Timber.w(e) + return startFailure(e.readableMessage) + } + } + setPassphrase(passphrase) + when (val oc = operatingChannel) { + 0 -> setGroupOperatingBand(when (val band = operatingBand) { + SoftApConfigurationCompat.BAND_2GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_2GHZ + SoftApConfigurationCompat.BAND_5GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_5GHZ else -> { - setGroupOperatingFrequency(SoftApConfigurationCompat.channelToFrequency(operatingBand, oc)) + require(SoftApConfigurationCompat.isLegacyEitherBand(band)) { "Unknown band $band" } + WifiP2pConfig.GROUP_OWNER_BAND_AUTO } + }) + else -> { + setGroupOperatingFrequency(SoftApConfigurationCompat.channelToFrequency(operatingBand, oc)) } - setDeviceAddress(deviceAddress?.toPlatform()) - }.build().run { - useParcel { p -> - p.writeParcelable(this, 0) - val end = p.dataPosition() - p.setDataPosition(0) - val creator = p.readString() - val deviceAddress = p.readString() - val wps = p.readParcelable(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) + } + setDeviceAddress(deviceAddress?.toPlatform()) + }.build(), listener) } } /** @@ -512,7 +488,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene if (reason != WifiP2pManager.BUSY) { SmartSnackbar.make(formatReason(R.string.repeater_remove_group_failure, reason)).show() } // else assuming it's already gone - launch { cleanLocked() } + onSuccess() } }) } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/ServiceNotification.kt b/mobile/src/main/java/be/mygod/vpnhotspot/ServiceNotification.kt index fd7c8853..740b170e 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/ServiceNotification.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/ServiceNotification.kt @@ -12,25 +12,29 @@ import be.mygod.vpnhotspot.App.Companion.app import java.util.* object ServiceNotification { - private const val CHANNEL = "tethering" - private const val CHANNEL_ID = 1 + private const val CHANNEL_ACTIVE = "tethering" + private const val CHANNEL_INACTIVE = "tethering-inactive" + private const val NOTIFICATION_ID = 1 private val deviceCountsMap = WeakHashMap>() private val inactiveMap = WeakHashMap>() private val manager = app.getSystemService()!! private fun buildNotification(context: Context): Notification { - val builder = NotificationCompat.Builder(context, CHANNEL) - .setWhen(0) - .setCategory(NotificationCompat.CATEGORY_SERVICE) - .setColor(ContextCompat.getColor(context, R.color.colorPrimary)) - .setContentTitle(context.getText(R.string.notification_tethering_title)) - .setSmallIcon(R.drawable.ic_quick_settings_tile_on) - .setContentIntent(PendingIntent.getActivity(context, 0, - Intent(context, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT)) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) val deviceCounts = deviceCountsMap.values.flatMap { it.entries }.sortedBy { it.key } val inactive = inactiveMap.values.flatten() + val isInactive = inactive.isNotEmpty() && deviceCounts.isEmpty() + val builder = NotificationCompat.Builder(context, if (isInactive) CHANNEL_INACTIVE else CHANNEL_ACTIVE).apply { + setWhen(0) + setCategory(NotificationCompat.CATEGORY_SERVICE) + color = ContextCompat.getColor(context, R.color.colorPrimary) + setContentTitle(context.getText(R.string.notification_tethering_title)) + setSmallIcon(R.drawable.ic_quick_settings_tile_on) + setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)) + setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + priority = if (isInactive) NotificationCompat.PRIORITY_MIN else NotificationCompat.PRIORITY_LOW + } var lines = deviceCounts.map { (dev, size) -> context.resources.getQuantityString(R.plurals.notification_connected_devices, size, size, dev) } @@ -54,23 +58,28 @@ object ServiceNotification { synchronized(this) { deviceCountsMap[service] = deviceCounts if (inactive.isEmpty()) inactiveMap.remove(service) else inactiveMap[service] = inactive - service.startForeground(CHANNEL_ID, buildNotification(service)) + service.startForeground(NOTIFICATION_ID, buildNotification(service)) } } fun stopForeground(service: Service) = synchronized(this) { - deviceCountsMap.remove(service) - if (deviceCountsMap.isEmpty()) service.stopForeground(true) else { - service.stopForeground(false) - manager.notify(CHANNEL_ID, buildNotification(service)) - } + deviceCountsMap.remove(service) ?: return@synchronized + val shutdown = deviceCountsMap.isEmpty() + service.stopForeground(shutdown) + if (!shutdown) manager.notify(NOTIFICATION_ID, buildNotification(service)) } fun updateNotificationChannels() { if (Build.VERSION.SDK_INT >= 26) @TargetApi(26) { - val tethering = NotificationChannel(CHANNEL, - app.getText(R.string.notification_channel_tethering), NotificationManager.IMPORTANCE_LOW) - tethering.lockscreenVisibility = Notification.VISIBILITY_PUBLIC - manager.createNotificationChannel(tethering) + NotificationChannel(CHANNEL_ACTIVE, + app.getText(R.string.notification_channel_tethering), NotificationManager.IMPORTANCE_LOW).apply { + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + manager.createNotificationChannel(this) + } + NotificationChannel(CHANNEL_INACTIVE, + app.getText(R.string.notification_channel_monitor), NotificationManager.IMPORTANCE_LOW).apply { + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + manager.createNotificationChannel(this) + } // remove old service channels manager.deleteNotificationChannel("hotspot") manager.deleteNotificationChannel("repeater") diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/SettingsPreferenceFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/SettingsPreferenceFragment.kt index 5d3372b0..cef0e46a 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/SettingsPreferenceFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/SettingsPreferenceFragment.kt @@ -14,13 +14,12 @@ import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor import be.mygod.vpnhotspot.net.monitor.IpMonitor import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock -import be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreferenceDialogFragment +import be.mygod.vpnhotspot.preference.AutoCompleteNetworkPreferenceDialogFragment import be.mygod.vpnhotspot.preference.SharedPreferenceDataStore import be.mygod.vpnhotspot.preference.SummaryFallbackProvider import be.mygod.vpnhotspot.root.Dump import be.mygod.vpnhotspot.root.RootManager import be.mygod.vpnhotspot.util.Services -import be.mygod.vpnhotspot.util.allInterfaceNames import be.mygod.vpnhotspot.util.launchUrl import be.mygod.vpnhotspot.util.showAllowingStateLoss import be.mygod.vpnhotspot.widget.SmartSnackbar @@ -144,16 +143,12 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() { } } - override fun onDisplayPreferenceDialog(preference: Preference) { - when (preference.key) { - UpstreamMonitor.KEY, FallbackUpstreamMonitor.KEY -> - AlwaysAutoCompleteEditTextPreferenceDialogFragment().apply { - setArguments(preference.key, Services.connectivity.allNetworks.mapNotNull { - Services.connectivity.getLinkProperties(it)?.allInterfaceNames - }.flatten().toTypedArray()) - setTargetFragment(this@SettingsPreferenceFragment, 0) - }.showAllowingStateLoss(parentFragmentManager, preference.key) - else -> super.onDisplayPreferenceDialog(preference) - } + override fun onDisplayPreferenceDialog(preference: Preference) = when (preference.key) { + UpstreamMonitor.KEY, FallbackUpstreamMonitor.KEY -> + AutoCompleteNetworkPreferenceDialogFragment().apply { + setArguments(preference.key) + setTargetFragment(this@SettingsPreferenceFragment, 0) + }.showAllowingStateLoss(parentFragmentManager, preference.key) + else -> super.onDisplayPreferenceDialog(preference) } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientViewModel.kt b/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientViewModel.kt index a29271d2..69d08aad 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientViewModel.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientViewModel.kt @@ -4,7 +4,10 @@ import android.content.ComponentName import android.content.IntentFilter import android.content.ServiceConnection import android.net.wifi.p2p.WifiP2pDevice +import android.os.Build import android.os.IBinder +import android.os.Parcelable +import androidx.annotation.RequiresApi import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.MutableLiveData @@ -13,13 +16,19 @@ import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.RepeaterService import be.mygod.vpnhotspot.net.IpNeighbour import be.mygod.vpnhotspot.net.MacAddressCompat +import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toCompat +import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor +import be.mygod.vpnhotspot.net.wifi.WifiApManager +import be.mygod.vpnhotspot.net.wifi.WifiClient +import be.mygod.vpnhotspot.root.WifiApCommands import be.mygod.vpnhotspot.util.broadcastReceiver -class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callback, DefaultLifecycleObserver { +class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callback, DefaultLifecycleObserver, + WifiApManager.SoftApCallbackCompat { private var tetheredInterfaces = emptySet() private val receiver = broadcastReceiver { _, intent -> tetheredInterfaces = (intent.tetheredIfaces ?: return@broadcastReceiver).toSet() + @@ -29,6 +38,7 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb private var repeater: RepeaterService.Binder? = null private var p2p: Collection = emptyList() + private var wifiAp = emptyList>() private var neighbours: Collection = emptyList() val clients = MutableLiveData>() val fullMode = object : DefaultLifecycleObserver { @@ -42,11 +52,18 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb private fun populateClients() { val clients = HashMap, Client>() - val group = repeater?.group - val p2pInterface = group?.`interface` - if (p2pInterface != null) { - for (client in p2p) clients[p2pInterface to MacAddressCompat.fromString(client.deviceAddress)] = - WifiP2pClient(p2pInterface, client) + repeater?.group?.`interface`?.let { p2pInterface -> + for (client in p2p) { + val addr = MacAddressCompat.fromString(client.deviceAddress!!) + clients[p2pInterface to addr] = object : Client(addr, p2pInterface) { + override val icon: Int get() = TetherType.WIFI_P2P.icon + } + } + } + for (client in wifiAp) { + clients[client] = object : Client(client.second, client.first) { + override val icon: Int get() = TetherType.WIFI.icon + } } for (neighbour in neighbours) { val key = neighbour.dev to neighbour.lladdr @@ -70,8 +87,10 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb override fun onStart(owner: LifecycleOwner) { app.registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED)) IpNeighbourMonitor.registerCallback(this, false) + if (Build.VERSION.SDK_INT >= 31) WifiApCommands.registerSoftApCallback(this) } override fun onStop(owner: LifecycleOwner) { + if (Build.VERSION.SDK_INT >= 31) WifiApCommands.unregisterSoftApCallback(this) IpNeighbourMonitor.unregisterCallback(this) app.unregisterReceiver(receiver) } @@ -94,4 +113,12 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb this.neighbours = neighbours populateClients() } + + @RequiresApi(31) + override fun onConnectedClientsChanged(clients: List) { + wifiAp = clients.mapNotNull { + val client = WifiClient(it) + client.apInstanceIdentifier?.run { this to client.macAddress.toCompat() } + } + } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientsFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientsFragment.kt index 914fab11..db48cb38 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientsFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientsFragment.kt @@ -37,7 +37,7 @@ import be.mygod.vpnhotspot.net.monitor.TrafficRecorder import be.mygod.vpnhotspot.room.AppDatabase import be.mygod.vpnhotspot.room.ClientStats import be.mygod.vpnhotspot.room.TrafficRecord -import be.mygod.vpnhotspot.util.SpanFormatter +import be.mygod.vpnhotspot.util.format import be.mygod.vpnhotspot.util.showAllowingStateLoss import be.mygod.vpnhotspot.util.toPluralInt import be.mygod.vpnhotspot.widget.SmartSnackbar @@ -82,10 +82,11 @@ class ClientsFragment : Fragment() { data class StatsArg(val title: CharSequence, val stats: ClientStats) : Parcelable class StatsDialogFragment : AlertDialogFragment() { override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) { - setTitle(SpanFormatter.format(getText(R.string.clients_stats_title), arg.title)) val context = context val resources = resources - val format = NumberFormat.getIntegerInstance(resources.configuration.locale) + val locale = resources.configuration.locale + setTitle(getText(R.string.clients_stats_title).format(locale, arg.title)) + val format = NumberFormat.getIntegerInstance(locale) setMessage("%s\n%s\n%s".format( resources.getQuantityString(R.plurals.clients_stats_message_1, arg.stats.count.toPluralInt(), format.format(arg.stats.count), DateUtils.formatDateTime(context, arg.stats.timestamp, diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/client/WifiP2pClient.kt b/mobile/src/main/java/be/mygod/vpnhotspot/client/WifiP2pClient.kt deleted file mode 100644 index fa805954..00000000 --- a/mobile/src/main/java/be/mygod/vpnhotspot/client/WifiP2pClient.kt +++ /dev/null @@ -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 -} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/BluetoothTethering.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/BluetoothTethering.kt index 5a52d2ab..ac87da09 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/BluetoothTethering.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/BluetoothTethering.kt @@ -1,5 +1,6 @@ package be.mygod.vpnhotspot.manage +import android.annotation.SuppressLint import android.annotation.TargetApi import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothProfile @@ -9,7 +10,6 @@ import android.content.Intent import android.content.IntentFilter import android.os.Build import androidx.annotation.RequiresApi -import androidx.core.os.BuildCompat import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.util.broadcastReceiver @@ -18,7 +18,7 @@ import be.mygod.vpnhotspot.widget.SmartSnackbar import timber.log.Timber import java.lang.reflect.InvocationTargetException -class BluetoothTethering(context: Context, val stateListener: () -> Unit) : +class BluetoothTethering(context: Context, private val adapter: BluetoothAdapter, val stateListener: () -> Unit) : BluetoothProfile.ServiceListener, AutoCloseable { companion object : BroadcastReceiver() { /** @@ -26,17 +26,9 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) : */ private const val PAN = 5 private val clazz by lazy { Class.forName("android.bluetooth.BluetoothPan") } - private val constructor by lazy { - clazz.getDeclaredConstructor(Context::class.java, BluetoothProfile.ServiceListener::class.java).apply { - isAccessible = true - } - } private val isTetheringOn by lazy { clazz.getDeclaredMethod("isTetheringOn") } - fun pan(context: Context, serviceListener: BluetoothProfile.ServiceListener) = - constructor.newInstance(context, serviceListener) as BluetoothProfile - val BluetoothProfile.isTetheringOn get() = isTetheringOn(this) as Boolean - fun BluetoothProfile.closePan() = BluetoothAdapter.getDefaultAdapter()!!.closeProfileProxy(PAN, this) + private val BluetoothProfile.isTetheringOn get() = isTetheringOn(this) as Boolean private fun registerBluetoothStateListener(receiver: BroadcastReceiver) = app.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)) @@ -58,28 +50,12 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) : pendingCallback = null app.unregisterReceiver(this) } - - /** - * https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/TetherSettings.java#384 - */ - @RequiresApi(24) - fun start(callback: TetheringManager.StartTetheringCallback) { - if (pendingCallback != null) return - val adapter = BluetoothAdapter.getDefaultAdapter() - try { - if (adapter?.state == BluetoothAdapter.STATE_OFF) { - registerBluetoothStateListener(this) - pendingCallback = callback - adapter.enable() - } else TetheringManager.startTethering(TetheringManager.TETHERING_BLUETOOTH, true, callback) - } catch (e: SecurityException) { - SmartSnackbar.make(e.readableMessage).shortToast().show() - } - } } + private var proxyCreated = false private var connected = false private var pan: BluetoothProfile? = null + private var stoppedByUser = false var activeFailureCause: Throwable? = null /** * Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/78d5efd/src/com/android/settings/TetherSettings.java @@ -88,7 +64,7 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) : val pan = pan ?: return null if (!connected) return null activeFailureCause = null - return BluetoothAdapter.getDefaultAdapter()?.state == BluetoothAdapter.STATE_ON && try { + val on = adapter.state == BluetoothAdapter.STATE_ON && try { pan.isTetheringOn } catch (e: InvocationTargetException) { activeFailureCause = e.cause ?: e @@ -96,16 +72,21 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) : else Timber.w(e) return null } + return if (stoppedByUser) { + if (!on) stoppedByUser = false + false + } else on } private val receiver = broadcastReceiver { _, _ -> stateListener() } fun ensureInit(context: Context) { - if (pan == null && BluetoothAdapter.getDefaultAdapter() != null) try { - pan = pan(context, this) - } catch (e: ReflectiveOperationException) { - if (e.cause is SecurityException && BuildCompat.isAtLeastS()) Timber.d(e.readableMessage) - else Timber.w(e) + activeFailureCause = null + if (!proxyCreated) try { + check(adapter.getProfileProxy(context, this, PAN)) + proxyCreated = true + } catch (e: SecurityException) { + if (Build.VERSION.SDK_INT >= 31) Timber.d(e.readableMessage) else Timber.w(e) activeFailureCause = e } } @@ -116,13 +97,39 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) : override fun onServiceDisconnected(profile: Int) { connected = false + stoppedByUser = false } override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) { + pan = proxy connected = true stateListener() } + + /** + * https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/TetherSettings.java#384 + */ + @SuppressLint("MissingPermission") + @RequiresApi(24) + fun start(callback: TetheringManager.StartTetheringCallback) { + if (pendingCallback == null) try { + if (adapter.state == BluetoothAdapter.STATE_OFF) { + registerBluetoothStateListener(BluetoothTethering) + pendingCallback = callback + adapter.enable() + } else TetheringManager.startTethering(TetheringManager.TETHERING_BLUETOOTH, true, callback) + } catch (e: SecurityException) { + SmartSnackbar.make(e.readableMessage).shortToast().show() + pendingCallback = null + } + } + @RequiresApi(24) + fun stop(callback: (Exception) -> Unit) { + TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, callback) + stoppedByUser = true + } + override fun close() { app.unregisterReceiver(receiver) - pan?.closePan() + adapter.closeProfileProxy(PAN, pan) } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotManager.kt index 994d474a..62db344d 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotManager.kt @@ -1,13 +1,13 @@ package be.mygod.vpnhotspot.manage import android.Manifest -import android.annotation.TargetApi import android.content.* import android.os.Build import android.os.IBinder import android.provider.Settings import android.view.View import android.widget.Toast +import androidx.annotation.RequiresApi import androidx.recyclerview.widget.RecyclerView import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.LocalOnlyHotspotService @@ -18,7 +18,7 @@ import be.mygod.vpnhotspot.util.formatAddresses import be.mygod.vpnhotspot.widget.SmartSnackbar import java.net.NetworkInterface -@TargetApi(26) +@RequiresApi(26) class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager(), ServiceConnection { companion object { val permission = if (Build.VERSION.SDK_INT >= 29) { diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt index 239d17c1..796c8282 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt @@ -5,7 +5,6 @@ import android.content.ComponentName import android.content.DialogInterface import android.content.Intent import android.content.ServiceConnection -import android.content.pm.PackageManager import android.net.wifi.SoftApConfiguration import android.net.wifi.p2p.WifiP2pGroup import android.os.Build @@ -30,6 +29,7 @@ import be.mygod.vpnhotspot.net.MacAddressCompat import be.mygod.vpnhotspot.net.wifi.P2pSupplicantConfiguration import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat import be.mygod.vpnhotspot.net.wifi.WifiApDialogFragment +import be.mygod.vpnhotspot.net.wifi.WifiApManager import be.mygod.vpnhotspot.util.ServiceForegroundConnector import be.mygod.vpnhotspot.util.formatAddresses import be.mygod.vpnhotspot.util.showAllowingStateLoss @@ -89,11 +89,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic when (binder?.service?.status) { RepeaterService.Status.IDLE -> if (Build.VERSION.SDK_INT < 29) parent.requireContext().let { context -> ContextCompat.startForegroundService(context, Intent(context, RepeaterService::class.java)) - } else if (parent.requireContext().checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == - PackageManager.PERMISSION_GRANTED || - parent.shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) { - parent.startRepeater.launch(Manifest.permission.ACCESS_FINE_LOCATION) - } else SmartSnackbar.make(R.string.repeater_missing_location_permissions).shortToast().show() + } else parent.startRepeater.launch(Manifest.permission.ACCESS_FINE_LOCATION) RepeaterService.Status.ACTIVE -> binder.shutdown() else -> { } } @@ -192,23 +188,23 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic return SoftApConfigurationCompat( ssid = networkName, passphrase = passphrase, - band = RepeaterService.operatingBand, - channel = RepeaterService.operatingChannel, securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, // is not actually used isAutoShutdownEnabled = RepeaterService.isAutoShutdownEnabled, shutdownTimeoutMillis = RepeaterService.shutdownTimeoutMillis).apply { bssid = RepeaterService.deviceAddress + setChannel(RepeaterService.operatingChannel, RepeaterService.operatingBand) + setMacRandomizationEnabled(WifiApManager.p2pMacRandomizationSupported) } to false } } else binder?.let { binder -> val group = binder.group ?: binder.fetchPersistentGroup().let { binder.group } if (group != null) return SoftApConfigurationCompat( ssid = group.networkName, - channel = RepeaterService.operatingChannel, - band = SoftApConfigurationCompat.BAND_ANY, securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, // is not actually used isAutoShutdownEnabled = RepeaterService.isAutoShutdownEnabled, shutdownTimeoutMillis = RepeaterService.shutdownTimeoutMillis).run { + setChannel(RepeaterService.operatingChannel) + setMacRandomizationEnabled(WifiApManager.p2pMacRandomizationSupported) try { val config = P2pSupplicantConfiguration(group) config.init(binder.obtainDeviceAddress()?.toString()) @@ -230,6 +226,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic return null } private suspend fun updateConfiguration(config: SoftApConfigurationCompat) { + val (band, channel) = config.requireSingleBand() if (RepeaterService.safeMode) { RepeaterService.networkName = config.ssid RepeaterService.deviceAddress = config.bssid @@ -246,8 +243,8 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic } holder.config = null } - RepeaterService.operatingBand = config.band - RepeaterService.operatingChannel = config.channel + RepeaterService.operatingBand = band + RepeaterService.operatingChannel = channel RepeaterService.isAutoShutdownEnabled = config.isAutoShutdownEnabled RepeaterService.shutdownTimeoutMillis = config.shutdownTimeoutMillis } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt index 36b86630..685ee034 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt @@ -2,40 +2,41 @@ package be.mygod.vpnhotspot.manage import android.Manifest import android.annotation.TargetApi -import android.content.ClipData +import android.bluetooth.BluetoothAdapter import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.content.pm.PackageManager -import android.net.MacAddress import android.os.Build +import android.os.Parcelable import android.provider.Settings +import android.text.SpannableStringBuilder +import android.text.format.DateUtils import android.view.View import android.widget.Toast import androidx.annotation.RequiresApi import androidx.core.net.toUri -import androidx.core.os.BuildCompat import androidx.core.view.updatePaddingRelative -import androidx.fragment.app.Fragment import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.RecyclerView import be.mygod.vpnhotspot.App.Companion.app -import be.mygod.vpnhotspot.BuildConfig import be.mygod.vpnhotspot.MainActivity import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetheringManager -import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat -import be.mygod.vpnhotspot.net.wifi.WifiApManager +import be.mygod.vpnhotspot.net.wifi.* +import be.mygod.vpnhotspot.net.wifi.WifiApManager.wifiApState import be.mygod.vpnhotspot.root.WifiApCommands -import be.mygod.vpnhotspot.util.readableMessage +import be.mygod.vpnhotspot.util.* import be.mygod.vpnhotspot.widget.SmartSnackbar import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import timber.log.Timber import java.lang.reflect.InvocationTargetException +import java.util.* sealed class TetherManager(protected val parent: TetheringFragment) : Manager(), TetheringManager.StartTetheringCallback { @@ -63,12 +64,16 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(), } catch (e: RuntimeException) { app.logEvent("manage_write_settings") { param("message", e.toString()) } } - if (manager.isStarted) try { - manager.stop() - } catch (e: InvocationTargetException) { - if (e.targetException !is SecurityException) Timber.w(e) - manager.onException(e) - } else manager.start() + when (manager.isStarted) { + true -> try { + manager.stop() + } catch (e: InvocationTargetException) { + if (e.targetException !is SecurityException) Timber.w(e) + manager.onException(e) + } + false -> manager.start() + null -> manager.onClickNull() + } } } @@ -79,13 +84,13 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(), override val icon get() = tetherType.icon override val title get() = this@TetherManager.title override val text get() = this@TetherManager.text - override val active get() = isStarted + override val active get() = isStarted == true } val data = Data() abstract val title: CharSequence abstract val tetherType: TetherType - open val isStarted get() = parent.enabledTypes.contains(tetherType) + open val isStarted: Boolean? get() = parent.enabledTypes.contains(tetherType) protected open val text: CharSequence get() = baseError ?: "" protected var baseError: String? = null @@ -93,6 +98,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(), protected abstract fun start() protected abstract fun stop() + protected open fun onClickNull(): Unit = throw UnsupportedOperationException() override fun onTetheringStarted() = data.notifyChange() override fun onTetheringFailed(error: Int?) { @@ -119,11 +125,13 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(), (viewHolder as ViewHolder).manager = this } - fun updateErrorMessage(errored: List) { + fun updateErrorMessage(errored: List, lastErrors: Map) { val interested = errored.filter { TetherType.ofInterface(it) == tetherType } baseError = if (interested.isEmpty()) null else interested.joinToString("\n") { iface -> "$iface: " + try { - TetheringManager.tetherErrorLookup(TetheringManager.getLastTetherError(iface)) + TetheringManager.tetherErrorLookup(if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") { + TetheringManager.getLastTetherError(iface) + } else lastErrors[iface] ?: 0) } catch (e: InvocationTargetException) { if (Build.VERSION.SDK_INT !in 24..25 || e.cause !is SecurityException) Timber.w(e) else Timber.d(e) e.readableMessage @@ -135,77 +143,156 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(), @RequiresApi(24) class Wifi(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver, WifiApManager.SoftApCallbackCompat { + private val receiver = broadcastReceiver { _, intent -> + failureReason = if (intent.wifiApState == WifiApManager.WIFI_AP_STATE_FAILED) { + intent.getIntExtra(WifiApManager.EXTRA_WIFI_AP_FAILURE_REASON, 0) + } else null + data.notifyChange() + } private var failureReason: Int? = null private var numClients: Int? = null - private var frequency = 0 - private var bandwidth = WifiApManager.CHANNEL_WIDTH_INVALID - private var capability: Pair? = null + private var info = emptyList() + private var capability: Parcelable? = null init { - if (Build.VERSION.SDK_INT >= 28) parent.viewLifecycleOwner.lifecycle.addObserver(this) + if (Build.VERSION.SDK_INT >= 23) parent.viewLifecycleOwner.lifecycle.addObserver(this) } - @TargetApi(28) override fun onStart(owner: LifecycleOwner) { - WifiApCommands.registerSoftApCallback(this) + if (Build.VERSION.SDK_INT < 28) { + parent.requireContext().registerReceiver(receiver, + IntentFilter(WifiApManager.WIFI_AP_STATE_CHANGED_ACTION)) + } else WifiApCommands.registerSoftApCallback(this) } - @TargetApi(28) override fun onStop(owner: LifecycleOwner) { - WifiApCommands.unregisterSoftApCallback(this) + if (Build.VERSION.SDK_INT < 28) { + parent.requireContext().unregisterReceiver(receiver) + } else WifiApCommands.unregisterSoftApCallback(this) } override fun onStateChanged(state: Int, failureReason: Int) { - if (state < 10 || state > 14) { - Timber.w(Exception("Unknown state $state, $failureReason")) - return - } - this.failureReason = if (state == 14) failureReason else null // WIFI_AP_STATE_FAILED + if (!WifiApManager.checkWifiApState(state)) return + this.failureReason = if (state == WifiApManager.WIFI_AP_STATE_FAILED) failureReason else null data.notifyChange() } override fun onNumClientsChanged(numClients: Int) { this.numClients = numClients - if (Build.VERSION.SDK_INT >= 30) data.notifyChange() // only emits when onCapabilityChanged can be called - } - override fun onInfoChanged(frequency: Int, bandwidth: Int) { - this.frequency = frequency - this.bandwidth = bandwidth data.notifyChange() } - override fun onCapabilityChanged(maxSupportedClients: Int, supportedFeatures: Long) { - capability = maxSupportedClients to supportedFeatures + override fun onInfoChanged(info: List) { + this.info = info data.notifyChange() } - override fun onBlockedClientConnecting(client: MacAddress, blockedReason: Int) { - val reason = WifiApManager.clientBlockLookup(blockedReason, true) - Timber.i("$client blocked from connecting: $reason ($blockedReason)") - SmartSnackbar.make(parent.getString(R.string.tethering_manage_wifi_client_blocked, client, reason)).apply { - action(R.string.tethering_manage_wifi_copy_mac) { - app.clipboard.setPrimaryClip(ClipData.newPlainText(null, client.toString())) - } - }.show() + override fun onCapabilityChanged(capability: Parcelable) { + this.capability = capability + data.notifyChange() } override val title get() = parent.getString(R.string.tethering_manage_wifi) override val tetherType get() = TetherType.WIFI override val type get() = VIEW_TYPE_WIFI - override val text get() = listOfNotNull(failureReason?.let { WifiApManager.failureReasonLookup(it) }, baseError, - if (frequency != 0 || bandwidth != WifiApManager.CHANNEL_WIDTH_INVALID) { - parent.getString(R.string.tethering_manage_wifi_info, frequency, - SoftApConfigurationCompat.frequencyToChannel(frequency), - WifiApManager.channelWidthLookup(bandwidth, true)) - } else null, - capability?.let { (maxSupportedClients, supportedFeatures) -> - app.resources.getQuantityString(R.plurals.tethering_manage_wifi_capabilities, maxSupportedClients, - numClients ?: "?", maxSupportedClients, sequence { - var features = supportedFeatures - if (features == 0L) yield(parent.getString(R.string.tethering_manage_wifi_no_features)) - else while (features != 0L) { - val bit = features.takeLowestOneBit() - yield(WifiApManager.featureLookup(bit, true)) - features = features and bit.inv() + + @TargetApi(30) + private fun formatCapability(locale: Locale) = capability?.let { parcel -> + val capability = SoftApCapability(parcel) + val numClients = numClients + val maxClients = capability.maxSupportedClients + var features = capability.supportedFeatures + if (Build.VERSION.SDK_INT >= 31) for ((flag, band) in arrayOf( + SoftApCapability.SOFTAP_FEATURE_BAND_24G_SUPPORTED to SoftApConfigurationCompat.BAND_2GHZ, + SoftApCapability.SOFTAP_FEATURE_BAND_5G_SUPPORTED to SoftApConfigurationCompat.BAND_5GHZ, + SoftApCapability.SOFTAP_FEATURE_BAND_6G_SUPPORTED to SoftApConfigurationCompat.BAND_6GHZ, + SoftApCapability.SOFTAP_FEATURE_BAND_60G_SUPPORTED to SoftApConfigurationCompat.BAND_60GHZ, + )) { + if (capability.getSupportedChannelList(band).isEmpty()) continue + // reduce double reporting + features = features and flag.inv() + } + val result = parent.resources.getQuantityText(R.plurals.tethering_manage_wifi_capabilities, numClients ?: 0) + .format(locale, numClients ?: "?", maxClients, sequence { + if (WifiApManager.isApMacRandomizationSupported) yield(parent.getText( + R.string.tethering_manage_wifi_feature_ap_mac_randomization)) + if (Services.wifi.isStaApConcurrencySupported) yield(parent.getText( + R.string.tethering_manage_wifi_feature_sta_ap_concurrency)) + if (Build.VERSION.SDK_INT >= 31) { + if (Services.wifi.isBridgedApConcurrencySupported) yield(parent.getText( + R.string.tethering_manage_wifi_feature_bridged_ap_concurrency)) + if (Services.wifi.isStaBridgedApConcurrencySupported) yield(parent.getText( + R.string.tethering_manage_wifi_feature_sta_bridged_ap_concurrency)) + } + if (features != 0L) while (features != 0L) { + val bit = features.takeLowestOneBit() + yield(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()) - }).joinToString("\n") + pending?.let { + append('-') + append(it) + } + append(')') + } else null + }.filterNotNull() + if (list.isNotEmpty()) result.append(parent.getText(R.string.tethering_manage_wifi_supported_channels) + .format(locale, list.joinToString("; "))) + } + result + } ?: numClients?.let { numClients -> + app.resources.getQuantityText(R.plurals.tethering_manage_wifi_clients, numClients).format(locale, + numClients) + } + override val text get() = parent.resources.configuration.locale.let { locale -> + listOfNotNull(failureReason?.let { WifiApManager.failureReasonLookup(it) }, baseError, info.run { + if (isEmpty()) null else joinToSpanned("\n") @TargetApi(30) { parcel -> + val info = SoftApInfo(parcel) + val frequency = info.frequency + val channel = SoftApConfigurationCompat.frequencyToChannel(frequency) + val bandwidth = SoftApInfo.channelWidthLookup(info.bandwidth, true) + if (Build.VERSION.SDK_INT >= 31) { + val bssid = info.bssid.let { if (it == null) null else makeMacSpan(it.toString()) } + val bssidAp = info.apInstanceIdentifier?.let { + when (bssid) { + null -> it + is String -> "$bssid%$it" // take the fast route if possible + else -> SpannableStringBuilder(bssid).append("%$it") + } + } ?: bssid ?: "?" + val timeout = info.autoShutdownTimeoutMillis + parent.getText(if (timeout == 0L) { + R.string.tethering_manage_wifi_info_timeout_disabled + } else R.string.tethering_manage_wifi_info_timeout_enabled).format(locale, + frequency, channel, bandwidth, bssidAp, info.wifiStandard, + // http://unicode.org/cldr/trac/ticket/3407 + DateUtils.formatElapsedTime(timeout / 1000)) + } else parent.getText(R.string.tethering_manage_wifi_info).format(locale, + frequency, channel, bandwidth) + } + }, formatCapability(locale)).joinToSpanned("\n") + } override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this) override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI, this::onException) @@ -220,25 +307,24 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(), override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_USB, this::onException) } @RequiresApi(24) - class Bluetooth(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver { - companion object { - // TODO: migrate to framework Manifest.permission when stable - private const val BLUETOOTH_CONNECT = "android.permission.BLUETOOTH_CONNECT" - } - - private val tethering = BluetoothTethering(parent.requireContext()) { data.notifyChange() } + class Bluetooth(parent: TetheringFragment, adapter: BluetoothAdapter) : + TetherManager(parent), DefaultLifecycleObserver { + private val tethering = BluetoothTethering(parent.requireContext(), adapter) { data.notifyChange() } init { parent.viewLifecycleOwner.lifecycle.addObserver(this) } - fun ensureInit(context: Context) = tethering.ensureInit(context) + fun ensureInit(context: Context) { + tethering.ensureInit(context) + onTetheringStarted() // force flush + } override fun onResume(owner: LifecycleOwner) { - if (!BuildCompat.isAtLeastS() || parent.requireContext().checkSelfPermission(BLUETOOTH_CONNECT) == - PackageManager.PERMISSION_GRANTED) { - ensureInit(parent.requireContext()) - } else if (parent.shouldShowRequestPermissionRationale(BLUETOOTH_CONNECT)) { - parent.requestBluetooth.launch(BLUETOOTH_CONNECT) + if (Build.VERSION.SDK_INT < 31 || parent.requireContext().checkSelfPermission( + Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) { + tethering.ensureInit(parent.requireContext()) + } else if (parent.shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_CONNECT)) { + parent.requestBluetooth.launch(Manifest.permission.BLUETOOTH_CONNECT) } } override fun onDestroy(owner: LifecycleOwner) = tethering.close() @@ -246,17 +332,17 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(), override val title get() = parent.getString(R.string.tethering_manage_bluetooth) override val tetherType get() = TetherType.BLUETOOTH override val type get() = VIEW_TYPE_BLUETOOTH - override val isStarted get() = tethering.active == true + override val isStarted get() = tethering.active override val text get() = listOfNotNull( if (tethering.active == null) tethering.activeFailureCause?.readableMessage else null, baseError).joinToString("\n") - override fun start() = BluetoothTethering.start(this) + override fun start() = tethering.start(this) override fun stop() { - TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, this::onException) - Thread.sleep(1) // give others a room to breathe + tethering.stop(this::onException) onTetheringStarted() // force flush state } + override fun onClickNull() = ManageBar.start(parent.requireContext()) } @RequiresApi(30) class Ethernet(parent: TetheringFragment) : TetherManager(parent) { diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt index 7284ac35..6253db9f 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt @@ -3,6 +3,7 @@ package be.mygod.vpnhotspot.manage import android.annotation.TargetApi +import android.bluetooth.BluetoothManager import android.content.* import android.os.Build import android.os.Bundle @@ -15,6 +16,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.appcompat.widget.Toolbar import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DefaultItemAnimator @@ -34,6 +36,7 @@ import be.mygod.vpnhotspot.root.RootManager import be.mygod.vpnhotspot.root.WifiApCommands import be.mygod.vpnhotspot.util.* import be.mygod.vpnhotspot.widget.SmartSnackbar +import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred import timber.log.Timber @@ -42,16 +45,24 @@ import java.net.NetworkInterface import java.net.SocketException class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickListener { - inner class ManagerAdapter : ListAdapter(Manager) { + inner class ManagerAdapter : ListAdapter(Manager), + TetheringManager.TetheringEventCallback { internal val repeaterManager by lazy { RepeaterManager(this@TetheringFragment) } @get:RequiresApi(26) internal val localOnlyHotspotManager by lazy @TargetApi(26) { LocalOnlyHotspotManager(this@TetheringFragment) } - 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()?.adapter?.let { + TetherManager.Bluetooth(this@TetheringFragment, it) + } else null + } @get:RequiresApi(24) private val tetherManagers by lazy @TargetApi(24) { - listOf(TetherManager.Wifi(this@TetheringFragment), - TetherManager.Usb(this@TetheringFragment), - bluetoothManager) + listOfNotNull( + TetherManager.Wifi(this@TetheringFragment), + TetherManager.Usb(this@TetheringFragment), + bluetoothManager, + ) } @get:RequiresApi(30) private val tetherManagers30 by lazy @TargetApi(30) { @@ -59,30 +70,32 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick TetherManager.Ncm(this@TetheringFragment), TetherManager.WiGig(this@TetheringFragment)) } - private val wifiManagerLegacy by lazy @Suppress("Deprecation") { - TetherManager.WifiLegacy(this@TetheringFragment) - } + private val wifiManagerLegacy by lazy { TetherManager.WifiLegacy(this@TetheringFragment) } - private var enabledIfaces = emptyList() + var activeIfaces = emptyList() + var localOnlyIfaces = emptyList() + var erroredIfaces = emptyList() private var listDeferred = CompletableDeferred>(emptyList()) - private fun updateEnabledTypes() { - this@TetheringFragment.enabledTypes = enabledIfaces.map { TetherType.ofInterface(it) }.toSet() + fun updateEnabledTypes() { + this@TetheringFragment.enabledTypes = + (activeIfaces + localOnlyIfaces).map { TetherType.ofInterface(it) }.toSet() } - suspend fun notifyInterfaceChanged(lastList: List? = null) { - @Suppress("NAME_SHADOWING") val lastList = lastList ?: listDeferred.await() - val first = lastList.indexOfFirst { it is InterfaceManager } - if (first >= 0) notifyItemRangeChanged(first, lastList.indexOfLast { it is InterfaceManager } - first + 1) + val lastErrors = mutableMapOf() + override fun onError(ifName: String, error: Int) { + if (error == 0) lastErrors.remove(ifName) else lastErrors[ifName] = error } + suspend fun notifyTetherTypeChanged() { updateEnabledTypes() val lastList = listDeferred.await() - notifyInterfaceChanged(lastList) - val first = lastList.indexOfLast { it !is TetherManager } + 1 + var first = lastList.indexOfFirst { it is InterfaceManager } + if (first >= 0) notifyItemRangeChanged(first, lastList.indexOfLast { it is InterfaceManager } - first + 1) + first = lastList.indexOfLast { it !is TetherManager } + 1 notifyItemRangeChanged(first, lastList.size - first) } - fun update(activeIfaces: List, localOnlyIfaces: List, erroredIfaces: List) { + fun update() { val deferred = CompletableDeferred>() listDeferred = deferred ifaceLookup = try { @@ -91,8 +104,6 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick Timber.d(e) emptyMap() } - enabledIfaces = activeIfaces + localOnlyIfaces - updateEnabledTypes() val list = ArrayList() if (Services.p2p != null) list.add(repeaterManager) @@ -104,11 +115,11 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick list.add(ManageBar) if (Build.VERSION.SDK_INT >= 24) { list.addAll(tetherManagers) - tetherManagers.forEach { it.updateErrorMessage(erroredIfaces) } + tetherManagers.forEach { it.updateErrorMessage(erroredIfaces, lastErrors) } } if (Build.VERSION.SDK_INT >= 30) { list.addAll(tetherManagers30) - tetherManagers30.forEach { it.updateErrorMessage(erroredIfaces) } + tetherManagers30.forEach { it.updateErrorMessage(erroredIfaces, lastErrors) } } if (Build.VERSION.SDK_INT < 26) { list.add(wifiManagerLegacy) @@ -125,7 +136,10 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick @RequiresApi(29) val startRepeater = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> - if (granted) requireActivity().startForegroundService(Intent(activity, RepeaterService::class.java)) + if (granted) requireActivity().startForegroundService(Intent(activity, RepeaterService::class.java)) else { + Snackbar.make((activity as MainActivity).binding.fragmentHolder, + R.string.repeater_missing_location_permissions, Snackbar.LENGTH_LONG).show() + } } @RequiresApi(26) val startLocalOnlyHotspot = registerForActivityResult(ActivityResultContracts.RequestPermission()) { @@ -133,7 +147,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick } @RequiresApi(31) val requestBluetooth = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> - if (granted) adapter.bluetoothManager.ensureInit(requireContext()) + if (granted) adapter.bluetoothManager!!.ensureInit(requireContext()) } var ifaceLookup: Map = emptyMap() @@ -142,9 +156,12 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick var binder: TetheringService.Binder? = null private val adapter = ManagerAdapter() private val receiver = broadcastReceiver { _, intent -> - adapter.update(intent.tetheredIfaces ?: return@broadcastReceiver, - intent.localOnlyTetheredIfaces ?: return@broadcastReceiver, - intent.getStringArrayListExtra(TetheringManager.EXTRA_ERRORED_TETHER) ?: return@broadcastReceiver) + adapter.activeIfaces = intent.tetheredIfaces ?: return@broadcastReceiver + adapter.localOnlyIfaces = intent.localOnlyTetheredIfaces ?: return@broadcastReceiver + adapter.erroredIfaces = intent.getStringArrayListExtra(TetheringManager.EXTRA_ERRORED_TETHER) + ?: return@broadcastReceiver + adapter.updateEnabledTypes() + adapter.update() } private fun updateMonitorList(canMonitor: List = 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(this) { which, ret -> if (which == DialogInterface.BUTTON_POSITIVE) viewLifecycleOwner.lifecycleScope.launchWhenCreated { val configuration = ret!!.configuration @@ -251,7 +268,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick binding.interfaces.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) binding.interfaces.itemAnimator = DefaultItemAnimator() binding.interfaces.adapter = adapter - adapter.update(emptyList(), emptyList(), emptyList()) + adapter.update() ServiceForegroundConnector(this, this, TetheringService::class) (activity as MainActivity).binding.toolbar.apply { inflateMenu(R.menu.toolbar_tethering) @@ -275,19 +292,22 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick override fun onServiceConnected(name: ComponentName?, service: IBinder?) { binder = service as TetheringService.Binder - service.routingsChanged[this] = { - lifecycleScope.launchWhenStarted { adapter.notifyInterfaceChanged() } - } + service.routingsChanged[this] = { lifecycleScope.launchWhenStarted { adapter.update() } } requireContext().registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED)) - if (Build.VERSION.SDK_INT >= 30) TetherType.listener[this] = { - lifecycleScope.launchWhenStarted { adapter.notifyTetherTypeChanged() } + if (Build.VERSION.SDK_INT >= 30) { + TetheringManager.registerTetheringEventCallback(null, adapter) + TetherType.listener[this] = { lifecycleScope.launchWhenStarted { adapter.notifyTetherTypeChanged() } } } } override fun onServiceDisconnected(name: ComponentName?) { (binder ?: return).routingsChanged -= this binder = null - if (Build.VERSION.SDK_INT >= 30) TetherType.listener -= this + if (Build.VERSION.SDK_INT >= 30) { + TetherType.listener -= this + TetheringManager.unregisterTetheringEventCallback(adapter) + adapter.lastErrors.clear() + } requireContext().unregisterReceiver(receiver) } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringTileService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringTileService.kt index 6483beea..18442497 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringTileService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringTileService.kt @@ -1,5 +1,6 @@ package be.mygod.vpnhotspot.manage +import android.bluetooth.BluetoothManager import android.content.ComponentName import android.content.Context import android.content.Intent @@ -11,6 +12,7 @@ import android.service.quicksettings.Tile import android.widget.Toast import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.TetheringService import be.mygod.vpnhotspot.net.TetherType @@ -151,15 +153,16 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin override val labelString get() = R.string.tethering_manage_bluetooth override val tetherType get() = TetherType.BLUETOOTH - override fun start() = BluetoothTethering.start(this) + override fun start() = tethering!!.start(this) override fun stop() { - TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, this::onException) - Thread.sleep(1) // give others a room to breathe + tethering!!.stop(this::onException) onTetheringStarted() // force flush state } override fun onStartListening() { - tethering = BluetoothTethering(this) { updateTile() } + tethering = getSystemService()?.adapter?.let { + BluetoothTethering(this, it) { updateTile() } + } super.onStartListening() } override fun onStopListening() { @@ -187,7 +190,7 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin icon = tileOff } null -> { - state = Tile.STATE_UNAVAILABLE + state = Tile.STATE_INACTIVE icon = tileOff subtitle(tethering?.activeFailureCause?.readableMessage) } @@ -198,7 +201,8 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin } override fun onClick() { - when (tethering?.active) { + val tethering = tethering + if (tethering == null) tapPending = true else when (tethering.active) { true -> { val binder = binder if (binder == null) tapPending = true else { @@ -212,7 +216,7 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin } } false -> start() - else -> tapPending = true + else -> ManageBar.start(this) } } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/TetherType.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/TetherType.kt index 18b6ef72..aad28004 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/TetherType.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/TetherType.kt @@ -71,9 +71,9 @@ enum class TetherType(@DrawableRes val icon: Int) { } @RequiresApi(30) - override fun onTetherableInterfaceRegexpsChanged(args: Array?) = synchronized(this) { + override fun onTetherableInterfaceRegexpsChanged(reg: Any?) = synchronized(this) { if (requiresUpdate) return@synchronized - Timber.i("onTetherableInterfaceRegexpsChanged: ${args?.contentDeepToString()}") + Timber.i("onTetherableInterfaceRegexpsChanged: $reg") TetheringManager.unregisterTetheringEventCallback(this) requiresUpdate = true listener() diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt index 5d32771d..8fc28bec 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt @@ -7,7 +7,6 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager -import android.content.pm.ResolveInfo import android.net.ConnectivityManager import android.net.Network import android.os.Build @@ -67,7 +66,11 @@ object TetheringManager { } private object InPlaceExecutor : Executor { - override fun execute(command: Runnable) = command.run() + override fun execute(command: Runnable) = try { + command.run() + } catch (e: Exception) { + Timber.w(e) // prevent Binder stub swallowing the exception + } } /** @@ -135,7 +138,7 @@ object TetheringManager { * Requires MANAGE_USB permission, unfortunately. * * Source: https://android.googlesource.com/platform/frameworks/base/+/7ca5d3a/services/usb/java/com/android/server/usb/UsbService.java#389 - * @see [startTethering]. + * @see startTethering */ @RequiresApi(24) const val TETHERING_USB = 1 @@ -143,14 +146,14 @@ object TetheringManager { * Bluetooth tethering type. * * Requires BLUETOOTH permission. - * @see [startTethering]. + * @see startTethering */ @RequiresApi(24) const val TETHERING_BLUETOOTH = 2 /** * Ncm local tethering type. * - * @see [startTethering] + * @see startTethering */ @RequiresApi(30) const val TETHERING_NCM = 4 @@ -158,7 +161,7 @@ object TetheringManager { * Ethernet tethering type. * * Requires MANAGE_USB permission, also. - * @see [startTethering] + * @see startTethering */ @RequiresApi(30) const val TETHERING_ETHERNET = 5 @@ -248,13 +251,12 @@ object TetheringManager { val proxy = ProxyBuilder.forClass(classOnStartTetheringCallback).apply { dexCache(cacheDir) handler { proxy, method, args -> - if (args.isNotEmpty()) Timber.w("Unexpected args for ${method.name}: $args") @Suppress("NAME_SHADOWING") val callback = reference.get() - when (method.name) { - "onTetheringStarted" -> callback?.onTetheringStarted() - "onTetheringFailed" -> callback?.onTetheringFailed() - else -> ProxyBuilder.callSuper(proxy, method, args) + if (args.isEmpty()) when (method.name) { + "onTetheringStarted" -> return@handler callback?.onTetheringStarted() + "onTetheringFailed" -> return@handler callback?.onTetheringFailed() } + ProxyBuilder.callSuper(proxy, method, args) } }.build() startTetheringLegacy(Services.connectivity, type, showProvisioningUi, proxy, handler) @@ -276,13 +278,9 @@ object TetheringManager { arrayOf(interfaceStartTetheringCallback), object : InvocationHandler { override fun invoke(proxy: Any, method: Method, args: Array?): Any? { @Suppress("NAME_SHADOWING") val callback = reference.get() - return when (val name = method.name) { - "onTetheringStarted" -> { - if (!args.isNullOrEmpty()) Timber.w("Unexpected args for $name: $args") - callback?.onTetheringStarted() - } - "onTetheringFailed" -> { - if (args?.size != 1) Timber.w("Unexpected args for $name: $args") + return when { + method.matches("onTetheringStarted") -> callback?.onTetheringStarted() + method.matches("onTetheringFailed", Integer.TYPE) -> { callback?.onTetheringFailed(args?.get(0) as Int) } else -> callSuper(interfaceStartTetheringCallback, proxy, method, args) @@ -446,7 +444,7 @@ object TetheringManager { * *@param reg The new regular expressions. * @hide */ - fun onTetherableInterfaceRegexpsChanged(args: Array?) {} + fun onTetherableInterfaceRegexpsChanged(reg: Any?) {} /** * Called when there was a change in the list of tetherable interfaces. Tetherable @@ -542,40 +540,34 @@ object TetheringManager { override fun invoke(proxy: Any, method: Method, args: Array?): Any? { @Suppress("NAME_SHADOWING") val callback = reference.get() - val noArgs = args?.size ?: 0 - return when (val name = method.name) { - "onTetheringSupported" -> { - if (noArgs != 1) Timber.w("Unexpected args for $name: $args") + return when { + method.matches("onTetheringSupported", Boolean::class.java) -> { callback?.onTetheringSupported(args!![0] as Boolean) } - "onUpstreamChanged" -> { - if (noArgs != 1) Timber.w("Unexpected args for $name: $args") + method.matches1("onUpstreamChanged") -> { callback?.onUpstreamChanged(args!![0] as Network?) } - "onTetherableInterfaceRegexpsChanged" -> { - if (regexpsSent) callback?.onTetherableInterfaceRegexpsChanged(args) + method.name == "onTetherableInterfaceRegexpsChanged" && + method.parameters.singleOrNull()?.type?.name == + "android.net.TetheringManager\$TetheringInterfaceRegexps" -> { + if (regexpsSent) callback?.onTetherableInterfaceRegexpsChanged(args!!.single()) regexpsSent = true } - "onTetherableInterfacesChanged" -> { - if (noArgs != 1) Timber.w("Unexpected args for $name: $args") + method.matches1>("onTetherableInterfacesChanged") -> { @Suppress("UNCHECKED_CAST") callback?.onTetherableInterfacesChanged(args!![0] as List) } - "onTetheredInterfacesChanged" -> { - if (noArgs != 1) Timber.w("Unexpected args for $name: $args") + method.matches1>("onTetheredInterfacesChanged") -> { @Suppress("UNCHECKED_CAST") callback?.onTetheredInterfacesChanged(args!![0] as List) } - "onError" -> { - if (noArgs != 2) Timber.w("Unexpected args for $name: $args") + method.matches("onError", String::class.java, Integer.TYPE) -> { callback?.onError(args!![0] as String, args[1] as Int) } - "onClientsChanged" -> { - if (noArgs != 1) Timber.w("Unexpected args for $name: $args") + method.matches1>("onClientsChanged") -> { callback?.onClientsChanged(args!![0] as Collection<*>) } - "onOffloadStatusChanged" -> { - if (noArgs != 1) Timber.w("Unexpected args for $name: $args") + method.matches("onOffloadStatusChanged", Integer.TYPE) -> { callback?.onOffloadStatusChanged(args!![0] as Int) } else -> callSuper(interfaceTetheringEventCallback, proxy, method, args) @@ -629,6 +621,7 @@ object TetheringManager { * @return error The error code of the last error tethering or untethering the named * interface */ + @Deprecated("Use {@link TetheringEventCallback#onError(String, int)} instead.") fun getLastTetherError(iface: String): Int = getLastTetherError(Services.connectivity, iface) as Int val tetherErrorLookup = ConstantLookup("TETHER_ERROR_", diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/DefaultNetworkMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/DefaultNetworkMonitor.kt index c6548a16..6cbe6661 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/DefaultNetworkMonitor.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/DefaultNetworkMonitor.kt @@ -6,7 +6,10 @@ import android.net.LinkProperties import android.net.Network import android.net.NetworkCapabilities import android.os.Build +import android.os.Handler +import android.os.Looper import be.mygod.vpnhotspot.util.Services +import be.mygod.vpnhotspot.util.globalNetworkRequestBuilder import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -18,10 +21,10 @@ object DefaultNetworkMonitor : UpstreamMonitor() { * Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1: * https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e */ - private val networkRequest = networkRequestBuilder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) - .build() + private val networkRequest = globalNetworkRequestBuilder().apply { + addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) + }.build() private val networkCallback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { val properties = Services.connectivity.getLinkProperties(network) @@ -51,15 +54,22 @@ object DefaultNetworkMonitor : UpstreamMonitor() { callback.onAvailable(currentLinkProperties) } } else { - if (Build.VERSION.SDK_INT in 24..27) @TargetApi(24) { - Services.connectivity.registerDefaultNetworkCallback(networkCallback) - } else try { - Services.connectivity.requestNetwork(networkRequest, networkCallback) - } catch (e: RuntimeException) { - // SecurityException would be thrown in requestNetwork on Android 6.0 thanks to Google's stupid bug - if (Build.VERSION.SDK_INT != 23) throw e - GlobalScope.launch { callback.onFallback() } - return + when (Build.VERSION.SDK_INT) { + in 31..Int.MAX_VALUE -> @TargetApi(31) { + Services.connectivity.registerBestMatchingNetworkCallback(networkRequest, networkCallback, + Handler(Looper.getMainLooper())) + } + in 24..27 -> @TargetApi(24) { + Services.connectivity.registerDefaultNetworkCallback(networkCallback) + } + else -> try { + Services.connectivity.requestNetwork(networkRequest, networkCallback) + } catch (e: 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 } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/InterfaceMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/InterfaceMonitor.kt index 41b17b2b..e9c9913a 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/InterfaceMonitor.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/InterfaceMonitor.kt @@ -6,6 +6,7 @@ import android.net.Network import android.net.NetworkCapabilities import be.mygod.vpnhotspot.util.Services import be.mygod.vpnhotspot.util.allInterfaceNames +import be.mygod.vpnhotspot.util.globalNetworkRequestBuilder import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import timber.log.Timber @@ -18,7 +19,7 @@ class InterfaceMonitor(private val ifaceRegex: String) : UpstreamMonitor() { Timber.d(e); { it == ifaceRegex } } - private val request = networkRequestBuilder().apply { + private val request = globalNetworkRequestBuilder().apply { removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED) removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TetherTimeoutMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TetherTimeoutMonitor.kt index 75629031..abba1855 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TetherTimeoutMonitor.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TetherTimeoutMonitor.kt @@ -46,7 +46,7 @@ class TetherTimeoutMonitor(private val timeout: Long = 0, val info = WifiApManager.resolvedActivity.activityInfo val resources = app.packageManager.getResourcesForApplication(info.applicationInfo) resources.getInteger(resources.findIdentifier("config_wifiFrameworkSoftApShutDownTimeoutMilliseconds", - "integer", "com.android.wifi.resources", info.packageName)) + "integer", WifiApManager.RESOURCES_PACKAGE, info.packageName)) } } catch (e: RuntimeException) { Timber.w(e) diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/UpstreamMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/UpstreamMonitor.kt index c8629496..9f872c43 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/UpstreamMonitor.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/UpstreamMonitor.kt @@ -2,9 +2,6 @@ package be.mygod.vpnhotspot.net.monitor import android.content.SharedPreferences import android.net.LinkProperties -import android.net.NetworkCapabilities -import android.net.NetworkRequest -import android.os.Build import be.mygod.vpnhotspot.App.Companion.app import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -23,13 +20,6 @@ abstract class UpstreamMonitor { } private var monitor = generateMonitor() - fun networkRequestBuilder() = NetworkRequest.Builder().apply { - if (Build.VERSION.SDK_INT == 23) { // workarounds for OEM bugs - removeCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - removeCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL) - } - } - fun registerCallback(callback: Callback) = synchronized(this) { monitor.registerCallback(callback) } fun unregisterCallback(callback: Callback) = synchronized(this) { monitor.unregisterCallback(callback) } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/VpnMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/VpnMonitor.kt index 03ef2575..880ba10b 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/VpnMonitor.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/VpnMonitor.kt @@ -5,15 +5,16 @@ import android.net.LinkProperties import android.net.Network import android.net.NetworkCapabilities import be.mygod.vpnhotspot.util.Services +import be.mygod.vpnhotspot.util.globalNetworkRequestBuilder import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import timber.log.Timber object VpnMonitor : UpstreamMonitor() { - private val request = networkRequestBuilder() - .addTransportType(NetworkCapabilities.TRANSPORT_VPN) - .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) - .build() + private val request = globalNetworkRequestBuilder().apply { + addTransportType(NetworkCapabilities.TRANSPORT_VPN) + removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) + }.build() private var registered = false private val available = HashMap() diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/P2pSupplicantConfiguration.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/P2pSupplicantConfiguration.kt index 00f22e56..d291ac87 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/P2pSupplicantConfiguration.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/P2pSupplicantConfiguration.kt @@ -75,7 +75,13 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) { if (matchedBssid.isEmpty()) { check(block.pskLine == null && block.psk == null) if (match.groups[5] != null) { - block.psk = match.groupValues[5].apply { check(length in 8..63) } + block.psk = match.groupValues[5].apply { + when (length) { + in 8..63 -> { } + 64 -> error("WPA-PSK hex not supported") + else -> error("Unknown length $length") + } + } } block.pskLine = block.size } else if (bssids.any { matchedBssid.equals(it, true) }) { diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApCapability.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApCapability.kt new file mode 100644 index 00000000..dbed4cc0 --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApCapability.kt @@ -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 +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt index b6ff123e..fc4ee751 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt @@ -6,11 +6,15 @@ import android.net.MacAddress import android.net.wifi.SoftApConfiguration import android.os.Build import android.os.Parcelable +import android.util.SparseIntArray import androidx.annotation.RequiresApi import be.mygod.vpnhotspot.net.MacAddressCompat import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toCompat import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor +import be.mygod.vpnhotspot.util.ConstantLookup +import be.mygod.vpnhotspot.util.UnblockCentral import kotlinx.parcelize.Parcelize +import timber.log.Timber @Parcelize data class SoftApConfigurationCompat( @@ -19,13 +23,18 @@ data class SoftApConfigurationCompat( var bssidAddr: Long? = null, var passphrase: String? = null, var isHiddenSsid: Boolean = false, + /** + * To read legacy band/channel pair, use [requireSingleBand]. For easy access, see [getChannel]. + * + * You should probably set or modify this field directly only when you want to use bridged AP, + * see also [android.net.wifi.WifiManager.isBridgedApConcurrencySupported]. + * Otherwise, use [optimizeChannels] or [setChannel]. + */ @TargetApi(23) - var band: Int = BAND_2GHZ, - @TargetApi(23) - var channel: Int = 0, + var channels: SparseIntArray = SparseIntArray(1).apply { append(BAND_2GHZ, 0) }, + var securityType: Int = SoftApConfiguration.SECURITY_TYPE_OPEN, @TargetApi(30) var maxNumberOfClients: Int = 0, - var securityType: Int = SoftApConfiguration.SECURITY_TYPE_OPEN, @TargetApi(28) var isAutoShutdownEnabled: Boolean = true, @TargetApi(28) @@ -36,12 +45,41 @@ data class SoftApConfigurationCompat( var blockedClientList: List = emptyList(), @RequiresApi(30) var allowedClientList: List = emptyList(), + @TargetApi(31) + var macRandomizationSetting: Int = RANDOMIZATION_PERSISTENT, + @TargetApi(31) + var isBridgedModeOpportunisticShutdownEnabled: Boolean = true, + @TargetApi(31) + var isIeee80211axEnabled: Boolean = true, + @TargetApi(31) + var isUserConfiguration: Boolean = true, var underlying: Parcelable? = null) : Parcelable { companion object { const val BAND_2GHZ = 1 const val BAND_5GHZ = 2 + @TargetApi(30) const val BAND_6GHZ = 4 - const val BAND_ANY = 7 + @TargetApi(31) + const val BAND_60GHZ = 8 + const val BAND_LEGACY = BAND_2GHZ or BAND_5GHZ + val BAND_TYPES by lazy { + if (Build.VERSION.SDK_INT >= 31) try { + return@lazy UnblockCentral.SoftApConfiguration_BAND_TYPES + } catch (e: ReflectiveOperationException) { + Timber.w(e) + } + intArrayOf(BAND_2GHZ, BAND_5GHZ, BAND_6GHZ, BAND_60GHZ) + } + @RequiresApi(31) + val bandLookup = ConstantLookup("BAND_") + + @TargetApi(31) + const val RANDOMIZATION_NONE = 0 + @TargetApi(31) + const val RANDOMIZATION_PERSISTENT = 1 + + fun isLegacyEitherBand(band: Int) = band and BAND_LEGACY == BAND_LEGACY + /** * [android.net.wifi.WifiConfiguration.KeyMgmt.WPA2_PSK] */ @@ -53,7 +91,8 @@ data class SoftApConfigurationCompat( /** * Based on: - * https://elixir.bootlin.com/linux/v5.7.6/source/net/wireless/util.c#L75 + * https://elixir.bootlin.com/linux/v5.12.8/source/net/wireless/util.c#L75 + * TODO: update for Android 12 * https://cs.android.com/android/platform/superproject/+/master:frameworks/base/wifi/java/android/net/wifi/ScanResult.java;l=624;drc=f7ccda05642b55700d67a288462bada488fc7f5e */ fun channelToFrequency(band: Int, chan: Int) = when (band) { @@ -67,18 +106,24 @@ data class SoftApConfigurationCompat( in 1..Int.MAX_VALUE -> 5000 + chan * 5 else -> throw IllegalArgumentException("Invalid 5GHz channel $chan") } - BAND_6GHZ -> if (chan in 1..253) { - 5940 + chan * 5 - } else throw IllegalArgumentException("Invalid 6GHz channel $chan") - // BAND_60GHZ -> if (chan in 1 until 7) 56160 + chan * 2160 + BAND_6GHZ -> when (chan) { + 2 -> 5935 + in 1..233 -> 5950 + chan * 5 + else -> throw IllegalArgumentException("Invalid 6GHz channel $chan") + } + BAND_60GHZ -> { + require(chan in 1 until 7) { "Invalid 60GHz channel $chan" } + 56160 + chan * 2160 + } else -> throw IllegalArgumentException("Invalid band $band") } fun frequencyToChannel(freq: Int) = when (freq) { 2484 -> 14 in Int.MIN_VALUE until 2484 -> (freq - 2407) / 5 in 4910..4980 -> (freq - 4000) / 5 - in Int.MIN_VALUE until 5945 -> (freq - 5000) / 5 - in Int.MIN_VALUE..45000 -> (freq - 5940) / 5 + in Int.MIN_VALUE until 5925 -> (freq - 5000) / 5 + 5935 -> 2 + in Int.MIN_VALUE..45000 -> (freq - 5950) / 5 in 58320..70200 -> (freq - 56160) / 2160 else -> throw IllegalArgumentException("Invalid frequency $freq") } @@ -122,6 +167,14 @@ data class SoftApConfigurationCompat( private val getChannel by lazy @TargetApi(30) { SoftApConfiguration::class.java.getDeclaredMethod("getChannel") } + @get:RequiresApi(31) + private val getChannels by lazy @TargetApi(31) { + SoftApConfiguration::class.java.getDeclaredMethod("getChannels") + } + @get:RequiresApi(31) + private val getMacRandomizationSetting by lazy @TargetApi(31) { + SoftApConfiguration::class.java.getDeclaredMethod("getMacRandomizationSetting") + } @get:RequiresApi(30) private val getMaxNumberOfClients by lazy @TargetApi(30) { SoftApConfiguration::class.java.getDeclaredMethod("getMaxNumberOfClients") @@ -134,10 +187,22 @@ data class SoftApConfigurationCompat( private val isAutoShutdownEnabled by lazy @TargetApi(30) { SoftApConfiguration::class.java.getDeclaredMethod("isAutoShutdownEnabled") } + @get:RequiresApi(31) + private val isBridgedModeOpportunisticShutdownEnabled by lazy @TargetApi(31) { + SoftApConfiguration::class.java.getDeclaredMethod("isBridgedModeOpportunisticShutdownEnabled") + } @get:RequiresApi(30) private val isClientControlByUserEnabled by lazy @TargetApi(30) { SoftApConfiguration::class.java.getDeclaredMethod("isClientControlByUserEnabled") } + @get:RequiresApi(31) + private val isIeee80211axEnabled by lazy @TargetApi(31) { + SoftApConfiguration::class.java.getDeclaredMethod("isIeee80211axEnabled") + } + @get:RequiresApi(31) + private val isUserConfiguration by lazy @TargetApi(31) { + SoftApConfiguration::class.java.getDeclaredMethod("isUserConfiguration") + } @get:RequiresApi(30) private val classBuilder by lazy { Class.forName("android.net.wifi.SoftApConfiguration\$Builder") } @@ -159,6 +224,10 @@ data class SoftApConfigurationCompat( private val setBlockedClientList by lazy { classBuilder.getDeclaredMethod("setBlockedClientList", java.util.List::class.java) } + @get:RequiresApi(31) + private val setBridgedModeOpportunisticShutdownEnabled by lazy { + classBuilder.getDeclaredMethod("setBridgedModeOpportunisticShutdownEnabled", Boolean::class.java) + } @get:RequiresApi(30) private val setBssid by lazy @TargetApi(30) { classBuilder.getDeclaredMethod("setBssid", MacAddress::class.java) @@ -167,12 +236,24 @@ data class SoftApConfigurationCompat( private val setChannel by lazy { classBuilder.getDeclaredMethod("setChannel", Int::class.java, Int::class.java) } + @get:RequiresApi(31) + private val setChannels by lazy { + classBuilder.getDeclaredMethod("setChannels", SparseIntArray::class.java) + } @get:RequiresApi(30) private val setClientControlByUserEnabled by lazy { classBuilder.getDeclaredMethod("setClientControlByUserEnabled", Boolean::class.java) } @get:RequiresApi(30) private val setHiddenSsid by lazy { classBuilder.getDeclaredMethod("setHiddenSsid", Boolean::class.java) } + @get:RequiresApi(31) + private val setIeee80211axEnabled by lazy { + classBuilder.getDeclaredMethod("setIeee80211axEnabled", Boolean::class.java) + } + @get:RequiresApi(31) + private val setMacRandomizationSetting by lazy { + classBuilder.getDeclaredMethod("setMacRandomizationSetting", Int::class.java) + } @get:RequiresApi(30) private val setMaxNumberOfClients by lazy { classBuilder.getDeclaredMethod("setMaxNumberOfClients", Int::class.java) @@ -187,6 +268,8 @@ data class SoftApConfigurationCompat( } @get:RequiresApi(30) private val setSsid by lazy { classBuilder.getDeclaredMethod("setSsid", String::class.java) } + @get:RequiresApi(31) + private val setUserConfiguration by lazy @TargetApi(31) { UnblockCentral.setUserConfiguration(classBuilder) } @Deprecated("Class deprecated in framework") @Suppress("DEPRECATION") @@ -196,14 +279,14 @@ data class SoftApConfigurationCompat( preSharedKey, hiddenSSID, // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/wifi/java/android/net/wifi/SoftApConfToXmlMigrationUtil.java;l=87;drc=aa6527cf41671d1ed417b8ebdb6b3aa614f62344 - if (Build.VERSION.SDK_INT >= 23) when (val band = apBand.getInt(this)) { - 0 -> BAND_2GHZ - 1 -> BAND_5GHZ - -1 -> BAND_2GHZ or BAND_5GHZ - else -> throw IllegalArgumentException("Unexpected band $band") - } else BAND_ANY, - if (Build.VERSION.SDK_INT >= 23) apChannel.getInt(this) else 0, - 0, + SparseIntArray(1).also { + if (Build.VERSION.SDK_INT >= 23) it.append(when (val band = apBand.getInt(this)) { + 0 -> BAND_2GHZ + 1 -> BAND_5GHZ + -1 -> BAND_LEGACY + else -> throw IllegalArgumentException("Unexpected band $band") + }, apChannel.getInt(this)) else it.append(BAND_LEGACY, 0) + }, allowedKeyManagement.nextSetBit(0).let { selected -> require(allowedKeyManagement.nextSetBit(selected + 1) < 0) { "More than 1 key managements supplied: $allowedKeyManagement" @@ -224,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) @RequiresApi(30) @Suppress("UNCHECKED_CAST") fun SoftApConfiguration.toCompat() = SoftApConfigurationCompat( - ssid, - bssid?.toCompat()?.addr, - passphrase, - isHiddenSsid, - getBand(this) as Int, - getChannel(this) as Int, - getMaxNumberOfClients(this) as Int, - securityType, - isAutoShutdownEnabled(this) as Boolean, - getShutdownTimeoutMillis(this) as Long, - isClientControlByUserEnabled(this) as Boolean, - getBlockedClientList(this) as List, - getAllowedClientList(this) as List, - this) + ssid, + bssid?.toCompat()?.addr, + passphrase, + isHiddenSsid, + if (Build.VERSION.SDK_INT >= 31) getChannels(this) as SparseIntArray else SparseIntArray(1).also { + it.append(getBand(this) as Int, getChannel(this) as Int) + }, + securityType, + getMaxNumberOfClients(this) as Int, + isAutoShutdownEnabled(this) as Boolean, + getShutdownTimeoutMillis(this) as Long, + isClientControlByUserEnabled(this) as Boolean, + getBlockedClientList(this) as List, + getAllowedClientList(this) as List, + if (Build.VERSION.SDK_INT >= 31) getMacRandomizationSetting(this) as Int else RANDOMIZATION_PERSISTENT, + Build.VERSION.SDK_INT < 31 || isBridgedModeOpportunisticShutdownEnabled(this) as Boolean, + Build.VERSION.SDK_INT < 31 || isIeee80211axEnabled(this) as Boolean, + Build.VERSION.SDK_INT < 31 || isUserConfiguration(this) as Boolean, + this, + ) } @Suppress("DEPRECATION") @@ -253,6 +342,47 @@ data class SoftApConfigurationCompat( bssidAddr = value?.addr } + /** + * Only single band/channel can be supplied on API 23-30 + */ + fun requireSingleBand(): Pair { + require(channels.size() == 1) { "Unsupported number of bands configured" } + return channels.keyAt(0) to channels.valueAt(0) + } + fun getChannel(band: Int): Int { + var result = -1 + repeat(channels.size()) { i -> + if (band and channels.keyAt(i) != band) return@repeat + require(result == -1) { "Duplicate band found" } + result = channels.valueAt(i) + } + return result + } + fun setChannel(channel: Int, band: Int = BAND_LEGACY) { + channels = SparseIntArray(1).apply { + append(when { + channel <= 0 || band != BAND_LEGACY -> band + channel > 14 -> BAND_5GHZ + else -> BAND_2GHZ + }, channel) + } + } + fun optimizeChannels(channels: SparseIntArray = this.channels) { + this.channels = SparseIntArray(channels.size()).apply { + var setBand = 0 + repeat(channels.size()) { i -> if (channels.valueAt(i) == 0) setBand = setBand or channels.keyAt(i) } + if (setBand != 0) append(setBand, 0) // merge all bands into one + repeat(channels.size()) { i -> + val band = channels.keyAt(i) + if (band and setBand == 0) put(band, channels.valueAt(i)) + } + } + } + + fun setMacRandomizationEnabled(enabled: Boolean) { + macRandomizationSetting = if (enabled) RANDOMIZATION_PERSISTENT else RANDOMIZATION_NONE + } + /** * Based on: * https://android.googlesource.com/platform/packages/apps/Settings/+/android-5.0.0_r1/src/com/android/settings/wifi/WifiApDialog.java#88 @@ -263,6 +393,7 @@ data class SoftApConfigurationCompat( @Deprecated("Class deprecated in framework, use toPlatform().toWifiConfiguration()") @Suppress("DEPRECATION") fun toWifiConfiguration(): android.net.wifi.WifiConfiguration { + val (band, channel) = requireSingleBand() val wc = underlying as? android.net.wifi.WifiConfiguration val result = if (wc == null) android.net.wifi.WifiConfiguration() else android.net.wifi.WifiConfiguration(wc) val original = wc?.toCompat() @@ -273,11 +404,14 @@ data class SoftApConfigurationCompat( apBand.setInt(result, when (band) { BAND_2GHZ -> 0 BAND_5GHZ -> 1 - BAND_2GHZ or BAND_5GHZ, BAND_ANY -> -1 - else -> throw IllegalArgumentException("Convert fail, unsupported band setting :$band") + else -> { + require(Build.VERSION.SDK_INT >= 28) { "A band must be specified on this platform" } + require(isLegacyEitherBand(band)) { "Convert fail, unsupported band setting :$band" } + -1 + } }) apChannel.setInt(result, channel) - } else require(band == BAND_ANY) { "Specifying band is unsupported on this platform" } + } else require(isLegacyEitherBand(band)) { "Specifying band is unsupported on this platform" } if (original?.securityType != securityType) { result.allowedKeyManagement.clear() result.allowedKeyManagement.set(when (securityType) { @@ -304,7 +438,10 @@ data class SoftApConfigurationCompat( setSsid(builder, ssid) setPassphrase(builder, if (securityType == SoftApConfiguration.SECURITY_TYPE_OPEN) null else passphrase, securityType) - if (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()) setMaxNumberOfClients(builder, maxNumberOfClients) setShutdownTimeoutMillis(builder, shutdownTimeoutMillis) @@ -313,6 +450,16 @@ data class SoftApConfigurationCompat( setHiddenSsid(builder, isHiddenSsid) setAllowedClientList(builder, allowedClientList) setBlockedClientList(builder, blockedClientList) + if (Build.VERSION.SDK_INT >= 31) { + setMacRandomizationSetting(builder, macRandomizationSetting) + setBridgedModeOpportunisticShutdownEnabled(builder, isBridgedModeOpportunisticShutdownEnabled) + setIeee80211axEnabled(builder, isIeee80211axEnabled) + if (sac?.let { isUserConfiguration(it) as Boolean } != false != isUserConfiguration) try { + setUserConfiguration(builder, isUserConfiguration) + } catch (e: ReflectiveOperationException) { + Timber.w(e) // as far as we are concerned, this field is not used anywhere so ignore for now + } + } return build(builder) as SoftApConfiguration } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApInfo.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApInfo.kt new file mode 100644 index 00000000..167534d5 --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApInfo.kt @@ -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 +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt index cb23f5b7..8b4b412c 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt @@ -10,10 +10,13 @@ import android.os.Parcelable import android.text.Editable import android.text.TextWatcher import android.util.Base64 +import android.util.SparseIntArray import android.view.MenuItem import android.view.View import android.widget.AdapterView import android.widget.ArrayAdapter +import android.widget.Spinner +import android.widget.Toast import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.Toolbar @@ -30,8 +33,8 @@ import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor import be.mygod.vpnhotspot.util.QRCodeDialog import be.mygod.vpnhotspot.util.readableMessage import be.mygod.vpnhotspot.util.showAllowingStateLoss -import be.mygod.vpnhotspot.widget.SmartSnackbar import kotlinx.parcelize.Parcelize +import timber.log.Timber /** * Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/39b4674/src/com/android/settings/wifi/WifiApDialog.java @@ -40,27 +43,30 @@ import kotlinx.parcelize.Parcelize * Related: https://android.googlesource.com/platform/packages/apps/Settings/+/defb1183ecb00d6231bac7d934d07f58f90261ea */ class WifiApDialogFragment : AlertDialogFragment(), TextWatcher, - Toolbar.OnMenuItemClickListener { + Toolbar.OnMenuItemClickListener, AdapterView.OnItemSelectedListener { companion object { private const val BASE64_FLAGS = Base64.NO_PADDING or Base64.NO_WRAP private val nonMacChars = "[^0-9a-fA-F:]+".toRegex() - private val channels by lazy { - val list = ArrayList() - for (chan in 1..14) list.add(BandOption.Channel(SoftApConfigurationCompat.BAND_2GHZ, chan)) - for (chan in 1..196) list.add(BandOption.Channel(SoftApConfigurationCompat.BAND_5GHZ, chan)) - if (Build.VERSION.SDK_INT >= 30) { - for (chan in 1..253) list.add(BandOption.Channel(SoftApConfigurationCompat.BAND_6GHZ, chan)) - } - list + private val baseOptions by lazy { listOf(ChannelOption.Disabled, ChannelOption.Auto) } + private val channels2G by lazy { + baseOptions + (1..14).map { ChannelOption(it, SoftApConfigurationCompat.BAND_2GHZ) } + } + private val channels5G by lazy { + baseOptions + (1..196).map { ChannelOption(it, SoftApConfigurationCompat.BAND_5GHZ) } + } + @get:RequiresApi(30) + private val channels6G by lazy { + baseOptions + (1..233).map { ChannelOption(it, SoftApConfigurationCompat.BAND_6GHZ) } + } + @get:RequiresApi(31) + private val channels60G by lazy { + baseOptions + (1..6).map { ChannelOption(it, SoftApConfigurationCompat.BAND_60GHZ) } } /** * Source: https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/c2fc6a1/service/java/com/android/server/wifi/p2p/SupplicantP2pIfaceHal.java#1396 */ private val p2pChannels by lazy { - (1..165).map { - val band = if (it <= 14) SoftApConfigurationCompat.BAND_2GHZ else SoftApConfigurationCompat.BAND_5GHZ - BandOption.Channel(band, it) - } + baseOptions + (15..165).map { ChannelOption(it, SoftApConfigurationCompat.BAND_5GHZ) } } } @@ -73,35 +79,20 @@ class WifiApDialogFragment : AlertDialogFragment private lateinit var base: SoftApConfigurationCompat private var started = false + private val currentChannels5G get() = if (arg.p2pMode && !RepeaterService.safeMode) p2pChannels else channels5G override val ret get() = Arg(generateConfig()) private fun generateConfig(full: Boolean = true) = base.copy( @@ -117,9 +108,17 @@ class WifiApDialogFragment : AlertDialogFragment= 23 || arg.p2pMode) { - val bandOption = dialogView.band.selectedItem as BandOption - band = bandOption.band - channel = bandOption.channel + val channels = SparseIntArray(4) + for ((band, spinner) in arrayOf(SoftApConfigurationCompat.BAND_2GHZ to dialogView.band2G, + SoftApConfigurationCompat.BAND_5GHZ to dialogView.band5G, + SoftApConfigurationCompat.BAND_6GHZ to dialogView.band6G, + SoftApConfigurationCompat.BAND_60GHZ to dialogView.band60G)) { + val channel = (spinner.selectedItem as ChannelOption?)?.channel + if (channel != null && channel >= 0) channels.append(band, channel) + } + if (!arg.p2pMode && Build.VERSION.SDK_INT >= 31 && dialogView.bridgedMode.isChecked) { + this.channels = channels + } else optimizeChannels(channels) } bssid = if (dialogView.bssid.length() != 0) { MacAddressCompat.fromString(dialogView.bssid.text.toString()) @@ -132,6 +131,10 @@ class WifiApDialogFragment : AlertDialogFragment= 23 || arg.p2pMode) dialogView.band.apply { - bandOptions = mutableListOf().apply { - if (arg.p2pMode) { - add(BandOption.BandAny) - if (RepeaterService.safeMode) { - add(BandOption.Band2GHz) - add(BandOption.Band5GHz) - addAll(channels) - } else addAll(p2pChannels) - } else { - if (Build.VERSION.SDK_INT >= 28) add(BandOption.BandAny) - add(BandOption.Band2GHz) - add(BandOption.Band5GHz) - if (Build.VERSION.SDK_INT >= 30) add(BandOption.Band6GHz) - addAll(channels) - } - } - adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0, bandOptions).apply { + fun Spinner.configure(options: List) { + adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0, options).apply { setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) } - } else dialogView.bandWrapper.isGone = true - dialogView.bssid.addTextChangedListener(this@WifiApDialogFragment) - if (arg.p2pMode) dialogView.hiddenSsid.isGone = true - if (arg.p2pMode || Build.VERSION.SDK_INT < 30) { - dialogView.maxClientWrapper.isGone = true - dialogView.clientUserControl.isGone = true - dialogView.blockedListWrapper.isGone = true - dialogView.allowedListWrapper.isGone = true - } else { + onItemSelectedListener = this@WifiApDialogFragment + } + if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) { + dialogView.band2G.configure(channels2G) + dialogView.band5G.configure(currentChannels5G) + if (Build.VERSION.SDK_INT >= 30 && !arg.p2pMode) dialogView.band6G.configure(channels6G) + else dialogView.bandWrapper6G.isGone = true + if (Build.VERSION.SDK_INT >= 31 && !arg.p2pMode) dialogView.band60G.configure(channels60G) else { + dialogView.bandWrapper60G.isGone = true + dialogView.bridgedMode.isGone = true + dialogView.bridgedModeOpportunisticShutdown.isGone = true + } + } else dialogView.bandGroup.isGone = true + if (!arg.p2pMode && Build.VERSION.SDK_INT >= 30) { dialogView.maxClient.addTextChangedListener(this@WifiApDialogFragment) dialogView.blockedList.addTextChangedListener(this@WifiApDialogFragment) dialogView.allowedList.addTextChangedListener(this@WifiApDialogFragment) + } else dialogView.accessControlGroup.isGone = true + dialogView.bssid.addTextChangedListener(this@WifiApDialogFragment) + if (arg.p2pMode) dialogView.hiddenSsid.isGone = true + if (arg.p2pMode && Build.VERSION.SDK_INT >= 29) dialogView.macRandomization.isEnabled = false + else if (arg.p2pMode || Build.VERSION.SDK_INT < 31) dialogView.macRandomization.isGone = true + if (arg.p2pMode || Build.VERSION.SDK_INT < 31) { + dialogView.ieee80211ax.isGone = true + dialogView.userConfig.isGone = true } base = arg.configuration populateFromConfiguration() } + private fun locate(band: Int, channels: List): Int { + val channel = base.getChannel(band) + val selection = channels.indexOfFirst { it.channel == channel } + return if (selection == -1) { + Timber.w(Exception("Unable to locate $band, $channel, ${arg.p2pMode && !RepeaterService.safeMode}")) + 0 + } else selection + } + private var userBridgedMode = false + private fun setBridgedMode(): Boolean { + var auto = 0 + var set = 0 + for (s in arrayOf(dialogView.band2G, dialogView.band5G, dialogView.band6G, + dialogView.band60G)) when (s.selectedItem) { + is ChannelOption.Auto -> auto = 1 + !is ChannelOption.Disabled -> ++set + } + if (auto + set > 1) { + if (dialogView.bridgedMode.isEnabled) { + userBridgedMode = dialogView.bridgedMode.isChecked + dialogView.bridgedMode.isEnabled = false + dialogView.bridgedMode.isChecked = true + } + } else if (!dialogView.bridgedMode.isEnabled) { + dialogView.bridgedMode.isEnabled = true + dialogView.bridgedMode.isChecked = userBridgedMode + } + return auto + set > 0 + } private fun populateFromConfiguration() { dialogView.ssid.setText(base.ssid) if (!arg.p2pMode) dialogView.security.setSelection(base.securityType) @@ -208,10 +237,13 @@ class WifiApDialogFragment : AlertDialogFragment= 23 || arg.p2pMode) { - val selection = if (base.channel != 0) { - bandOptions.indexOfFirst { it.channel == base.channel } - } else bandOptions.indexOfFirst { it.band == base.band } - dialogView.band.setSelection(if (selection == -1) 0 else selection) + dialogView.band2G.setSelection(locate(SoftApConfigurationCompat.BAND_2GHZ, channels2G)) + dialogView.band5G.setSelection(locate(SoftApConfigurationCompat.BAND_5GHZ, currentChannels5G)) + dialogView.band6G.setSelection(locate(SoftApConfigurationCompat.BAND_6GHZ, channels6G)) + dialogView.band60G.setSelection(locate(SoftApConfigurationCompat.BAND_60GHZ, channels60G)) + userBridgedMode = base.channels.size() > 1 + dialogView.bridgedMode.isChecked = userBridgedMode + setBridgedMode() } dialogView.bssid.setText(base.bssid?.toString()) dialogView.hiddenSsid.isChecked = base.isHiddenSsid @@ -219,6 +251,11 @@ class WifiApDialogFragment : AlertDialogFragment { + val option5G = dialogView.band5G.selectedItem + when (dialogView.band2G.selectedItem) { + is ChannelOption.Disabled -> option5G !is ChannelOption.Disabled && + (!arg.p2pMode || RepeaterService.safeMode || option5G !is ChannelOption.Auto) + is ChannelOption.Auto -> + (arg.p2pMode || Build.VERSION.SDK_INT >= 28) && option5G is ChannelOption.Auto || + (!arg.p2pMode || RepeaterService.safeMode) && option5G is ChannelOption.Disabled + else -> option5G is ChannelOption.Disabled + } + } + Build.VERSION.SDK_INT == 30 -> { + var expected = 1 + var set = 0 + for (s in arrayOf(dialogView.band2G, dialogView.band5G, dialogView.band6G)) when (s.selectedItem) { + is ChannelOption.Auto -> expected = 0 + !is ChannelOption.Disabled -> ++set + } + set == expected + } + else -> setBridgedMode() + } dialogView.bssidWrapper.error = null val bssidValid = dialogView.bssid.length() == 0 || try { MacAddressCompat.fromString(dialogView.bssid.text.toString()) @@ -290,7 +350,7 @@ class WifiApDialogFragment : AlertDialogFragment?, view: View?, position: Int, id: Long) = validate() + override fun onNothingSelected(parent: AdapterView<*>?) = error("unreachable") + override fun onMenuItemClick(item: MenuItem?): Boolean { return when (item?.itemId) { android.R.id.copy -> { @@ -318,7 +381,7 @@ class WifiApDialogFragment : AlertDialogFragment { diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt index 82234eed..d99e13f8 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt @@ -3,18 +3,16 @@ package be.mygod.vpnhotspot.net.wifi import android.annotation.TargetApi import android.content.Intent import android.content.pm.PackageManager -import android.net.MacAddress +import android.content.res.Resources import android.net.wifi.SoftApConfiguration import android.net.wifi.WifiManager import android.os.Build import android.os.Handler +import android.os.Parcelable import androidx.annotation.RequiresApi import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat -import be.mygod.vpnhotspot.util.ConstantLookup -import be.mygod.vpnhotspot.util.LongConstantLookup -import be.mygod.vpnhotspot.util.Services -import be.mygod.vpnhotspot.util.callSuper +import be.mygod.vpnhotspot.util.* import timber.log.Timber import java.lang.reflect.InvocationHandler import java.lang.reflect.Method @@ -27,6 +25,8 @@ object WifiApManager { */ @RequiresApi(30) private const val ACTION_RESOURCES_APK = "com.android.server.wifi.intent.action.SERVICE_WIFI_RESOURCES_APK" + @RequiresApi(30) + const val RESOURCES_PACKAGE = "com.android.wifi.resources" /** * Based on: https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/000ad45/service/java/com/android/server/wifi/WifiContext.java#66 */ @@ -34,6 +34,115 @@ object WifiApManager { val resolvedActivity get() = app.packageManager.queryIntentActivities(Intent(ACTION_RESOURCES_APK), PackageManager.MATCH_SYSTEM_ONLY).single() + private const val CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED = "config_wifi_p2p_mac_randomization_supported" + val p2pMacRandomizationSupported get() = when (Build.VERSION.SDK_INT) { + 29 -> Resources.getSystem().run { + getBoolean(getIdentifier(CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED, "bool", "android")) + } + in 30..Int.MAX_VALUE -> @TargetApi(30) { + val info = resolvedActivity.activityInfo + val resources = app.packageManager.getResourcesForApplication(info.applicationInfo) + resources.getBoolean(resources.findIdentifier(CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED, "bool", + RESOURCES_PACKAGE, info.packageName)) + } + else -> false + } + + @get:RequiresApi(30) + private val apMacRandomizationSupported by lazy { + WifiManager::class.java.getDeclaredMethod("isApMacRandomizationSupported") + } + @get:RequiresApi(30) + val isApMacRandomizationSupported get() = apMacRandomizationSupported(Services.wifi) as Boolean + + /** + * Broadcast intent action indicating that Wi-Fi AP has been enabled, disabled, + * enabling, disabling, or failed. + */ + const val WIFI_AP_STATE_CHANGED_ACTION = "android.net.wifi.WIFI_AP_STATE_CHANGED" + /** + * The lookup key for an int that indicates whether Wi-Fi AP is enabled, + * disabled, enabling, disabling, or failed. Retrieve it with [Intent.getIntExtra]. + * + * @see WIFI_AP_STATE_DISABLED + * @see WIFI_AP_STATE_DISABLING + * @see WIFI_AP_STATE_ENABLED + * @see WIFI_AP_STATE_ENABLING + * @see WIFI_AP_STATE_FAILED + */ + private const val EXTRA_WIFI_AP_STATE = "wifi_state" + /** + * An extra containing the int error code for Soft AP start failure. + * Can be obtained from the [WIFI_AP_STATE_CHANGED_ACTION] using [Intent.getIntExtra]. + * This extra will only be attached if [EXTRA_WIFI_AP_STATE] is + * attached and is equal to [WIFI_AP_STATE_FAILED]. + * + * The error code will be one of: + * {@link #SAP_START_FAILURE_GENERAL}, + * {@link #SAP_START_FAILURE_NO_CHANNEL}, + * {@link #SAP_START_FAILURE_UNSUPPORTED_CONFIGURATION} + * + * Source: https://android.googlesource.com/platform/frameworks/base/+/android-6.0.0_r1/wifi/java/android/net/wifi/WifiManager.java#210 + */ + @get:RequiresApi(23) + val EXTRA_WIFI_AP_FAILURE_REASON get() = + if (Build.VERSION.SDK_INT >= 30) "android.net.wifi.extra.WIFI_AP_FAILURE_REASON" else "wifi_ap_error_code" + /** + * The lookup key for a String extra that stores the interface name used for the Soft AP. + * This extra is included in the broadcast [WIFI_AP_STATE_CHANGED_ACTION]. + * Retrieve its value with [Intent.getStringExtra]. + * + * Source: https://android.googlesource.com/platform/frameworks/base/+/android-8.0.0_r1/wifi/java/android/net/wifi/WifiManager.java#413 + */ + @get:RequiresApi(26) + val EXTRA_WIFI_AP_INTERFACE_NAME get() = + if (Build.VERSION.SDK_INT >= 30) "android.net.wifi.extra.WIFI_AP_INTERFACE_NAME" else "wifi_ap_interface_name" + + fun checkWifiApState(state: Int) = if (state < WIFI_AP_STATE_DISABLING || state > WIFI_AP_STATE_FAILED) { + Timber.w(Exception("Unknown state $state")) + false + } else true + val Intent.wifiApState get() = + getIntExtra(EXTRA_WIFI_AP_STATE, WIFI_AP_STATE_DISABLED).also { checkWifiApState(it) } + /** + * Wi-Fi AP is currently being disabled. The state will change to + * [WIFI_AP_STATE_DISABLED] if it finishes successfully. + * + * @see WIFI_AP_STATE_CHANGED_ACTION + * @see #getWifiApState() + */ + const val WIFI_AP_STATE_DISABLING = 10 + /** + * Wi-Fi AP is disabled. + * + * @see WIFI_AP_STATE_CHANGED_ACTION + * @see #getWifiState() + */ + const val WIFI_AP_STATE_DISABLED = 11 + /** + * Wi-Fi AP is currently being enabled. The state will change to + * {@link #WIFI_AP_STATE_ENABLED} if it finishes successfully. + * + * @see WIFI_AP_STATE_CHANGED_ACTION + * @see #getWifiApState() + */ + const val WIFI_AP_STATE_ENABLING = 12 + /** + * Wi-Fi AP is enabled. + * + * @see WIFI_AP_STATE_CHANGED_ACTION + * @see #getWifiApState() + */ + const val WIFI_AP_STATE_ENABLED = 13 + /** + * Wi-Fi AP is in a failed state. This state will occur when an error occurs during + * enabling or disabling + * + * @see WIFI_AP_STATE_CHANGED_ACTION + * @see #getWifiApState() + */ + const val WIFI_AP_STATE_FAILED = 14 + private val getWifiApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getWifiApConfiguration") } @Suppress("DEPRECATION") private val setWifiApConfiguration by lazy { @@ -63,40 +172,65 @@ object WifiApManager { /** * Called when soft AP state changes. * - * @param state new new AP state. One of {@link #WIFI_AP_STATE_DISABLED}, - * {@link #WIFI_AP_STATE_DISABLING}, {@link #WIFI_AP_STATE_ENABLED}, - * {@link #WIFI_AP_STATE_ENABLING}, {@link #WIFI_AP_STATE_FAILED} + * @param state the new AP state. One of [WIFI_AP_STATE_DISABLED], [WIFI_AP_STATE_DISABLING], + * [WIFI_AP_STATE_ENABLED], [WIFI_AP_STATE_ENABLING], [WIFI_AP_STATE_FAILED] * @param failureReason reason when in failed state. One of - * {@link #SAP_START_FAILURE_GENERAL}, {@link #SAP_START_FAILURE_NO_CHANNEL} + * {@link #SAP_START_FAILURE_GENERAL}, + * {@link #SAP_START_FAILURE_NO_CHANNEL}, + * {@link #SAP_START_FAILURE_UNSUPPORTED_CONFIGURATION} */ fun onStateChanged(state: Int, failureReason: Int) { } /** * Called when number of connected clients to soft AP changes. * + * It is not recommended to use this legacy method on API 30+. + * * @param numClients number of connected clients */ - @Deprecated("onConnectedClientsChanged") fun onNumClientsChanged(numClients: Int) { } + /** + * Called when the connected clients to soft AP changes. + * + * @param clients the currently connected clients + */ @RequiresApi(30) - fun onConnectedClientsChanged(clients: List) { - @Suppress("DEPRECATION") - onNumClientsChanged(clients.size) - } + fun onConnectedClientsChanged(clients: List) = onNumClientsChanged(clients.size) + /** + * Called when information of softap changes. + * + * @param info is the softap information. [SoftApInfo] + * At most one will be returned on API 30. + */ @RequiresApi(30) - fun onInfoChanged(frequency: Int, bandwidth: Int) { } + fun onInfoChanged(info: List) { } + /** + * Called when capability of softap changes. + * + * @param capability is the softap capability. [SoftApCapability] + */ @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) - fun onBlockedClientConnecting(client: MacAddress, blockedReason: Int) { } + fun onBlockedClientConnecting(client: Parcelable, blockedReason: Int) { } } - @RequiresApi(28) - val failureReasonLookup = ConstantLookup("SAP_START_FAILURE_", - "SAP_START_FAILURE_GENERAL", "SAP_START_FAILURE_NO_CHANNEL") + @RequiresApi(23) + val failureReasonLookup = ConstantLookup("SAP_START_FAILURE_", "GENERAL", "NO_CHANNEL") @get:RequiresApi(30) val clientBlockLookup by lazy { ConstantLookup("SAP_CLIENT_") } @@ -111,25 +245,6 @@ object WifiApManager { WifiManager::class.java.getDeclaredMethod("unregisterSoftApCallback", interfaceSoftApCallback) } - private val getMacAddress by lazy { - Class.forName("android.net.wifi.WifiClient").getDeclaredMethod("getMacAddress") - } - - private val classSoftApInfo by lazy { Class.forName("android.net.wifi.SoftApInfo") } - private val getFrequency by lazy { classSoftApInfo.getDeclaredMethod("getFrequency") } - private val getBandwidth by lazy { classSoftApInfo.getDeclaredMethod("getBandwidth") } - @RequiresApi(30) - val channelWidthLookup = ConstantLookup("CHANNEL_WIDTH_") { classSoftApInfo } - const val CHANNEL_WIDTH_INVALID = 0 - - private val classSoftApCapability by lazy { Class.forName("android.net.wifi.SoftApCapability") } - private val getMaxSupportedClients by lazy { classSoftApCapability.getDeclaredMethod("getMaxSupportedClients") } - private val areFeaturesSupported by lazy { - classSoftApCapability.getDeclaredMethod("areFeaturesSupported", Long::class.java) - } - @get:RequiresApi(30) - val featureLookup by lazy { LongConstantLookup(classSoftApCapability, "SOFTAP_FEATURE_") } - @RequiresApi(28) fun registerSoftApCallback(callback: SoftApCallbackCompat, executor: Executor): Any { val proxy = Proxy.newProxyInstance(interfaceSoftApCallback.classLoader, @@ -141,49 +256,37 @@ object WifiApManager { } else invokeActual(proxy, method, args) private fun invokeActual(proxy: Any, method: Method, args: Array?): Any? { - val noArgs = args?.size ?: 0 - return when (val name = method.name) { - "onStateChanged" -> { - if (noArgs != 2) Timber.w("Unexpected args for $name: ${args?.contentToString()}") + return when { + method.matches("onStateChanged", Integer.TYPE, Integer.TYPE) -> { callback.onStateChanged(args!![0] as Int, args[1] as Int) } - "onNumClientsChanged" -> @Suppress("DEPRECATION") { + method.matches("onNumClientsChanged", Integer.TYPE) -> @Suppress("DEPRECATION") { if (Build.VERSION.SDK_INT >= 30) Timber.w(Exception("Unexpected onNumClientsChanged")) - if (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}") callback.onNumClientsChanged(args!![0] as Int) } - "onConnectedClientsChanged" -> @TargetApi(30) { + method.matches1>("onConnectedClientsChanged") -> @TargetApi(30) { if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onConnectedClientsChanged")) - if (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}") - callback.onConnectedClientsChanged((args!![0] as? Iterable<*> ?: return null) - .map { getMacAddress(it) as MacAddress }) + @Suppress("UNCHECKED_CAST") + callback.onConnectedClientsChanged(args!![0] as List) } - "onInfoChanged" -> @TargetApi(30) { - if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onInfoChanged")) - if (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}") - val softApInfo = args!![0] - if (softApInfo != null && classSoftApInfo.isAssignableFrom(softApInfo.javaClass)) { - callback.onInfoChanged(getFrequency(softApInfo) as Int, getBandwidth(softApInfo) as Int) - } else null + method.matches1>("onInfoChanged") -> @TargetApi(31) { + if (Build.VERSION.SDK_INT < 31) Timber.w(Exception("Unexpected onInfoChanged API 31+")) + @Suppress("UNCHECKED_CAST") + callback.onInfoChanged(args!![0] as List) } - "onCapabilityChanged" -> @TargetApi(30) { - if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onCapabilityChanged")) - if (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}") - val softApCapability = args!![0] - var supportedFeatures = 0L - var probe = 1L - while (probe != 0L) { - if (areFeaturesSupported(softApCapability, probe) as Boolean) { - supportedFeatures = supportedFeatures or probe - } - probe += probe - } - callback.onCapabilityChanged(getMaxSupportedClients(softApCapability) as Int, supportedFeatures) + Build.VERSION.SDK_INT >= 30 && method.matches("onInfoChanged", SoftApInfo.clazz) -> { + if (Build.VERSION.SDK_INT >= 31) return null // ignore old version calls + val arg = args!![0] + val info = SoftApInfo(arg as Parcelable) + callback.onInfoChanged( // check for legacy empty info with CHANNEL_WIDTH_INVALID + if (info.frequency == 0 && info.bandwidth == 0) emptyList() else listOf(arg)) } - "onBlockedClientConnecting" -> @TargetApi(30) { - if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onBlockedClientConnecting")) - if (noArgs != 2) Timber.w("Unexpected args for $name: ${args?.contentToString()}") - callback.onBlockedClientConnecting(getMacAddress(args!![0]) as MacAddress, args[1] as Int) + Build.VERSION.SDK_INT >= 30 && method.matches("onCapabilityChanged", SoftApCapability.clazz) -> { + callback.onCapabilityChanged(args!![0] as Parcelable) + } + 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) } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiClient.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiClient.kt new file mode 100644 index 00000000..b106db9d --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiClient.kt @@ -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 + } +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiP2pManagerHelper.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiP2pManagerHelper.kt index 1dd6bf2b..d4749c8e 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiP2pManagerHelper.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiP2pManagerHelper.kt @@ -9,8 +9,8 @@ import androidx.annotation.RequiresApi import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.net.MacAddressCompat import be.mygod.vpnhotspot.util.callSuper +import be.mygod.vpnhotspot.util.matchesCompat import kotlinx.coroutines.CompletableDeferred -import timber.log.Timber import java.lang.reflect.InvocationHandler import java.lang.reflect.Method import java.lang.reflect.Proxy @@ -96,12 +96,11 @@ object WifiP2pManagerHelper { return result.future.await() } - private val interfacePersistentGroupInfoListener by lazy @SuppressLint("PrivateApi") { + private val interfacePersistentGroupInfoListener by lazy { Class.forName("android.net.wifi.p2p.WifiP2pManager\$PersistentGroupInfoListener") } - private val getGroupList by lazy @SuppressLint("PrivateApi") { - Class.forName("android.net.wifi.p2p.WifiP2pGroupList").getDeclaredMethod("getGroupList") - } + private val classWifiP2pGroupList by lazy { Class.forName("android.net.wifi.p2p.WifiP2pGroupList") } + private val getGroupList by lazy { classWifiP2pGroupList.getDeclaredMethod("getGroupList") } private val requestPersistentGroupInfo by lazy { WifiP2pManager::class.java.getDeclaredMethod("requestPersistentGroupInfo", WifiP2pManager.Channel::class.java, interfacePersistentGroupInfoListener) @@ -112,15 +111,13 @@ object WifiP2pManagerHelper { * Requires one of NETWORK_SETTING, NETWORK_STACK, or READ_WIFI_CREDENTIAL permission since API 30. * * @param c is the channel created at {@link #initialize} - * @param listener for callback when persistent group info list is available. Can be null. */ suspend fun WifiP2pManager.requestPersistentGroupInfo(c: WifiP2pManager.Channel): Collection { val result = CompletableDeferred>() requestPersistentGroupInfo(this, c, Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader, arrayOf(interfacePersistentGroupInfoListener), object : InvocationHandler { - override fun invoke(proxy: Any, method: Method, args: Array?): Any? = when (method.name) { - "onPersistentGroupInfoAvailable" -> { - if (args?.size != 1) Timber.w(IllegalArgumentException("Unexpected args: $args")) + override fun invoke(proxy: Any, method: Method, args: Array?): Any? = when { + method.matchesCompat("onPersistentGroupInfoAvailable", args, classWifiP2pGroupList) -> { @Suppress("UNCHECKED_CAST") result.complete(getGroupList(args!![0]) as Collection) } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/preference/AlwaysAutoCompleteEditTextPreferenceDialogFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/preference/AlwaysAutoCompleteEditTextPreferenceDialogFragment.kt deleted file mode 100644 index c1fce475..00000000 --- a/mobile/src/main/java/be/mygod/vpnhotspot/preference/AlwaysAutoCompleteEditTextPreferenceDialogFragment.kt +++ /dev/null @@ -1,42 +0,0 @@ -package be.mygod.vpnhotspot.preference - -import android.content.Context -import android.view.View -import android.view.ViewGroup -import android.widget.ArrayAdapter -import androidx.core.os.bundleOf -import androidx.preference.EditTextPreferenceDialogFragmentCompat -import be.mygod.vpnhotspot.R -import be.mygod.vpnhotspot.widget.AlwaysAutoCompleteEditText - -class AlwaysAutoCompleteEditTextPreferenceDialogFragment : EditTextPreferenceDialogFragmentCompat() { - companion object { - private const val ARG_SUGGESTIONS = "suggestions" - } - - fun setArguments(key: String, suggestions: Array) { - arguments = bundleOf(ARG_KEY to key, ARG_SUGGESTIONS to suggestions) - } - - private lateinit var editText: AlwaysAutoCompleteEditText - - override fun onCreateDialogView(context: Context) = super.onCreateDialogView(context).apply { - editText = AlwaysAutoCompleteEditText(context).apply { - id = android.R.id.edit - minHeight = resources.getDimensionPixelSize(R.dimen.touch_target_min) - } - val oldEditText = findViewById(android.R.id.edit)!! - val container = oldEditText.parent as ViewGroup - container.removeView(oldEditText) - container.addView(editText, oldEditText.layoutParams) - } - - override fun onBindDialogView(view: View) { - super.onBindDialogView(view) - editText.hint = (preference.summaryProvider as SummaryFallbackProvider).fallback - arguments?.getStringArray(ARG_SUGGESTIONS)?.let { suggestions -> - editText.setAdapter(ArrayAdapter(view.context, android.R.layout.select_dialog_item, suggestions)) - } - editText.clearFocus() // having focus is buggy currently - } -} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/preference/AutoCompleteNetworkPreferenceDialogFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/preference/AutoCompleteNetworkPreferenceDialogFragment.kt new file mode 100644 index 00000000..a2effe05 --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/preference/AutoCompleteNetworkPreferenceDialogFragment.kt @@ -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 + private fun updateAdapter() { + adapter.clear() + adapter.addAll(interfaceNames.flatMap { it.value }) + } + + private val interfaceNames = mutableMapOf>() + private val callback = object : ConnectivityManager.NetworkCallback() { + override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) { + interfaceNames[network] = properties.allInterfaceNames + 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(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() + } +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/preference/UpstreamsPreference.kt b/mobile/src/main/java/be/mygod/vpnhotspot/preference/UpstreamsPreference.kt index 5ab3df9b..d826d161 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/preference/UpstreamsPreference.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/preference/UpstreamsPreference.kt @@ -13,8 +13,8 @@ import androidx.preference.Preference import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor -import be.mygod.vpnhotspot.util.SpanFormatter import be.mygod.vpnhotspot.util.allRoutes +import be.mygod.vpnhotspot.util.format import be.mygod.vpnhotspot.util.parseNumericAddress import timber.log.Timber @@ -73,7 +73,7 @@ class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(co } private fun onUpdate() = (context as LifecycleOwner).lifecycleScope.launchWhenStarted { - summary = SpanFormatter.format(context.getText(R.string.settings_service_upstream_monitor_summary), - primary.charSequence, fallback.charSequence) + summary = context.getText(R.string.settings_service_upstream_monitor_summary).format( + context.resources.configuration.locale, primary.charSequence, fallback.charSequence) } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/root/MiscCommands.kt b/mobile/src/main/java/be/mygod/vpnhotspot/root/MiscCommands.kt index 10179637..e1714073 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/root/MiscCommands.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/root/MiscCommands.kt @@ -102,8 +102,8 @@ class ProcessListener(private val terminateRegex: Regex, try { launch(parent) { try { - process.inputStream.bufferedReader().useLines { - for (line in it) { + process.inputStream.bufferedReader().useLines { lines -> + for (line in lines) { trySend(ProcessData.StdoutLine(line)).onClosed { return@useLines }.onFailure { throw it!! } if (terminateRegex.containsMatchIn(line)) process.destroy() } @@ -112,8 +112,8 @@ class ProcessListener(private val terminateRegex: Regex, } launch(parent) { try { - process.errorStream.bufferedReader().useLines { - for (line in it) trySend(ProcessData.StdoutLine(line)).onClosed { + process.errorStream.bufferedReader().useLines { lines -> + for (line in lines) trySend(ProcessData.StdoutLine(line)).onClosed { return@useLines }.onFailure { throw it!! } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/root/RootManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/root/RootManager.kt index d874958f..a9bebf3b 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/root/RootManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/root/RootManager.kt @@ -1,5 +1,6 @@ package be.mygod.vpnhotspot.root +import android.annotation.SuppressLint import android.os.Parcelable import android.util.Log import be.mygod.librootkotlinx.* @@ -13,6 +14,7 @@ object RootManager : RootSession(), Logger { class RootInit : RootCommandNoResult { override suspend fun execute(): Parcelable? { Timber.plant(object : Timber.DebugTree() { + @SuppressLint("LogNotTimber") override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { if (priority >= Log.WARN) { System.err.println("$priority/$tag: $message") diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/root/RoutingCommands.kt b/mobile/src/main/java/be/mygod/vpnhotspot/root/RoutingCommands.kt index 4bef650d..f43fbad9 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/root/RoutingCommands.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/root/RoutingCommands.kt @@ -1,7 +1,6 @@ package be.mygod.vpnhotspot.root import android.os.Parcelable -import android.util.Log import be.mygod.librootkotlinx.RootCommand import be.mygod.librootkotlinx.RootCommandOneWay import be.mygod.vpnhotspot.net.Routing @@ -10,6 +9,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize +import timber.log.Timber object RoutingCommands { @Parcelize @@ -20,7 +20,7 @@ object RoutingCommands { process.outputStream.bufferedWriter().use(Routing.Companion::appendCleanCommands) when (val code = process.waitFor()) { 0 -> { } - else -> Log.d("RoutingCommands.Clean", "Unexpected exit code $code") + else -> Timber.w("Unexpected exit code $code") } check(process.waitFor() == 0) } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/root/WifiApCommands.kt b/mobile/src/main/java/be/mygod/vpnhotspot/root/WifiApCommands.kt index 29cb1e24..e7db4c6d 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/root/WifiApCommands.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/root/WifiApCommands.kt @@ -1,13 +1,18 @@ package be.mygod.vpnhotspot.root -import android.net.MacAddress +import android.annotation.TargetApi +import android.content.ClipData +import android.os.Build import android.os.Parcelable import androidx.annotation.RequiresApi import be.mygod.librootkotlinx.ParcelableBoolean import be.mygod.librootkotlinx.RootCommand import be.mygod.librootkotlinx.RootCommandChannel +import be.mygod.vpnhotspot.App.Companion.app +import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat import be.mygod.vpnhotspot.net.wifi.WifiApManager +import be.mygod.vpnhotspot.net.wifi.WifiClient import be.mygod.vpnhotspot.widget.SmartSnackbar import kotlinx.coroutines.* import kotlinx.coroutines.channels.* @@ -26,32 +31,29 @@ object WifiApCommands { } @Parcelize data class OnNumClientsChanged(val numClients: Int) : SoftApCallbackParcel() { - @Suppress("DEPRECATION") override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) = callback.onNumClientsChanged(numClients) } @Parcelize @RequiresApi(30) - data class OnConnectedClientsChanged(val clients: List) : SoftApCallbackParcel() { + data class OnConnectedClientsChanged(val clients: List) : SoftApCallbackParcel() { override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) = callback.onConnectedClientsChanged(clients) } @Parcelize @RequiresApi(30) - data class OnInfoChanged(val frequency: Int, val bandwidth: Int) : SoftApCallbackParcel() { - override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) = - callback.onInfoChanged(frequency, bandwidth) + data class OnInfoChanged(val info: List) : SoftApCallbackParcel() { + override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) = callback.onInfoChanged(info) } @Parcelize @RequiresApi(30) - data class OnCapabilityChanged(val maxSupportedClients: Int, - val supportedFeatures: Long) : SoftApCallbackParcel() { + data class OnCapabilityChanged(val capability: Parcelable) : SoftApCallbackParcel() { override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) = - callback.onCapabilityChanged(maxSupportedClients, supportedFeatures) + callback.onCapabilityChanged(capability) } @Parcelize @RequiresApi(30) - data class OnBlockedClientConnecting(val client: MacAddress, val blockedReason: Int) : SoftApCallbackParcel() { + data class OnBlockedClientConnecting(val client: Parcelable, val blockedReason: Int) : SoftApCallbackParcel() { override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) = callback.onBlockedClientConnecting(client, blockedReason) } @@ -60,7 +62,7 @@ object WifiApCommands { @Parcelize @RequiresApi(28) class RegisterSoftApCallback : RootCommandChannel { - override fun create(scope: CoroutineScope) = scope.produce(capacity = capacity) { + override fun create(scope: CoroutineScope) = scope.produce(capacity = capacity) { val finish = CompletableDeferred() val key = WifiApManager.registerSoftApCallback(object : WifiApManager.SoftApCallbackCompat { private fun push(parcel: SoftApCallbackParcel) { @@ -71,20 +73,18 @@ object WifiApCommands { override fun onStateChanged(state: Int, failureReason: Int) = push(SoftApCallbackParcel.OnStateChanged(state, failureReason)) - @Suppress("OverridingDeprecatedMember") override fun onNumClientsChanged(numClients: Int) = push(SoftApCallbackParcel.OnNumClientsChanged(numClients)) @RequiresApi(30) - override fun onConnectedClientsChanged(clients: List) = + override fun onConnectedClientsChanged(clients: List) = push(SoftApCallbackParcel.OnConnectedClientsChanged(clients)) @RequiresApi(30) - override fun onInfoChanged(frequency: Int, bandwidth: Int) = - push(SoftApCallbackParcel.OnInfoChanged(frequency, bandwidth)) + override fun onInfoChanged(info: List) = push(SoftApCallbackParcel.OnInfoChanged(info)) @RequiresApi(30) - override fun onCapabilityChanged(maxSupportedClients: Int, supportedFeatures: Long) = - push(SoftApCallbackParcel.OnCapabilityChanged(maxSupportedClients, supportedFeatures)) + override fun onCapabilityChanged(capability: Parcelable) = + push(SoftApCallbackParcel.OnCapabilityChanged(capability)) @RequiresApi(30) - override fun onBlockedClientConnecting(client: MacAddress, blockedReason: Int) = + override fun onBlockedClientConnecting(client: Parcelable, blockedReason: Int) = push(SoftApCallbackParcel.OnBlockedClientConnecting(client, blockedReason)) }) { scope.launch { @@ -125,8 +125,21 @@ object WifiApCommands { } is SoftApCallbackParcel.OnInfoChanged -> synchronized(callbacks) { lastCallback.info = parcel } is SoftApCallbackParcel.OnCapabilityChanged -> synchronized(callbacks) { lastCallback.capability = parcel } + is SoftApCallbackParcel.OnBlockedClientConnecting -> @TargetApi(30) { // passively consume events + val client = WifiClient(parcel.client) + val macAddress = client.macAddress + var name = macAddress.toString() + if (Build.VERSION.SDK_INT >= 31) client.apInstanceIdentifier?.let { name += "%$it" } + val reason = WifiApManager.clientBlockLookup(parcel.blockedReason, true) + Timber.i("$name blocked from connecting: $reason (${parcel.blockedReason})") + SmartSnackbar.make(app.getString(R.string.tethering_manage_wifi_client_blocked, name, reason)).apply { + action(R.string.tethering_manage_wifi_copy_mac) { + app.clipboard.setPrimaryClip(ClipData.newPlainText(null, macAddress.toString())) + } + }.show() + } } - for (callback in synchronized(callbacks) { callbacks }) parcel.dispatch(callback) + for (callback in synchronized(callbacks) { callbacks.toList() }) parcel.dispatch(callback) } @RequiresApi(28) fun registerSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) { @@ -142,8 +155,8 @@ object WifiApCommands { SmartSnackbar.make(e).show() } } - lastCallback - } else null + null + } else lastCallback }?.toSequence()?.forEach { it?.dispatch(callback) } @RequiresApi(28) fun unregisterSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) { diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/ConstantLookup.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/ConstantLookup.kt index c7d3fb17..135ebdfb 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/util/ConstantLookup.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/util/ConstantLookup.kt @@ -7,12 +7,12 @@ import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.R import timber.log.Timber -class ConstantLookup(private val prefix: String, private val lookup29: Array, +class ConstantLookup(private val prefix: String, private val lookup29: Array, private val clazz: () -> Class<*>) { private val lookup by lazy { SparseArrayCompat().apply { for (field in clazz().declaredFields) try { - if (field.name.startsWith(prefix)) put(field.getInt(null), field.name) + if (field.type == Int::class.java && field.name.startsWith(prefix)) put(field.getInt(null), field.name) } catch (e: Exception) { Timber.w(e) } @@ -25,22 +25,22 @@ class ConstantLookup(private val prefix: String, private val lookup29: Array Class<*>) = +fun ConstantLookup(prefix: String, vararg lookup29: String?, clazz: () -> Class<*>) = ConstantLookup(prefix, lookup29, clazz) @Suppress("FunctionName") -inline fun ConstantLookup(prefix: String, vararg lookup29: String) = +inline fun ConstantLookup(prefix: String, vararg lookup29: String?) = ConstantLookup(prefix, lookup29) { T::class.java } class LongConstantLookup(private val clazz: Class<*>, private val prefix: String) { private val lookup = LongSparseArray().apply { for (field in clazz.declaredFields) try { - if (field.name.startsWith(prefix)) put(field.getLong(null), field.name) + if (field.type == Long::class.java && field.name.startsWith(prefix)) put(field.getLong(null), field.name) } catch (e: Exception) { Timber.w(e) } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/Services.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/Services.kt index 03ae7e29..4d17d675 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/util/Services.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/util/Services.kt @@ -1,15 +1,12 @@ package be.mygod.vpnhotspot.util -import android.annotation.SuppressLint import android.content.Context import android.net.ConnectivityManager import android.net.wifi.WifiManager import android.net.wifi.p2p.WifiP2pManager -import android.util.Log import androidx.core.content.getSystemService import timber.log.Timber -@SuppressLint("LogNotTimber") object Services { private lateinit var contextInit: () -> Context val context by lazy { contextInit() } @@ -22,7 +19,7 @@ object Services { try { context.getSystemService() } catch (e: RuntimeException) { - if (android.os.Process.myUid() == 0) Log.w("WifiP2pManager", e) else Timber.w(e) + Timber.w(e) null } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/SpanFormatter.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/SpanFormatter.kt deleted file mode 100644 index e3d056fe..00000000 --- a/mobile/src/main/java/be/mygod/vpnhotspot/util/SpanFormatter.kt +++ /dev/null @@ -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]+\\$| "%" - "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) - } -} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/UnblockCentral.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/UnblockCentral.kt new file mode 100644 index 00000000..d5b43755 --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/util/UnblockCentral.kt @@ -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.java) + val clazz = Class.forName("dalvik.system.VMRuntime") + val setHiddenApiExemptions = getDeclaredMethod(clazz, "setHiddenApiExemptions", + arrayOf(Array::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 } + } +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt index d530d8ba..75d27f73 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt @@ -4,17 +4,13 @@ import android.annotation.SuppressLint import android.annotation.TargetApi import android.content.* import android.content.res.Resources -import android.net.InetAddresses -import android.net.LinkProperties -import android.net.RouteInfo +import android.net.* import android.os.Build import android.os.RemoteException import android.system.ErrnoException import android.system.Os import android.system.OsConstants -import android.text.Spannable -import android.text.SpannableString -import android.text.SpannableStringBuilder +import android.text.* import android.view.MenuItem import android.view.View import android.widget.ImageView @@ -39,6 +35,7 @@ import java.lang.reflect.Method import java.net.InetAddress import java.net.NetworkInterface import java.net.SocketException +import java.util.* tailrec fun Throwable.getRootCause(): Throwable { if (this is InvocationTargetException || this is RemoteException) return (cause ?: return this).getRootCause() @@ -56,6 +53,19 @@ fun Long.toPluralInt(): Int { return (this % 1000000000).toInt() + 1000000000 } +@RequiresApi(26) +fun Method.matches(name: String, vararg classes: Class<*>) = this.name == name && parameterCount == classes.size && + classes.indices.all { i -> parameters[i].type == classes[i] } +@RequiresApi(26) +inline fun Method.matches1(name: String) = matches(name, T::class.java) + +fun Method.matchesCompat(name: String, args: Array?, vararg classes: Class<*>) = + if (Build.VERSION.SDK_INT < 26) { + this.name == name && args?.size ?: 0 == classes.size && classes.indices.all { i -> + args!![i]?.let { classes[i].isInstance(it) } != false + } + } else matches(name, *classes) + fun Context.ensureReceiverUnregistered(receiver: BroadcastReceiver) { try { unregisterReceiver(receiver) @@ -80,6 +90,61 @@ fun setVisibility(view: View, value: Boolean) { view.isVisible = value } +private val formatSequence = "%([0-9]+\\$| "%" + "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 Iterable.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 Sequence.joinToSpanned(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "", + limit: Int = -1, truncated: CharSequence = "...", + transform: ((T) -> CharSequence)? = null) = + joinTo(SpannableStringBuilder(), separator, prefix, postfix, limit, truncated, transform) + fun makeIpSpan(ip: InetAddress) = ip.hostAddress.let { // exclude all bogon IP addresses supported by Android APIs if (!app.hasTouch || ip.isMulticastAddress || ip.isAnyLocalAddress || ip.isLoopbackAddress || @@ -184,6 +249,14 @@ fun InvocationHandler.callSuper(interfaceClass: Class<*>, proxy: Any, method: Me } } +fun globalNetworkRequestBuilder() = NetworkRequest.Builder().apply { + if (Build.VERSION.SDK_INT >= 31) setIncludeOtherUidNetworks(true) else if (Build.VERSION.SDK_INT == 23) { + // workarounds for OEM bugs + removeCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + removeCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL) + } +} + @Suppress("FunctionName") fun if_nametoindex(ifname: String) = if (Build.VERSION.SDK_INT >= 26) { Os.if_nametoindex(ifname) diff --git a/mobile/src/main/res/layout/dialog_wifi_ap.xml b/mobile/src/main/res/layout/dialog_wifi_ap.xml index 9d44a04a..cff941e6 100644 --- a/mobile/src/main/res/layout/dialog_wifi_ap.xml +++ b/mobile/src/main/res/layout/dialog_wifi_ap.xml @@ -37,6 +37,8 @@ android:layout_height="wrap_content" android:descendantFocusability="beforeDescendants" android:focusableInTouchMode="true" + android:layout_marginStart="0dp" + android:layout_marginEnd="0dp" style="@style/wifi_item"> + + + + android:prompt="@string/wifi_ap_choose_2G" /> + + + + + + + + + + + + - - + - - - - + - - - - + - - - + + + - + android:layout_marginTop="8dip" + android:minHeight="@dimen/touch_target_min" + android:text="@string/wifi_client_user_control" /> + + + + + + + + + + + + + + + + + + + diff --git a/mobile/src/main/res/values-ru/strings.xml b/mobile/src/main/res/values-ru/strings.xml index 09fb0188..c74ace52 100644 --- a/mobile/src/main/res/values-ru/strings.xml +++ b/mobile/src/main/res/values-ru/strings.xml @@ -61,7 +61,9 @@ "Диапазон частот Wi-Fi" "Авто" "2,4 ГГц" - "5,0 ГГц" + "5 ГГц" + "6 ГГц" + "60 ГГц" "MAC-адрес" "Скрытая сеть" "Сохранить" diff --git a/mobile/src/main/res/values-zh-rCN/strings.xml b/mobile/src/main/res/values-zh-rCN/strings.xml index d3927cf8..1e7be6c6 100644 --- a/mobile/src/main/res/values-zh-rCN/strings.xml +++ b/mobile/src/main/res/values-zh-rCN/strings.xml @@ -61,9 +61,21 @@ USB 网络共享 (NCM) WiGig 热点 %1$d MHz, 频道 %2$d, 频宽 %3$s + %4$s: Wi\u2011Fi %5$d, %1$d MHz, 频道 %2$d, 频宽 %3$s, + 关闭延迟 %6$s + %4$s: Wi\u2011Fi %5$d, %1$d MHz, 频道 %2$d, 频宽 %3$s, + 不自动关闭 已连接 %1$s/%2$d 个设备\n支持功能:%3$s + + 已连接 %d 个设备 + + \n支持频道: %s + 随机接入点 MAC + 桥接 AP 并发 + STA/AP 并发 + STA/桥接 AP 并发 已屏蔽 %1$s:%2$s 复制 MAC @@ -140,6 +152,7 @@ VPN 共享已启用 VPN 共享服务 + 监视不活跃接口 %d 个设备已连接到 %s @@ -166,16 +179,25 @@ 关闭延迟 默认延迟:%d 毫秒 "AP 频段" + Disabled "自动" "2.4 GHz 频段" - "5.0 GHz 频段" - 6.0 GHz 频段 + "5 GHz 频段" + 6 GHz 频段 + 60 GHz 频段 + 访问控制 + 高级接入点设置 "MAC 地址" "隐藏的网络" 允许连接设备数上限 过滤可以连接的设备 设备黑名单 设备白名单 + 随机生成 MAC 地址 + 启用无线接入点桥接模式 + 启用桥接模式伺机关闭 + 启用 Wi\u2011Fi 6 + 用户提供配置 "保存" diff --git a/mobile/src/main/res/values-zh-rTW/strings.xml b/mobile/src/main/res/values-zh-rTW/strings.xml index 2cd41f49..615d40be 100644 --- a/mobile/src/main/res/values-zh-rTW/strings.xml +++ b/mobile/src/main/res/values-zh-rTW/strings.xml @@ -71,6 +71,9 @@ 已連接 %1$s/%2$d 個設備\n支持功能:%3$s + + 已連接 %d 個設備 + 已隱藏 %1$s:%2$s 複製 MAC @@ -173,16 +176,20 @@ 關閉延遲時間 默認延遲:%d 毫秒 AP 頻帶 + 停用 自動 2.4 GHz 頻帶 - 5.0 GHz 頻帶 - 6.0 GHz 頻帶 + 5 GHz 頻帶 + 6 GHz 頻帶 + 60 GHz 頻帶 "MAC 地址" "隱藏的網路" 允許的連接裝置數量 過濾可以連接的裝置 裝置黑名單 裝置白名單 + 隨機化 MAC 位址 + 啟用 Wi\u2011Fi 6 儲存 diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index f2b020a9..0fbfb5fc 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -73,10 +73,23 @@ USB tethering (NCM) WiGig hotspot %1$d MHz, channel %2$d, width %3$s + %4$s: Wi\u2011Fi %5$d, %1$d MHz, channel %2$d, + width %3$s, idle timeout in %6$s + %4$s: Wi\u2011Fi %5$d, %1$d MHz, channel %2$d, + width %3$s, idle timeout disabled %1$s/%2$d client connected\nSupported features: %3$s %1$s/%2$d clients connected\nSupported features: %3$s + + %d client connected + %1d clients connected + + \nSupported channels: %s + Randomized AP MAC + Bridged AP concurrency + STA + AP concurrency + STA + Bridged AP concurrency None Blocked %1$s: %2$s Copy MAC @@ -157,8 +170,9 @@ Restart this app to apply this setting. Exit - VPN tethering active + VPN tethering VPN Tethering Service + Monitor Inactive Interfaces %d device connected to %s %d devices connected to %s @@ -188,16 +202,25 @@ Inactive timeout Default timeout: %dms AP Band + Disabled Auto 2.4 GHz Band - 5.0 GHz Band - 6.0 GHz Band + 5 GHz Band + 6 GHz Band + 60 GHz Band + Access Control + Advanced AP Options MAC address Hidden network Maximum number of clients Control which client can use hotspot Blocked list of clients Allowed list of clients + Use randomized MAC + Enable Bridged Access point (AP) concurrency + Enable Bridged mode opportunistic shutdown + Enable Wi\u2011Fi 6 + User Supplied Configuration Save diff --git a/mobile/src/main/res/values/styles.xml b/mobile/src/main/res/values/styles.xml index 013d57fa..8f249d97 100644 --- a/mobile/src/main/res/values/styles.xml +++ b/mobile/src/main/res/values/styles.xml @@ -43,5 +43,16 @@ 4dip 18sp + + diff --git a/mobile/src/main/res/xml/no_backup.xml b/mobile/src/main/res/xml/no_backup.xml new file mode 100644 index 00000000..3e2c649f --- /dev/null +++ b/mobile/src/main/res/xml/no_backup.xml @@ -0,0 +1,9 @@ + + + + + + + + +