Bump to minapi 28
85
README.md
@@ -1,7 +1,7 @@
|
||||
# VPN Hotspot
|
||||
|
||||
[](https://circleci.com/gh/Mygod/VPNHotspot)
|
||||
[](https://android-arsenal.com/api?level=21)
|
||||
[](https://android-arsenal.com/api?level=28)
|
||||
[](https://github.com/Mygod/VPNHotspot/releases)
|
||||
[](https://github.com/Mygod/VPNHotspot/search?l=kotlin)
|
||||
[](https://www.codacy.com/gh/Mygod/VPNHotspot/dashboard?utm_source=github.com&utm_medium=referral&utm_content=Mygod/VPNHotspot&utm_campaign=Badge_Grade)
|
||||
@@ -10,7 +10,7 @@
|
||||
Connecting things to your VPN made simple. Share your VPN connection over hotspot or repeater. (**root required**)
|
||||
|
||||
| Release channel | [GitHub](https://github.com/Mygod/VPNHotspot/releases) | [Google Play](https://play.google.com/store/apps/details?id=be.mygod.vpnhotspot) ([beta](https://play.google.com/apps/testing/be.mygod.vpnhotspot)) |
|
||||
|---------------------------------------------------------|:--------------------------------------------------------------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------:|
|
||||
|---------------------------------------------------------|:------------------------------------------------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------:|
|
||||
| Auto update | Email updates via watching releases | ✓ |
|
||||
| In-app update channel | GitHub | Google Play |
|
||||
| [Sponsor/Donation](https://github.com/sponsors/Mygod) | ✓ | Google Play In-App Purchases only |
|
||||
@@ -73,7 +73,7 @@ Default settings are picked to suit general use cases and maximize compatibility
|
||||
I find turning this option off sometimes works better for dummy VPNs like ad-blockers and socksifiers than Simple mode, e.g. Shadowsocks.
|
||||
But you should never use this for real VPNs like OpenVPN, etc.
|
||||
- Simple: Source address/port from downstream packets will be remapped and that's about it.
|
||||
- (since Android 9) Android Netd Service:
|
||||
- Android Netd Service:
|
||||
Let your system handle masquerade.
|
||||
Android system will do a few extra things to make things like FTP and tethering traffic counter work.
|
||||
You should probably not use this if you are trying to hide your tethering activity from your carrier.
|
||||
@@ -82,7 +82,7 @@ Default settings are picked to suit general use cases and maximize compatibility
|
||||
|
||||
* Disable IPv6 tethering: Turning this option on will disable IPv6 for system tethering. Useful for stopping IPv6 leaks
|
||||
as this app currently doesn't handle IPv6 VPN tethering (see [#6](https://github.com/Mygod/VPNHotspot/issues/6)).
|
||||
* (since Android 8.1) Tethering hardware acceleration:
|
||||
* Tethering hardware acceleration:
|
||||
This is a shortcut to the same setting in system Developer options.
|
||||
Turning this option off is probably a must for making VPN tethering over system tethering work,
|
||||
but it might also decrease your battery life while tethering is enabled.
|
||||
@@ -164,43 +164,41 @@ Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded
|
||||
* (since API 31) `Landroid/net/wifi/WifiClient;->getApInstanceIdentifier()Ljava/lang/String;,blocked`
|
||||
* (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`
|
||||
* (prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_2GHZ:I,lo-prio,max-target-o`
|
||||
* (prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_5GHZ:I,lo-prio,max-target-o`
|
||||
* (prior to API 30) `Landroid/net/wifi/WifiConfiguration;->AP_BAND_ANY:I,lo-prio,max-target-o`
|
||||
* (prior to API 30) `Landroid/net/wifi/WifiConfiguration;->apBand:I,unsupported`
|
||||
* (prior to API 30) `Landroid/net/wifi/WifiConfiguration;->apChannel:I,unsupported`
|
||||
* (prior to API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onNumClientsChanged(I)V,greylist-max-o`
|
||||
* `Landroid/net/wifi/WifiManager;->cancelLocalOnlyHotspotRequest()V,unsupported`
|
||||
* `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,unsupported`
|
||||
* (since API 28, prior to API 30) `Landroid/provider/Settings$Global;->SOFT_AP_TIMEOUT_ENABLED:Ljava/lang/String;,lo-prio,max-target-o`
|
||||
* (prior to API 30) `Landroid/provider/Settings$Global;->SOFT_AP_TIMEOUT_ENABLED:Ljava/lang/String;,lo-prio,max-target-o`
|
||||
* (prior to API 30) `Lcom/android/internal/R$array;->config_tether_bluetooth_regexs:I,max-target-q`
|
||||
* (prior to API 30) `Lcom/android/internal/R$array;->config_tether_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`
|
||||
* (prior to API 30) `Lcom/android/internal/R$integer;->config_wifi_framework_soft_ap_timeout_delay:I,greylist-max-o`
|
||||
* `Lcom/android/internal/R$string;->config_ethernet_iface_regex:I,lo-prio,max-target-o`
|
||||
* (since API 27) `Lcom/android/server/connectivity/tethering/OffloadHardwareInterface;->DEFAULT_TETHER_OFFLOAD_DISABLED:I`
|
||||
* (since API 30) `Lcom/android/server/wifi/WifiContext;->ACTION_RESOURCES_APK:Ljava/lang/String;`
|
||||
* (since API 29) `Lcom/android/server/wifi/p2p/WifiP2pServiceImpl;->ANONYMIZED_DEVICE_ADDRESS:Ljava/lang/String;`
|
||||
* (since API 30) `Lcom/android/server/SystemServer;->TETHERING_CONNECTOR_CLASS:Ljava/lang/String;`
|
||||
* (since API 26) `Ljava/lang/invoke/MethodHandles$Lookup;-><init>(Ljava/lang/Class;I)V,unsupported`
|
||||
* (since API 26) `Ljava/lang/invoke/MethodHandles$Lookup;->ALL_MODES:I,lo-prio,max-target-o`
|
||||
* `Ljava/lang/invoke/MethodHandles$Lookup;-><init>(Ljava/lang/Class;I)V,unsupported`
|
||||
* `Ljava/lang/invoke/MethodHandles$Lookup;->ALL_MODES:I,lo-prio,max-target-o`
|
||||
* (prior to API 29) `Ljava/net/InetAddress;->parseNumericAddress(Ljava/lang/String;)Ljava/net/InetAddress;,core-platform-api,max-target-p`
|
||||
|
||||
<details>
|
||||
<summary>Hidden whitelisted APIs: (same catch as above, however, things in this list are less likely to be broken)</summary>
|
||||
|
||||
* (since API 24) `Landroid/bluetooth/BluetoothPan;->isTetheringOn()Z,sdk,system-api,test-api`
|
||||
* (since API 24) `Landroid/bluetooth/BluetoothProfile;->PAN:I,sdk,system-api,test-api`
|
||||
* `Landroid/bluetooth/BluetoothPan;->isTetheringOn()Z,sdk,system-api,test-api`
|
||||
* `Landroid/bluetooth/BluetoothProfile;->PAN:I,sdk,system-api,test-api`
|
||||
* (since API 30) `Landroid/content/Context;->TETHERING_SERVICE:Ljava/lang/String;,sdk,system-api,test-api`
|
||||
* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;-><init>()V,sdk,system-api,test-api`
|
||||
* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringFailed()V,sdk,system-api,test-api`
|
||||
* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringStarted()V,sdk,system-api,test-api`
|
||||
* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager;->startTethering(IZLandroid/net/ConnectivityManager$OnStartTetheringCallback;Landroid/os/Handler;)V,sdk,system-api,test-api`
|
||||
* (since API 24, prior to API 30) `Landroid/net/ConnectivityManager;->stopTethering(I)V,sdk,system-api,test-api`
|
||||
* (prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;-><init>()V,sdk,system-api,test-api`
|
||||
* (prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringFailed()V,sdk,system-api,test-api`
|
||||
* (prior to API 30) `Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringStarted()V,sdk,system-api,test-api`
|
||||
* (prior to API 30) `Landroid/net/ConnectivityManager;->startTethering(IZLandroid/net/ConnectivityManager$OnStartTetheringCallback;Landroid/os/Handler;)V,sdk,system-api,test-api`
|
||||
* (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`
|
||||
@@ -217,13 +215,13 @@ Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded
|
||||
* (since API 30) `Landroid/net/TetheringManager$TetheringRequest$Builder;->setExemptFromEntitlementCheck(Z)Landroid/net/TetheringManager$TetheringRequest$Builder;,sdk,system-api,test-api`
|
||||
* (since API 30) `Landroid/net/TetheringManager$TetheringRequest$Builder;->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_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`
|
||||
* `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 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;->TETHERING_USB:I,sdk,system-api,test-api`
|
||||
* `Landroid/net/TetheringManager;->TETHERING_WIFI:I,sdk,system-api,test-api`
|
||||
* `Landroid/net/TetheringManager;->TETHER_ERROR_*:I,sdk,system-api,test-api`
|
||||
* (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`
|
||||
@@ -308,25 +306,25 @@ Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded
|
||||
* (since API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onConnectedClientsChanged(Ljava/util/List;)V,sdk,system-api,test-api`
|
||||
* (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`
|
||||
* `Landroid/net/wifi/WifiManager$SoftApCallback;->onStateChanged(II)V,sdk,system-api,test-api`
|
||||
* `Landroid/net/wifi/WifiManager;->EXTRA_WIFI_AP_FAILURE_REASON:Ljava/lang/String;,sdk,system-api,test-api`
|
||||
* `Landroid/net/wifi/WifiManager;->EXTRA_WIFI_AP_INTERFACE_NAME:Ljava/lang/String;,sdk,system-api,test-api`
|
||||
* `Landroid/net/wifi/WifiManager;->EXTRA_WIFI_AP_STATE:Ljava/lang/String;,sdk,system-api,test-api`
|
||||
* (since API 30) `Landroid/net/wifi/WifiManager;->SAP_CLIENT_BLOCK_REASON_CODE_*:I,sdk,system-api,test-api`
|
||||
* (since API 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`
|
||||
* `Landroid/net/wifi/WifiManager;->SAP_START_FAILURE_*:I,sdk,system-api,test-api`
|
||||
* `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_CHANGED_ACTION:Ljava/lang/String;,sdk,system-api,test-api`
|
||||
* `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_DISABLED:I,sdk,system-api,test-api`
|
||||
* `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_DISABLING:I,sdk,system-api,test-api`
|
||||
* `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_ENABLED:I,sdk,system-api,test-api`
|
||||
* `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_ENABLING:I,sdk,system-api,test-api`
|
||||
* `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_FAILED:I,sdk,system-api,test-api`
|
||||
* (since API 30) `Landroid/net/wifi/WifiManager;->getSoftApConfiguration()Landroid/net/wifi/SoftApConfiguration;,sdk,system-api,test-api`
|
||||
* (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`
|
||||
* `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/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`
|
||||
@@ -351,15 +349,14 @@ Nonexported system resources:
|
||||
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).
|
||||
DHCP server like `dnsmasq` is assumed to run and send DHCP packets as root.
|
||||
|
||||
Undocumented system binaries are all bundled and executable:
|
||||
|
||||
* (since API 24) `iptables-save`, `ip6tables-save`;
|
||||
* `iptables-save`, `ip6tables-save`;
|
||||
* `echo`;
|
||||
* `/system/bin/ip` (`monitor neigh rule unreachable`);
|
||||
* `ndc` (`ipfwd` since API 23, `nat` since API 28);
|
||||
* `ndc` (`ipfwd nat`);
|
||||
* `iptables`, `ip6tables` (with correct version corresponding to API level, `-nvx -L <chain>`);
|
||||
* `sh`;
|
||||
* `su`.
|
||||
|
||||
@@ -22,7 +22,7 @@ android {
|
||||
kotlinOptions.jvmTarget = javaVersion.toString()
|
||||
defaultConfig {
|
||||
applicationId = "be.mygod.vpnhotspot"
|
||||
minSdk = 21
|
||||
minSdk = 28
|
||||
targetSdk = 33
|
||||
resourceConfigurations.addAll(arrayOf("it", "pt-rBR", "ru", "zh-rCN", "zh-rTW"))
|
||||
versionCode = 1000
|
||||
|
||||
@@ -34,10 +34,10 @@
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"mac"
|
||||
],
|
||||
"autoGenerate": false
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
@@ -114,10 +114,10 @@
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
@@ -126,7 +126,8 @@
|
||||
"columnNames": [
|
||||
"previousId"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX `index_TrafficRecord_previousId` ON `${TABLE_NAME}` (`previousId`)"
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TrafficRecord_previousId` ON `${TABLE_NAME}` (`previousId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
@@ -144,9 +145,10 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"92a6c0406ed7265dbd98eb3c24095651\")"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '92a6c0406ed7265dbd98eb3c24095651')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -103,7 +103,7 @@ object UpdateChecker {
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e)
|
||||
} finally {
|
||||
conn.disconnectCompat()
|
||||
conn.disconnect()
|
||||
putLong(KEY_LAST_FETCHED, System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,8 +90,7 @@
|
||||
<service
|
||||
android:name=".LocalOnlyHotspotService"
|
||||
android:directBootAware="true"
|
||||
android:foregroundServiceType="location|connectedDevice"
|
||||
tools:targetApi="26"/>
|
||||
android:foregroundServiceType="location|connectedDevice"/>
|
||||
<service
|
||||
android:name=".RepeaterService"
|
||||
android:directBootAware="true"
|
||||
@@ -107,8 +106,7 @@
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_action_settings_input_antenna"
|
||||
android:label="@string/title_repeater"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:targetApi="24">
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
@@ -119,12 +117,10 @@
|
||||
<service
|
||||
android:name=".manage.LocalOnlyHotspotTileService"
|
||||
android:directBootAware="true"
|
||||
android:enabled="@bool/api_ge_26"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_action_perm_scan_wifi"
|
||||
android:label="@string/tethering_temp_hotspot"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:targetApi="26">
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
@@ -138,8 +134,7 @@
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_device_wifi_tethering"
|
||||
android:label="@string/tethering_manage_wifi"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:targetApi="24">
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
@@ -153,8 +148,7 @@
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_device_usb"
|
||||
android:label="@string/tethering_manage_usb"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:targetApi="24">
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
@@ -168,8 +162,7 @@
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_device_bluetooth"
|
||||
android:label="@string/tethering_manage_bluetooth"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:targetApi="24">
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
@@ -193,23 +186,6 @@
|
||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
android:value="true" />
|
||||
</service>
|
||||
<!--suppress DeprecatedClassUsageInspection -->
|
||||
<service
|
||||
android:name=".manage.TetheringTileService$WifiLegacy"
|
||||
android:directBootAware="true"
|
||||
android:enabled="@bool/api_lt_25"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_device_wifi_tethering"
|
||||
android:label="@string/tethering_manage_wifi_legacy"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:targetApi="24">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name=".BootReceiver"
|
||||
|
||||
@@ -15,7 +15,6 @@ import android.widget.Toast
|
||||
import androidx.annotation.Size
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.preference.PreferenceManager
|
||||
import be.mygod.librootkotlinx.NoShellException
|
||||
@@ -46,13 +45,11 @@ class App : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
app = this
|
||||
if (Build.VERSION.SDK_INT >= 24) @SuppressLint("RestrictedApi") {
|
||||
deviceStorage = DeviceStorageApp(this)
|
||||
// alternative to PreferenceManager.getDefaultSharedPreferencesName(this)
|
||||
deviceStorage.moveSharedPreferencesFrom(this, PreferenceManager(this).sharedPreferencesName)
|
||||
deviceStorage.moveDatabaseFrom(this, AppDatabase.DB_NAME)
|
||||
BootReceiver.migrateIfNecessary()
|
||||
} else deviceStorage = this
|
||||
Services.init { this }
|
||||
|
||||
// overhead of debug mode is minimal: https://github.com/Kotlin/kotlinx.coroutines/blob/f528898/docs/debugging.md#debug-mode
|
||||
@@ -62,7 +59,7 @@ class App : Application() {
|
||||
"REL" -> { }
|
||||
else -> FirebaseCrashlytics.getInstance().apply {
|
||||
setCustomKey("codename", codename)
|
||||
if (Build.VERSION.SDK_INT >= 23) setCustomKey("preview_sdk", Build.VERSION.PREVIEW_SDK_INT)
|
||||
setCustomKey("preview_sdk", Build.VERSION.PREVIEW_SDK_INT)
|
||||
}
|
||||
}
|
||||
Timber.plant(object : Timber.DebugTree() {
|
||||
@@ -115,19 +112,13 @@ class App : Application() {
|
||||
* https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/53e0284/service/java/com/android/server/wifi/WifiSettingsStore.java#228
|
||||
*/
|
||||
inline fun <reified T> startServiceWithLocation(context: Context) {
|
||||
val canStart = Build.VERSION.SDK_INT >= 33 || if (Build.VERSION.SDK_INT >= 28) {
|
||||
location?.isLocationEnabled == true
|
||||
} else @Suppress("DEPRECATION") {
|
||||
Settings.Secure.getInt(context.contentResolver, Settings.Secure.LOCATION_MODE,
|
||||
Settings.Secure.LOCATION_MODE_OFF) != Settings.Secure.LOCATION_MODE_OFF
|
||||
}
|
||||
if (canStart) ContextCompat.startForegroundService(context, Intent(context, T::class.java)) else try {
|
||||
if (Build.VERSION.SDK_INT < 33 && location?.isLocationEnabled != true) try {
|
||||
context.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
|
||||
Toast.makeText(context, R.string.tethering_location_off, Toast.LENGTH_LONG).show()
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
app.logEvent("location_settings") { param("message", e.toString()) }
|
||||
SmartSnackbar.make(R.string.tethering_location_off).show()
|
||||
}
|
||||
} else context.startForegroundService(Intent(context, T::class.java))
|
||||
}
|
||||
|
||||
lateinit var deviceStorage: Application
|
||||
@@ -145,10 +136,10 @@ class App : Application() {
|
||||
CustomTabsIntent.Builder().apply {
|
||||
setColorScheme(CustomTabsIntent.COLOR_SCHEME_SYSTEM)
|
||||
setColorSchemeParams(CustomTabsIntent.COLOR_SCHEME_LIGHT, CustomTabColorSchemeParams.Builder().apply {
|
||||
setToolbarColor(ContextCompat.getColor(app, R.color.light_colorPrimary))
|
||||
setToolbarColor(resources.getColor(R.color.light_colorPrimary, theme))
|
||||
}.build())
|
||||
setColorSchemeParams(CustomTabsIntent.COLOR_SCHEME_DARK, CustomTabColorSchemeParams.Builder().apply {
|
||||
setToolbarColor(ContextCompat.getColor(app, R.color.dark_colorPrimary))
|
||||
setToolbarColor(resources.getColor(R.color.dark_colorPrimary, theme))
|
||||
}.build())
|
||||
}.build()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.RequiresApi
|
||||
import be.mygod.librootkotlinx.toByteArray
|
||||
import be.mygod.librootkotlinx.toParcelable
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
@@ -74,7 +73,6 @@ class BootReceiver : BroadcastReceiver() {
|
||||
inline fun <reified T> add(value: Startable) = add(T::class.java.name, value)
|
||||
inline fun <reified T> delete() = delete(T::class.java.name)
|
||||
|
||||
@RequiresApi(24)
|
||||
fun migrateIfNecessary() {
|
||||
val oldFile = File(app.noBackupFilesDir, FILENAME)
|
||||
if (oldFile.canRead()) try {
|
||||
|
||||
@@ -94,7 +94,6 @@ class EBegFragment : AppCompatDialogFragment() {
|
||||
}.build()))
|
||||
}.build()) else SmartSnackbar.make(R.string.donations__google_android_market_not_supported).show()
|
||||
}
|
||||
@Suppress("ConstantConditionIf")
|
||||
if (BuildConfig.DONATIONS) (binding.donationsMoreStub.inflate() as Button).setOnClickListener {
|
||||
requireContext().launchUrl("https://mygod.be/donate/")
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import be.mygod.vpnhotspot.net.IpNeighbour
|
||||
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
||||
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
|
||||
@@ -21,7 +20,6 @@ import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
import java.net.Inet4Address
|
||||
|
||||
@RequiresApi(26)
|
||||
class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
||||
inner class Binder : android.os.Binder() {
|
||||
/**
|
||||
@@ -162,7 +160,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
||||
|
||||
override fun onIpNeighbourAvailable(neighbours: Collection<IpNeighbour>) {
|
||||
super.onIpNeighbourAvailable(neighbours)
|
||||
if (Build.VERSION.SDK_INT >= 28) timeoutMonitor?.onClientsChanged(neighbours.none {
|
||||
timeoutMonitor?.onClientsChanged(neighbours.none {
|
||||
it.ip is Inet4Address && it.state == IpNeighbour.State.VALID
|
||||
})
|
||||
}
|
||||
@@ -183,10 +181,8 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
||||
|
||||
private fun unregisterReceiver(exit: Boolean = false) {
|
||||
IpNeighbourMonitor.unregisterCallback(this)
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
timeoutMonitor?.close()
|
||||
timeoutMonitor = null
|
||||
}
|
||||
launch {
|
||||
routingManager?.stop()
|
||||
routingManager = null
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
@@ -50,16 +49,14 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
||||
setContentView(binding.root)
|
||||
binding.navigation.setOnItemSelectedListener(this)
|
||||
val badge = binding.navigation.getOrCreateBadge(R.id.navigation_clients).apply {
|
||||
backgroundColor = ContextCompat.getColor(this@MainActivity, R.color.colorSecondary)
|
||||
badgeTextColor = ContextCompat.getColor(this@MainActivity,
|
||||
androidx.appcompat.R.color.primary_text_default_material_light)
|
||||
backgroundColor = resources.getColor(R.color.colorSecondary, theme)
|
||||
badgeTextColor = resources.getColor(androidx.appcompat.R.color.primary_text_default_material_light, theme)
|
||||
}
|
||||
updateItem = binding.navigation.menu.findItem(R.id.navigation_update)
|
||||
updateItem.isCheckable = false
|
||||
updateBadge = binding.navigation.getOrCreateBadge(R.id.navigation_update).apply {
|
||||
backgroundColor = ContextCompat.getColor(this@MainActivity, R.color.colorSecondary)
|
||||
badgeTextColor = ContextCompat.getColor(this@MainActivity,
|
||||
androidx.appcompat.R.color.primary_text_default_material_light)
|
||||
backgroundColor = resources.getColor(R.color.colorSecondary, theme)
|
||||
badgeTextColor = resources.getColor(androidx.appcompat.R.color.primary_text_default_material_light, theme)
|
||||
}
|
||||
if (savedInstanceState == null) displayFragment(TetheringFragment())
|
||||
val model by viewModels<ClientViewModel>()
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.LocationManager
|
||||
import android.net.MacAddress
|
||||
import android.net.wifi.ScanResult
|
||||
import android.net.wifi.WpsInfo
|
||||
import android.net.wifi.p2p.*
|
||||
@@ -16,10 +17,10 @@ import android.os.Looper
|
||||
import android.provider.Settings
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.edit
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toLong
|
||||
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
|
||||
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
|
||||
import be.mygod.vpnhotspot.net.wifi.VendorElements
|
||||
@@ -98,17 +99,19 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
||||
var shutdownTimeoutMillis: Long
|
||||
get() = app.pref.getLong(KEY_SHUTDOWN_TIMEOUT, 0)
|
||||
set(value) = app.pref.edit { putLong(KEY_SHUTDOWN_TIMEOUT, value) }
|
||||
var deviceAddress: MacAddressCompat?
|
||||
var deviceAddress: MacAddress?
|
||||
get() = try {
|
||||
MacAddressCompat(app.pref.getLong(KEY_DEVICE_ADDRESS, MacAddressCompat.ANY_ADDRESS.addr)).run {
|
||||
validate()
|
||||
if (this == MacAddressCompat.ANY_ADDRESS) null else this
|
||||
MacAddressCompat(app.pref.getLong(KEY_DEVICE_ADDRESS, 2)).run {
|
||||
require(addr and ((1L shl 48) - 1).inv() == 0L)
|
||||
if (addr == 2L) null else toPlatform()
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.w(e)
|
||||
null
|
||||
}
|
||||
set(value) = app.pref.edit { putLong(KEY_DEVICE_ADDRESS, (value ?: MacAddressCompat.ANY_ADDRESS).addr) }
|
||||
set(value) = app.pref.edit {
|
||||
putLong(KEY_DEVICE_ADDRESS, (value ?: MacAddressCompat.ANY_ADDRESS).toLong())
|
||||
}
|
||||
@get:RequiresApi(33)
|
||||
@set:RequiresApi(33)
|
||||
var vendorElements: List<ScanResult.InformationElement>
|
||||
@@ -128,19 +131,17 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
||||
set(value) {
|
||||
field = value
|
||||
groupChanged(value)
|
||||
if (Build.VERSION.SDK_INT >= 28) value?.clientList?.let {
|
||||
timeoutMonitor?.onClientsChanged(it.isEmpty())
|
||||
}
|
||||
value?.clientList?.let { timeoutMonitor?.onClientsChanged(it.isEmpty()) }
|
||||
}
|
||||
val groupChanged = StickyEvent1 { group }
|
||||
|
||||
suspend fun obtainDeviceAddress(): MacAddressCompat? {
|
||||
suspend fun obtainDeviceAddress(): MacAddress? {
|
||||
return if (Build.VERSION.SDK_INT >= 29) p2pManager.requestDeviceAddress(channel ?: return null) ?: try {
|
||||
RootManager.use { it.execute(RepeaterCommands.RequestDeviceAddress()) }
|
||||
} catch (e: Exception) {
|
||||
Timber.d(e)
|
||||
null
|
||||
}?.let { MacAddressCompat(it.value) } else lastMac?.let { MacAddressCompat.fromString(it) }
|
||||
} else lastMac?.let { MacAddress.fromString(it) }
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi") // networkId is available since Android 4.2
|
||||
@@ -152,7 +153,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
||||
val ownedGroups = filter {
|
||||
if (!it.isGroupOwner) return@filter false
|
||||
val address = try {
|
||||
MacAddressCompat.fromString(it.owner.deviceAddress)
|
||||
MacAddress.fromString(it.owner.deviceAddress)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.w(e)
|
||||
return@filter true // assuming it was changed due to privacy
|
||||
@@ -220,7 +221,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
||||
@Parcelize
|
||||
class Starter : BootReceiver.Startable {
|
||||
override fun start(context: Context) {
|
||||
ContextCompat.startForegroundService(context, Intent(context, RepeaterService::class.java))
|
||||
context.startForegroundService(Intent(context, RepeaterService::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,7 +401,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
||||
val channel = channel ?: return START_NOT_STICKY.also { stopSelf() }
|
||||
status = Status.STARTING
|
||||
// bump self to foreground location service (API 29+) to use location later, also to avoid getting killed
|
||||
if (Build.VERSION.SDK_INT >= 26) showNotification()
|
||||
showNotification()
|
||||
launch {
|
||||
val filter = intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
|
||||
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)
|
||||
@@ -479,7 +480,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
||||
setGroupOperatingFrequency(SoftApConfigurationCompat.channelToFrequency(operatingBand, oc))
|
||||
}
|
||||
}
|
||||
setDeviceAddress(deviceAddress?.toPlatform())
|
||||
setDeviceAddress(deviceAddress)
|
||||
}.build(), listener)
|
||||
}
|
||||
}
|
||||
@@ -554,10 +555,8 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
||||
p2pPoller?.cancel()
|
||||
receiverRegistered = false
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
timeoutMonitor?.close()
|
||||
timeoutMonitor = null
|
||||
}
|
||||
routingManager?.stop()
|
||||
routingManager = null
|
||||
status = Status.IDLE
|
||||
@@ -574,7 +573,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
||||
app.pref.unregisterOnSharedPreferenceChangeListener(this)
|
||||
if (Build.VERSION.SDK_INT < 29) unregisterReceiver(deviceListener)
|
||||
status = Status.DESTROYED
|
||||
if (Build.VERSION.SDK_INT >= 27) channel?.close()
|
||||
channel?.close()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package be.mygod.vpnhotspot
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.os.Build
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.net.Routing
|
||||
@@ -15,15 +14,11 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
|
||||
companion object {
|
||||
private const val KEY_MASQUERADE_MODE = "service.masqueradeMode"
|
||||
var masqueradeMode: Routing.MasqueradeMode
|
||||
@TargetApi(28) get() = app.pref.run {
|
||||
get() = app.pref.run {
|
||||
getString(KEY_MASQUERADE_MODE, null)?.let { return@run Routing.MasqueradeMode.valueOf(it) }
|
||||
if (getBoolean("service.masquerade", true)) { // legacy settings
|
||||
Routing.MasqueradeMode.Simple
|
||||
} else Routing.MasqueradeMode.None
|
||||
}.let {
|
||||
// older app version enabled netd for everyone. should check again here
|
||||
if (Build.VERSION.SDK_INT >= 28 || it != Routing.MasqueradeMode.Netd) it
|
||||
else Routing.MasqueradeMode.Simple
|
||||
}
|
||||
set(value) = app.pref.edit().putString(KEY_MASQUERADE_MODE, value.name).apply()
|
||||
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
package be.mygod.vpnhotspot
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.*
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import java.util.*
|
||||
@@ -25,16 +20,15 @@ object ServiceNotification {
|
||||
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 {
|
||||
val builder = Notification.Builder(context, if (isInactive) CHANNEL_INACTIVE else CHANNEL_ACTIVE).apply {
|
||||
setWhen(0)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
color = ContextCompat.getColor(context, R.color.colorPrimary)
|
||||
setCategory(Notification.CATEGORY_SERVICE)
|
||||
setColor(context.resources.getColor(R.color.colorPrimary, context.theme))
|
||||
setContentTitle(context.getText(R.string.notification_tethering_title))
|
||||
setSmallIcon(R.drawable.ic_quick_settings_tile_on)
|
||||
setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
|
||||
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
priority = if (isInactive) NotificationCompat.PRIORITY_MIN else NotificationCompat.PRIORITY_LOW
|
||||
setVisibility(Notification.VISIBILITY_PUBLIC)
|
||||
}
|
||||
var lines = deviceCounts.map { (dev, size) ->
|
||||
context.resources.getQuantityString(R.plurals.notification_connected_devices, size, size, dev)
|
||||
@@ -45,13 +39,13 @@ object ServiceNotification {
|
||||
return if (lines.size <= 1) builder.setContentText(lines.singleOrNull()).build() else {
|
||||
val deviceCount = deviceCounts.sumOf { it.value }
|
||||
val interfaceCount = deviceCounts.size + inactive.size
|
||||
NotificationCompat.BigTextStyle(builder
|
||||
.setContentText(context.resources.getQuantityString(R.plurals.notification_connected_devices,
|
||||
deviceCount, deviceCount,
|
||||
Notification.BigTextStyle().apply {
|
||||
setBuilder(builder.setContentText(context.resources.getQuantityString(
|
||||
R.plurals.notification_connected_devices, deviceCount, deviceCount,
|
||||
context.resources.getQuantityString(R.plurals.notification_interfaces,
|
||||
interfaceCount, interfaceCount))))
|
||||
.bigText(lines.joinToString("\n"))
|
||||
.build()!!
|
||||
bigText(lines.joinToString("\n"))
|
||||
}.build()!!
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,13 +59,11 @@ object ServiceNotification {
|
||||
fun stopForeground(service: Service) = synchronized(this) {
|
||||
deviceCountsMap.remove(service) ?: return@synchronized
|
||||
val shutdown = deviceCountsMap.isEmpty()
|
||||
ServiceCompat.stopForeground(service,
|
||||
if (shutdown) ServiceCompat.STOP_FOREGROUND_REMOVE else ServiceCompat.STOP_FOREGROUND_DETACH)
|
||||
service.stopForeground(if (shutdown) Service.STOP_FOREGROUND_REMOVE else Service.STOP_FOREGROUND_DETACH)
|
||||
if (!shutdown) manager.notify(NOTIFICATION_ID, buildNotification(service))
|
||||
}
|
||||
|
||||
fun updateNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT >= 26) @TargetApi(26) {
|
||||
NotificationChannel(CHANNEL_ACTIVE,
|
||||
app.getText(R.string.notification_channel_tethering), NotificationManager.IMPORTANCE_LOW).apply {
|
||||
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
||||
@@ -86,5 +78,4 @@ object ServiceNotification {
|
||||
manager.deleteNotificationChannel("hotspot")
|
||||
manager.deleteNotificationChannel("repeater")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
||||
SummaryFallbackProvider(findPreference(UpstreamMonitor.KEY)!!)
|
||||
SummaryFallbackProvider(findPreference(FallbackUpstreamMonitor.KEY)!!)
|
||||
findPreference<TwoStatePreference>("system.enableTetherOffload")!!.apply {
|
||||
if (TetherOffloadManager.supported) {
|
||||
isChecked = TetherOffloadManager.enabled
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
if (TetherOffloadManager.enabled != newValue) viewLifecycleOwner.lifecycleScope.launchWhenCreated {
|
||||
@@ -64,7 +63,6 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
false
|
||||
}
|
||||
} else parent!!.removePreference(this)
|
||||
}
|
||||
findPreference<TwoStatePreference>(BootReceiver.KEY)!!.setOnPreferenceChangeListener { _, value ->
|
||||
BootReceiver.onUserSettingUpdated(value as Boolean)
|
||||
|
||||
@@ -2,9 +2,7 @@ package be.mygod.vpnhotspot
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.net.Routing
|
||||
import be.mygod.vpnhotspot.net.TetheringManager
|
||||
@@ -46,7 +44,7 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
|
||||
@Parcelize
|
||||
data class Starter(val monitored: ArrayList<String>) : BootReceiver.Startable {
|
||||
override fun start(context: Context) {
|
||||
ContextCompat.startForegroundService(context, Intent(context, TetheringService::class.java).apply {
|
||||
context.startForegroundService(Intent(context, TetheringService::class.java).apply {
|
||||
putStringArrayListExtra(EXTRA_ADD_INTERFACES_MONITOR, monitored)
|
||||
})
|
||||
}
|
||||
@@ -113,7 +111,7 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
BootReceiver.startIfEnabled()
|
||||
// call this first just in case we are shutting down immediately
|
||||
if (Build.VERSION.SDK_INT >= 26) updateNotification()
|
||||
updateNotification()
|
||||
launch {
|
||||
if (intent != null) {
|
||||
for (iface in intent.getStringArrayExtra(EXTRA_ADD_INTERFACES) ?: emptyArray()) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package be.mygod.vpnhotspot.client
|
||||
|
||||
import android.net.MacAddress
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.StrikethroughSpan
|
||||
@@ -9,7 +10,6 @@ import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.net.InetAddressComparator
|
||||
import be.mygod.vpnhotspot.net.IpNeighbour
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||
import be.mygod.vpnhotspot.net.TetherType
|
||||
import be.mygod.vpnhotspot.room.AppDatabase
|
||||
import be.mygod.vpnhotspot.room.ClientRecord
|
||||
@@ -18,7 +18,7 @@ import be.mygod.vpnhotspot.util.makeMacSpan
|
||||
import java.net.InetAddress
|
||||
import java.util.*
|
||||
|
||||
open class Client(val mac: MacAddressCompat, val iface: String) {
|
||||
open class Client(val mac: MacAddress, val iface: String) {
|
||||
companion object DiffCallback : DiffUtil.ItemCallback<Client>() {
|
||||
override fun areItemsTheSame(oldItem: Client, newItem: Client) =
|
||||
oldItem.iface == newItem.iface && oldItem.mac == newItem.mac
|
||||
@@ -65,7 +65,7 @@ open class Client(val mac: MacAddressCompat, val iface: String) {
|
||||
}.trimEnd()
|
||||
}
|
||||
|
||||
fun obtainRecord() = record.value ?: ClientRecord(mac.addr)
|
||||
fun obtainRecord() = record.value ?: ClientRecord(mac)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
|
||||
@@ -3,6 +3,7 @@ package be.mygod.vpnhotspot.client
|
||||
import android.content.ComponentName
|
||||
import android.content.IntentFilter
|
||||
import android.content.ServiceConnection
|
||||
import android.net.MacAddress
|
||||
import android.net.wifi.p2p.WifiP2pDevice
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
@@ -15,8 +16,6 @@ import androidx.lifecycle.ViewModel
|
||||
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
|
||||
@@ -38,7 +37,7 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
|
||||
|
||||
private var repeater: RepeaterService.Binder? = null
|
||||
private var p2p: Collection<WifiP2pDevice> = emptyList()
|
||||
private var wifiAp = emptyList<Pair<String, MacAddressCompat>>()
|
||||
private var wifiAp = emptyList<Pair<String, MacAddress>>()
|
||||
private var neighbours: Collection<IpNeighbour> = emptyList()
|
||||
val clients = MutableLiveData<List<Client>>()
|
||||
val fullMode = object : DefaultLifecycleObserver {
|
||||
@@ -51,10 +50,10 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
|
||||
}
|
||||
|
||||
private fun populateClients() {
|
||||
val clients = HashMap<Pair<String, MacAddressCompat>, Client>()
|
||||
val clients = HashMap<Pair<String, MacAddress>, Client>()
|
||||
repeater?.group?.`interface`?.let { p2pInterface ->
|
||||
for (client in p2p) {
|
||||
val addr = MacAddressCompat.fromString(client.deviceAddress!!)
|
||||
val addr = MacAddress.fromString(client.deviceAddress!!)
|
||||
clients[p2pInterface to addr] = object : Client(addr, p2pInterface) {
|
||||
override val icon: Int get() = TetherType.WIFI_P2P.icon
|
||||
}
|
||||
@@ -118,7 +117,7 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
|
||||
override fun onConnectedClientsChanged(clients: List<Parcelable>) {
|
||||
wifiAp = clients.mapNotNull {
|
||||
val client = WifiClient(it)
|
||||
client.apInstanceIdentifier?.run { this to client.macAddress.toCompat() }
|
||||
client.apInstanceIdentifier?.run { this to client.macAddress }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package be.mygod.vpnhotspot.client
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.net.MacAddress
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
@@ -30,7 +31,6 @@ import be.mygod.vpnhotspot.Empty
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.databinding.FragmentClientsBinding
|
||||
import be.mygod.vpnhotspot.databinding.ListitemClientBinding
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||
import be.mygod.vpnhotspot.net.TetherType
|
||||
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
||||
import be.mygod.vpnhotspot.net.monitor.TrafficRecorder
|
||||
@@ -41,18 +41,21 @@ import be.mygod.vpnhotspot.util.format
|
||||
import be.mygod.vpnhotspot.util.showAllowingStateLoss
|
||||
import be.mygod.vpnhotspot.util.toPluralInt
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.text.NumberFormat
|
||||
|
||||
class ClientsFragment : Fragment() {
|
||||
// FIXME: value class does not work with Parcelize
|
||||
@Parcelize
|
||||
data class NicknameArg(val mac: Long, val nickname: CharSequence) : Parcelable
|
||||
data class NicknameArg(val mac: MacAddress, val nickname: CharSequence) : Parcelable
|
||||
class NicknameDialogFragment : AlertDialogFragment<NicknameArg, Empty>() {
|
||||
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
|
||||
setView(R.layout.dialog_nickname)
|
||||
setTitle(getString(R.string.clients_nickname_title, MacAddressCompat(arg.mac).toString()))
|
||||
setTitle(getString(R.string.clients_nickname_title, arg.mac))
|
||||
setPositiveButton(android.R.string.ok, listener)
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
setNeutralButton(getText(R.string.clients_nickname_set_to_vendor), listener)
|
||||
@@ -64,7 +67,7 @@ class ClientsFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onClick(dialog: DialogInterface?, which: Int) {
|
||||
val mac = MacAddressCompat(arg.mac)
|
||||
val mac = arg.mac
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE -> {
|
||||
val newNickname = this.dialog!!.findViewById<EditText>(android.R.id.edit).text
|
||||
@@ -135,7 +138,7 @@ class ClientsFragment : Fragment() {
|
||||
R.id.nickname -> {
|
||||
val client = binding.client ?: return false
|
||||
NicknameDialogFragment().apply {
|
||||
arg(NicknameArg(client.mac.addr, client.nickname))
|
||||
arg(NicknameArg(client.mac, client.nickname))
|
||||
}.showAllowingStateLoss(parentFragmentManager)
|
||||
true
|
||||
}
|
||||
@@ -160,7 +163,7 @@ class ClientsFragment : Fragment() {
|
||||
withContext(Dispatchers.Unconfined) {
|
||||
StatsDialogFragment().apply {
|
||||
arg(StatsArg(client.title.value ?: return@withContext,
|
||||
AppDatabase.instance.trafficRecordDao.queryStats(client.mac.addr)))
|
||||
AppDatabase.instance.trafficRecordDao.queryStats(client.mac)))
|
||||
}.showAllowingStateLoss(parentFragmentManager)
|
||||
}
|
||||
}
|
||||
@@ -201,9 +204,7 @@ class ClientsFragment : Fragment() {
|
||||
check(newRecord.receivedPackets == oldRecord.receivedPackets)
|
||||
check(newRecord.receivedBytes == oldRecord.receivedBytes)
|
||||
} else {
|
||||
val rate = rates.computeIfAbsent(newRecord.downstream to MacAddressCompat(newRecord.mac)) {
|
||||
TrafficRate()
|
||||
}
|
||||
val rate = rates.computeIfAbsent(newRecord.downstream to newRecord.mac) { TrafficRate() }
|
||||
if (rate.send < 0 || rate.receive < 0) {
|
||||
rate.send = 0
|
||||
rate.receive = 0
|
||||
@@ -218,7 +219,7 @@ class ClientsFragment : Fragment() {
|
||||
|
||||
private lateinit var binding: FragmentClientsBinding
|
||||
private val adapter = ClientAdapter()
|
||||
private var rates = mutableMapOf<Pair<String, MacAddressCompat>, TrafficRate>()
|
||||
private var rates = mutableMapOf<Pair<String, MacAddress>, TrafficRate>()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = FragmentClientsBinding.inflate(inflater, container, false)
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package be.mygod.vpnhotspot.client
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Context
|
||||
import android.net.MacAddress
|
||||
import androidx.annotation.MainThread
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||
import be.mygod.vpnhotspot.room.AppDatabase
|
||||
import be.mygod.vpnhotspot.util.disconnectCompat
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
@@ -22,25 +21,26 @@ import java.net.URL
|
||||
* This class generates a default nickname for new clients.
|
||||
*/
|
||||
object MacLookup {
|
||||
class UnexpectedError(val mac: MacAddressCompat, val error: String) : JSONException("") {
|
||||
class UnexpectedError(val mac: MacAddress, val error: String) : JSONException("") {
|
||||
private fun formatMessage(context: Context) =
|
||||
context.getString(R.string.clients_mac_lookup_unexpected_error, mac.toOui(), error)
|
||||
context.getString(R.string.clients_mac_lookup_unexpected_error,
|
||||
mac.toByteArray().joinToString("") { "%02x".format(it) }.substring(0, 9), error)
|
||||
override val message get() = formatMessage(app.english)
|
||||
override fun getLocalizedMessage() = formatMessage(app)
|
||||
}
|
||||
|
||||
private val macLookupBusy = mutableMapOf<MacAddressCompat, Pair<HttpURLConnection, Job>>()
|
||||
private val macLookupBusy = mutableMapOf<MacAddress, Pair<HttpURLConnection, Job>>()
|
||||
// http://en.wikipedia.org/wiki/ISO_3166-1
|
||||
private val countryCodeRegex = "(?:^|[^A-Z])([A-Z]{2})[\\s\\d]*$".toRegex()
|
||||
|
||||
@MainThread
|
||||
fun abort(mac: MacAddressCompat) = macLookupBusy.remove(mac)?.let { (conn, job) ->
|
||||
fun abort(mac: MacAddress) = macLookupBusy.remove(mac)?.let { (conn, job) ->
|
||||
job.cancel()
|
||||
conn.disconnectCompat()
|
||||
conn.disconnect()
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun perform(mac: MacAddressCompat, explicit: Boolean = false) {
|
||||
fun perform(mac: MacAddress, explicit: Boolean = false) {
|
||||
abort(mac)
|
||||
val conn = URL("https://macvendors.co/api/$mac").openConnection() as HttpURLConnection
|
||||
macLookupBusy[mac] = conn to GlobalScope.launch(Dispatchers.IO) {
|
||||
@@ -70,7 +70,7 @@ object MacLookup {
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractCountry(mac: MacAddressCompat, response: String, obj: JSONObject): MatchResult? {
|
||||
private fun extractCountry(mac: MacAddress, response: String, obj: JSONObject): MatchResult? {
|
||||
countryCodeRegex.matchEntire(obj.optString("country"))?.also { return it }
|
||||
val address = obj.optString("address")
|
||||
if (address.isBlank()) return null
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package be.mygod.vpnhotspot.manage
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothProfile
|
||||
import android.content.BroadcastReceiver
|
||||
@@ -9,7 +8,6 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.net.TetheringManager
|
||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||
@@ -38,7 +36,6 @@ class BluetoothTethering(context: Context, private val adapter: BluetoothAdapter
|
||||
/**
|
||||
* https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/TetherSettings.java#215
|
||||
*/
|
||||
@TargetApi(24)
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
|
||||
BluetoothAdapter.STATE_ON -> {
|
||||
@@ -109,7 +106,6 @@ class BluetoothTethering(context: Context, private val adapter: BluetoothAdapter
|
||||
* https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/TetherSettings.java#384
|
||||
*/
|
||||
@SuppressLint("MissingPermission")
|
||||
@RequiresApi(24)
|
||||
fun start(callback: TetheringManager.StartTetheringCallback, context: Context) {
|
||||
if (pendingCallback == null) try {
|
||||
if (adapter.state == BluetoothAdapter.STATE_OFF) {
|
||||
@@ -123,7 +119,6 @@ class BluetoothTethering(context: Context, private val adapter: BluetoothAdapter
|
||||
pendingCallback = null
|
||||
}
|
||||
}
|
||||
@RequiresApi(24)
|
||||
fun stop(callback: (Exception) -> Unit) {
|
||||
TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, callback)
|
||||
stoppedByUser = true
|
||||
|
||||
@@ -2,7 +2,6 @@ package be.mygod.vpnhotspot.manage
|
||||
|
||||
import android.content.Intent
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.TetheringService
|
||||
@@ -25,7 +24,7 @@ class InterfaceManager(private val parent: TetheringFragment, val iface: String)
|
||||
val data = binding.data as Data
|
||||
if (data.active) context.startService(Intent(context, TetheringService::class.java)
|
||||
.putExtra(TetheringService.EXTRA_REMOVE_INTERFACE, iface))
|
||||
else ContextCompat.startForegroundService(context, Intent(context, TetheringService::class.java)
|
||||
else context.startForegroundService(Intent(context, TetheringService::class.java)
|
||||
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, arrayOf(iface)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
package be.mygod.vpnhotspot.manage
|
||||
|
||||
import android.service.quicksettings.Tile
|
||||
import androidx.annotation.RequiresApi
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.net.IpNeighbour
|
||||
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
||||
import be.mygod.vpnhotspot.util.KillableTileService
|
||||
import java.net.Inet4Address
|
||||
|
||||
@RequiresApi(24)
|
||||
abstract class IpNeighbourMonitoringTileService : KillableTileService(), IpNeighbourMonitor.Callback {
|
||||
private var neighbours: Collection<IpNeighbour> = emptyList()
|
||||
abstract fun updateTile()
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package be.mygod.vpnhotspot.manage
|
||||
|
||||
import android.Manifest
|
||||
import android.content.*
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.view.View
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.BuildConfig
|
||||
import be.mygod.vpnhotspot.LocalOnlyHotspotService
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
|
||||
@@ -16,7 +16,6 @@ import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
||||
import be.mygod.vpnhotspot.util.formatAddresses
|
||||
import java.net.NetworkInterface
|
||||
|
||||
@RequiresApi(26)
|
||||
class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager(), ServiceConnection {
|
||||
companion object {
|
||||
val permission = when {
|
||||
|
||||
@@ -6,12 +6,10 @@ import android.content.Intent
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.IBinder
|
||||
import android.service.quicksettings.Tile
|
||||
import androidx.annotation.RequiresApi
|
||||
import be.mygod.vpnhotspot.LocalOnlyHotspotService
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.util.stopAndUnbind
|
||||
|
||||
@RequiresApi(26)
|
||||
class LocalOnlyHotspotTileService : IpNeighbourMonitoringTileService() {
|
||||
private val tile by lazy { Icon.createWithResource(application, R.drawable.ic_action_perm_scan_wifi) }
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ object ManageBar : Manager() {
|
||||
private const val SETTINGS_2 = "com.android.settings.TetherSettings"
|
||||
|
||||
object Data : BaseObservable() {
|
||||
val offloadEnabled get() = TetherOffloadManager.supported && TetherOffloadManager.enabled
|
||||
val offloadEnabled get() = TetherOffloadManager.enabled
|
||||
}
|
||||
class ViewHolder(binding: ListitemManageBinding) : RecyclerView.ViewHolder(binding.root), View.OnClickListener {
|
||||
init {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package be.mygod.vpnhotspot.manage
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
@@ -18,7 +17,6 @@ abstract class Manager {
|
||||
const val VIEW_TYPE_USB = 3
|
||||
const val VIEW_TYPE_BLUETOOTH = 4
|
||||
const val VIEW_TYPE_ETHERNET = 8
|
||||
const val VIEW_TYPE_WIFI_LEGACY = 5
|
||||
const val VIEW_TYPE_LOCAL_ONLY_HOTSPOT = 6
|
||||
const val VIEW_TYPE_REPEATER = 7
|
||||
|
||||
@@ -33,11 +31,10 @@ abstract class Manager {
|
||||
VIEW_TYPE_WIFI,
|
||||
VIEW_TYPE_USB,
|
||||
VIEW_TYPE_BLUETOOTH,
|
||||
VIEW_TYPE_ETHERNET,
|
||||
VIEW_TYPE_WIFI_LEGACY -> {
|
||||
VIEW_TYPE_ETHERNET -> {
|
||||
TetherManager.ViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false))
|
||||
}
|
||||
VIEW_TYPE_LOCAL_ONLY_HOTSPOT -> @TargetApi(26) {
|
||||
VIEW_TYPE_LOCAL_ONLY_HOTSPOT -> {
|
||||
LocalOnlyHotspotManager.ViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false))
|
||||
}
|
||||
VIEW_TYPE_REPEATER -> RepeaterManager.ViewHolder(ListitemRepeaterBinding.inflate(inflater, parent, false))
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.ComponentName
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.net.MacAddress
|
||||
import android.net.wifi.SoftApConfiguration
|
||||
import android.net.wifi.p2p.WifiP2pGroup
|
||||
import android.os.Build
|
||||
@@ -16,16 +17,18 @@ import android.view.WindowManager
|
||||
import android.widget.EditText
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.databinding.BaseObservable
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import be.mygod.vpnhotspot.*
|
||||
import be.mygod.vpnhotspot.AlertDialogFragment
|
||||
import be.mygod.vpnhotspot.BR
|
||||
import be.mygod.vpnhotspot.Empty
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.RepeaterService
|
||||
import be.mygod.vpnhotspot.databinding.ListitemRepeaterBinding
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||
import be.mygod.vpnhotspot.net.wifi.P2pSupplicantConfiguration
|
||||
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
|
||||
import be.mygod.vpnhotspot.net.wifi.WifiApDialogFragment
|
||||
@@ -34,7 +37,11 @@ import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
||||
import be.mygod.vpnhotspot.util.formatAddresses
|
||||
import be.mygod.vpnhotspot.util.showAllowingStateLoss
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
import java.net.NetworkInterface
|
||||
@@ -91,7 +98,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
||||
val binder = binder
|
||||
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))
|
||||
context.startForegroundService(Intent(context, RepeaterService::class.java))
|
||||
} else parent.startRepeater.launch(if (Build.VERSION.SDK_INT >= 33) {
|
||||
Manifest.permission.NEARBY_WIFI_DEVICES
|
||||
} else Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
@@ -229,7 +236,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
||||
if (e !is CancellationException) Timber.w(e)
|
||||
passphrase = group.passphrase
|
||||
try {
|
||||
bssid = group.owner?.deviceAddress?.let(MacAddressCompat.Companion::fromString)
|
||||
bssid = group.owner?.deviceAddress?.let(MacAddress::fromString)
|
||||
} catch (_: IllegalArgumentException) { }
|
||||
this to true
|
||||
}
|
||||
|
||||
@@ -7,15 +7,12 @@ import android.graphics.drawable.Icon
|
||||
import android.net.wifi.p2p.WifiP2pGroup
|
||||
import android.os.IBinder
|
||||
import android.service.quicksettings.Tile
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.RepeaterService
|
||||
import be.mygod.vpnhotspot.util.KillableTileService
|
||||
import be.mygod.vpnhotspot.util.Services
|
||||
import be.mygod.vpnhotspot.util.stopAndUnbind
|
||||
|
||||
@RequiresApi(24)
|
||||
class RepeaterTileService : KillableTileService() {
|
||||
private val tile by lazy { Icon.createWithResource(application, R.drawable.ic_action_settings_input_antenna) }
|
||||
|
||||
@@ -37,8 +34,7 @@ class RepeaterTileService : KillableTileService() {
|
||||
val binder = binder
|
||||
if (binder == null) tapPending = true else when (binder.service.status) {
|
||||
RepeaterService.Status.ACTIVE -> binder.shutdown()
|
||||
RepeaterService.Status.IDLE -> ContextCompat.startForegroundService(this,
|
||||
Intent(this, RepeaterService::class.java))
|
||||
RepeaterService.Status.IDLE -> startForegroundService(Intent(this, RepeaterService::class.java))
|
||||
else -> { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.annotation.TargetApi
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
@@ -27,7 +26,6 @@ import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
|
||||
import be.mygod.vpnhotspot.net.TetherType
|
||||
import be.mygod.vpnhotspot.net.TetheringManager
|
||||
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.*
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
@@ -57,7 +55,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
||||
override fun onClick(v: View?) {
|
||||
val manager = manager!!
|
||||
val mainActivity = manager.parent.activity as MainActivity
|
||||
if (Build.VERSION.SDK_INT >= 23 && !Settings.System.canWrite(mainActivity)) try {
|
||||
if (!Settings.System.canWrite(mainActivity)) try {
|
||||
manager.parent.startActivity(Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS,
|
||||
"package:${mainActivity.packageName}".toUri()))
|
||||
return
|
||||
@@ -134,41 +132,29 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
||||
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)
|
||||
if (e.cause !is SecurityException) Timber.w(e) else Timber.d(e)
|
||||
e.readableMessage
|
||||
}
|
||||
}
|
||||
data.notifyChange()
|
||||
}
|
||||
|
||||
@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 info = emptyList<Parcelable>()
|
||||
private var capability: Parcelable? = null
|
||||
|
||||
init {
|
||||
if (Build.VERSION.SDK_INT >= 23) parent.viewLifecycleOwner.lifecycle.addObserver(this)
|
||||
parent.viewLifecycleOwner.lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
if (Build.VERSION.SDK_INT < 28) {
|
||||
parent.requireContext().registerReceiver(receiver,
|
||||
IntentFilter(WifiApManager.WIFI_AP_STATE_CHANGED_ACTION))
|
||||
} else WifiApCommands.registerSoftApCallback(this)
|
||||
WifiApCommands.registerSoftApCallback(this)
|
||||
}
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
if (Build.VERSION.SDK_INT < 28) {
|
||||
parent.requireContext().unregisterReceiver(receiver)
|
||||
} else WifiApCommands.unregisterSoftApCallback(this)
|
||||
WifiApCommands.unregisterSoftApCallback(this)
|
||||
}
|
||||
|
||||
override fun onStateChanged(state: Int, failureReason: Int) {
|
||||
@@ -277,7 +263,6 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
||||
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this)
|
||||
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI, this::onException)
|
||||
}
|
||||
@RequiresApi(24)
|
||||
class Usb(parent: TetheringFragment) : TetherManager(parent) {
|
||||
override val title get() = parent.getString(R.string.tethering_manage_usb)
|
||||
override val tetherType get() = TetherType.USB
|
||||
@@ -286,7 +271,6 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
||||
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_USB, true, this)
|
||||
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_USB, this::onException)
|
||||
}
|
||||
@RequiresApi(24)
|
||||
class Bluetooth(parent: TetheringFragment, adapter: BluetoothAdapter) :
|
||||
TetherManager(parent), DefaultLifecycleObserver {
|
||||
private val tethering = BluetoothTethering(parent.requireContext(), adapter) { data.notifyChange() }
|
||||
@@ -332,23 +316,4 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
||||
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_ETHERNET, true, this)
|
||||
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_ETHERNET, this::onException)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Not usable since API 26, malfunctioning on API 25")
|
||||
class WifiLegacy(parent: TetheringFragment) : TetherManager(parent) {
|
||||
override val title get() = parent.getString(R.string.tethering_manage_wifi_legacy)
|
||||
override val tetherType get() = TetherType.WIFI
|
||||
override val type get() = VIEW_TYPE_WIFI_LEGACY
|
||||
|
||||
override fun start() = try {
|
||||
WifiApManager.start()
|
||||
} catch (e: Exception) {
|
||||
onException(e)
|
||||
}
|
||||
override fun stop() = try {
|
||||
WifiApManager.stop()
|
||||
} catch (e: Exception) {
|
||||
onException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import android.view.ViewGroup
|
||||
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
|
||||
@@ -51,16 +50,13 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
||||
inner class ManagerAdapter : ListAdapter<Manager, RecyclerView.ViewHolder>(Manager),
|
||||
TetheringManager.TetheringEventCallback {
|
||||
internal val repeaterManager by lazy { RepeaterManager(this@TetheringFragment) }
|
||||
@get:RequiresApi(26)
|
||||
internal val localOnlyHotspotManager by lazy @TargetApi(26) { LocalOnlyHotspotManager(this@TetheringFragment) }
|
||||
@get:RequiresApi(24)
|
||||
internal val localOnlyHotspotManager by lazy { LocalOnlyHotspotManager(this@TetheringFragment) }
|
||||
internal val bluetoothManager by lazy {
|
||||
if (Build.VERSION.SDK_INT >= 24) requireContext().getSystemService<BluetoothManager>()?.adapter?.let {
|
||||
requireContext().getSystemService<BluetoothManager>()?.adapter?.let {
|
||||
TetherManager.Bluetooth(this@TetheringFragment, it)
|
||||
} else null
|
||||
}
|
||||
@get:RequiresApi(24)
|
||||
private val tetherManagers by lazy @TargetApi(24) {
|
||||
}
|
||||
private val tetherManagers by lazy {
|
||||
listOfNotNull(
|
||||
TetherManager.Wifi(this@TetheringFragment),
|
||||
TetherManager.Usb(this@TetheringFragment),
|
||||
@@ -69,7 +65,6 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val ethernetManager by lazy @TargetApi(30) { TetherManager.Ethernet(this@TetheringFragment) }
|
||||
private val wifiManagerLegacy by lazy { TetherManager.WifiLegacy(this@TetheringFragment) }
|
||||
|
||||
var activeIfaces = emptyList<String>()
|
||||
var localOnlyIfaces = emptyList<String>()
|
||||
@@ -106,24 +101,18 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
||||
|
||||
val list = ArrayList<Manager>()
|
||||
if (Services.p2p != null) list.add(repeaterManager)
|
||||
if (Build.VERSION.SDK_INT >= 26) list.add(localOnlyHotspotManager)
|
||||
list.add(localOnlyHotspotManager)
|
||||
val monitoredIfaces = binder?.monitoredIfaces ?: emptyList()
|
||||
updateMonitorList(activeIfaces - monitoredIfaces)
|
||||
updateMonitorList(activeIfaces - monitoredIfaces.toSet())
|
||||
list.addAll((activeIfaces + monitoredIfaces).toSortedSet()
|
||||
.map { InterfaceManager(this@TetheringFragment, it) })
|
||||
list.add(ManageBar)
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
list.addAll(tetherManagers)
|
||||
tetherManagers.forEach { it.updateErrorMessage(erroredIfaces, lastErrors) }
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
list.add(ethernetManager)
|
||||
ethernetManager.updateErrorMessage(erroredIfaces, lastErrors)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < 26) {
|
||||
list.add(wifiManagerLegacy)
|
||||
wifiManagerLegacy.onTetheringStarted()
|
||||
}
|
||||
submitList(list) { deferred.complete(list) }
|
||||
}
|
||||
|
||||
@@ -140,7 +129,6 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
||||
R.string.repeater_missing_location_permissions, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
@RequiresApi(26)
|
||||
val startLocalOnlyHotspot = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||
adapter.localOnlyHotspotManager.start(requireContext())
|
||||
}
|
||||
@@ -170,7 +158,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
||||
item.subMenu!!.apply {
|
||||
clear()
|
||||
for (iface in canMonitor.sorted()) add(iface).setOnMenuItemClickListener {
|
||||
ContextCompat.startForegroundService(activity, Intent(activity, TetheringService::class.java)
|
||||
activity.startForegroundService(Intent(activity, TetheringService::class.java)
|
||||
.putExtra(TetheringService.EXTRA_ADD_INTERFACE_MONITOR, iface))
|
||||
true
|
||||
}
|
||||
@@ -183,14 +171,14 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
||||
R.id.configuration -> item.subMenu!!.run {
|
||||
findItem(R.id.configuration_repeater).isNotGone = Services.p2p != null
|
||||
findItem(R.id.configuration_temp_hotspot).isNotGone =
|
||||
Build.VERSION.SDK_INT >= 26 && adapter.localOnlyHotspotManager.binder?.configuration != null
|
||||
adapter.localOnlyHotspotManager.binder?.configuration != null
|
||||
true
|
||||
}
|
||||
R.id.configuration_repeater -> {
|
||||
adapter.repeaterManager.configure()
|
||||
true
|
||||
}
|
||||
R.id.configuration_temp_hotspot -> @TargetApi(26) {
|
||||
R.id.configuration_temp_hotspot -> {
|
||||
WifiApDialogFragment().apply {
|
||||
arg(WifiApDialogFragment.Arg(adapter.localOnlyHotspotManager.binder?.configuration ?: return false,
|
||||
readOnly = true))
|
||||
@@ -216,7 +204,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
||||
null
|
||||
} catch (eRoot: Exception) {
|
||||
eRoot.addSuppressed(e)
|
||||
if (Build.VERSION.SDK_INT !in 26..29 || eRoot.getRootCause() !is SecurityException) {
|
||||
if (Build.VERSION.SDK_INT >= 29 || eRoot.getRootCause() !is SecurityException) {
|
||||
Timber.w(eRoot)
|
||||
}
|
||||
SmartSnackbar.make(eRoot).show()
|
||||
@@ -245,7 +233,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
||||
if (which == DialogInterface.BUTTON_POSITIVE) viewLifecycleOwner.lifecycleScope.launchWhenCreated {
|
||||
val configuration = ret!!.configuration
|
||||
@Suppress("DEPRECATION")
|
||||
if (Build.VERSION.SDK_INT in 28 until 30 &&
|
||||
if (Build.VERSION.SDK_INT < 30 &&
|
||||
configuration.isAutoShutdownEnabled != TetherTimeoutMonitor.enabled) try {
|
||||
TetherTimeoutMonitor.setEnabled(configuration.isAutoShutdownEnabled)
|
||||
} catch (e: Exception) {
|
||||
@@ -299,7 +287,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (Build.VERSION.SDK_INT >= 27) ManageBar.Data.notifyChange()
|
||||
ManageBar.Data.notifyChange()
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
|
||||
@@ -11,14 +11,12 @@ import android.os.IBinder
|
||||
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
|
||||
import be.mygod.vpnhotspot.net.TetheringManager
|
||||
import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces
|
||||
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||
import be.mygod.vpnhotspot.util.readableMessage
|
||||
import be.mygod.vpnhotspot.util.stopAndUnbind
|
||||
@@ -27,7 +25,6 @@ import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@RequiresApi(24)
|
||||
sealed class TetheringTileService : IpNeighbourMonitoringTileService(), TetheringManager.StartTetheringCallback {
|
||||
protected val tileOff by lazy { Icon.createWithResource(application, icon) }
|
||||
protected val tileOn by lazy { Icon.createWithResource(application, R.drawable.ic_quick_settings_tile_on) }
|
||||
@@ -110,7 +107,7 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
|
||||
stop()
|
||||
} catch (e: Exception) {
|
||||
onException(e)
|
||||
} else ContextCompat.startForegroundService(this, Intent(this, TetheringService::class.java)
|
||||
} else startForegroundService(Intent(this, TetheringService::class.java)
|
||||
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray()))
|
||||
}
|
||||
}
|
||||
@@ -211,7 +208,7 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
|
||||
stop()
|
||||
} catch (e: Exception) {
|
||||
onException(e)
|
||||
} else ContextCompat.startForegroundService(this, Intent(this, TetheringService::class.java)
|
||||
} else startForegroundService(Intent(this, TetheringService::class.java)
|
||||
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray()))
|
||||
}
|
||||
}
|
||||
@@ -228,23 +225,4 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
|
||||
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_ETHERNET, true, this)
|
||||
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_ETHERNET, this::onException)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Not usable since API 25")
|
||||
class WifiLegacy : TetheringTileService() {
|
||||
override val labelString get() = R.string.tethering_manage_wifi_legacy
|
||||
override val tetherType get() = TetherType.WIFI
|
||||
override val icon get() = R.drawable.ic_device_wifi_tethering
|
||||
|
||||
override fun start() = try {
|
||||
WifiApManager.start()
|
||||
} catch (e: Exception) {
|
||||
onException(e)
|
||||
}
|
||||
override fun stop() = try {
|
||||
WifiApManager.stop()
|
||||
} catch (e: Exception) {
|
||||
onException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import android.net.MacAddress
|
||||
import android.os.Build
|
||||
import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
@@ -15,7 +16,7 @@ import java.io.IOException
|
||||
import java.net.Inet4Address
|
||||
import java.net.InetAddress
|
||||
|
||||
data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddressCompat, val state: State) {
|
||||
data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddress, val state: State) {
|
||||
enum class State {
|
||||
INCOMPLETE, VALID, FAILED, DELETING
|
||||
}
|
||||
@@ -65,7 +66,7 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
|
||||
return devs.map { IpNeighbour(ip, it, lladdr, State.DELETING) }
|
||||
}
|
||||
if (match.groups[4] != null) try {
|
||||
lladdr = MacAddressCompat.fromString(match.groupValues[4])
|
||||
lladdr = MacAddress.fromString(match.groupValues[4])
|
||||
} catch (e: IllegalArgumentException) {
|
||||
if (state != State.INCOMPLETE && state != State.DELETING) {
|
||||
Timber.w(IOException("Failed to find MAC address for $line", e))
|
||||
@@ -79,7 +80,7 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
|
||||
val list = arp()
|
||||
.asSequence()
|
||||
.filter { parseNumericAddress(it[ARP_IP_ADDRESS]) == ip && it[ARP_DEVICE] in devs }
|
||||
.map { MacAddressCompat.fromString(it[ARP_HW_ADDRESS]) }
|
||||
.map { MacAddress.fromString(it[ARP_HW_ADDRESS]) }
|
||||
.filter { it != MacAddressCompat.ALL_ZEROS_ADDRESS }
|
||||
.distinct()
|
||||
.toList()
|
||||
@@ -138,5 +139,4 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
|
||||
data class IpDev(val ip: InetAddress, val dev: String) {
|
||||
override fun toString() = "$ip%$dev"
|
||||
}
|
||||
@Suppress("FunctionName")
|
||||
fun IpDev(neighbour: IpNeighbour) = IpDev(neighbour.ip, neighbour.dev)
|
||||
|
||||
@@ -1,95 +1,34 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import android.net.MacAddress
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
/**
|
||||
* Compat support class for [MacAddress].
|
||||
* This used to be a compat support class for [MacAddress].
|
||||
* Now it is just a convenient class for backwards compatibility.
|
||||
*/
|
||||
@JvmInline
|
||||
value class MacAddressCompat(val addr: Long) {
|
||||
companion object {
|
||||
private const val ETHER_ADDR_LEN = 6
|
||||
/**
|
||||
* The MacAddress zero MAC address.
|
||||
*
|
||||
* Not publicly exposed or treated specially since the OUI 00:00:00 is registered.
|
||||
* @hide
|
||||
*/
|
||||
val ALL_ZEROS_ADDRESS = MacAddressCompat(0)
|
||||
val ANY_ADDRESS = MacAddressCompat(2)
|
||||
val ALL_ZEROS_ADDRESS = MacAddress.fromBytes(byteArrayOf(0, 0, 0, 0, 0, 0))
|
||||
val ANY_ADDRESS = MacAddress.fromBytes(byteArrayOf(2, 0, 0, 0, 0, 0))
|
||||
|
||||
/**
|
||||
* Creates a MacAddress from the given byte array representation.
|
||||
* A valid byte array representation for a MacAddress is a non-null array of length 6.
|
||||
*
|
||||
* @param addr a byte array representation of a MAC address.
|
||||
* @return the MacAddress corresponding to the given byte array representation.
|
||||
* @throws IllegalArgumentException if the given byte array is not a valid representation.
|
||||
*/
|
||||
fun fromBytes(addr: ByteArray): MacAddressCompat {
|
||||
val buffer = when (addr.size) {
|
||||
ETHER_ADDR_LEN -> ByteBuffer.allocate(Long.SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN).put(addr)
|
||||
8 -> {
|
||||
require(addr.take(2).all { it == 0.toByte() }) {
|
||||
"Unrecognized padding " + addr.joinToString(":") { "%02x".format(it) }
|
||||
}
|
||||
ByteBuffer.allocate(Long.SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN).put(addr, 2, ETHER_ADDR_LEN)
|
||||
}
|
||||
else -> return fromString(String(addr))
|
||||
}
|
||||
buffer.rewind()
|
||||
return MacAddressCompat(buffer.long)
|
||||
}
|
||||
/**
|
||||
* Creates a MacAddress from the given String representation. A valid String representation
|
||||
* for a MacAddress is a series of 6 values in the range [0,ff] printed in hexadecimal
|
||||
* and joined by ':' characters.
|
||||
*
|
||||
* @param addr a String representation of a MAC address.
|
||||
* @return the MacAddress corresponding to the given String representation.
|
||||
* @throws IllegalArgumentException if the given String is not a valid representation.
|
||||
*/
|
||||
fun fromString(addr: String) = ByteBuffer.allocate(Long.SIZE_BYTES).run {
|
||||
fun MacAddress.toLong() = ByteBuffer.allocate(Long.SIZE_BYTES).apply {
|
||||
order(ByteOrder.LITTLE_ENDIAN)
|
||||
var start = 0
|
||||
var i = 0
|
||||
while (position() < ETHER_ADDR_LEN && start < addr.length) {
|
||||
val end = i
|
||||
if (addr.getOrElse(i) { ':' } == ':') ++i else if (i < start + 2) {
|
||||
++i
|
||||
continue
|
||||
}
|
||||
put(if (start == end) 0 else try {
|
||||
Integer.parseInt(addr.substring(start, end), 16).toByte()
|
||||
} catch (e: NumberFormatException) {
|
||||
throw IllegalArgumentException(e)
|
||||
})
|
||||
start = i
|
||||
}
|
||||
require(position() == ETHER_ADDR_LEN) { "MAC address too short" }
|
||||
put(toByteArray())
|
||||
rewind()
|
||||
MacAddressCompat(long)
|
||||
}.long
|
||||
}
|
||||
|
||||
@RequiresApi(28)
|
||||
fun MacAddress.toCompat() = fromBytes(toByteArray())
|
||||
}
|
||||
|
||||
fun validate() = require(addr and ((1L shl 48) - 1).inv() == 0L)
|
||||
|
||||
fun toList() = ByteBuffer.allocate(8).run {
|
||||
fun toPlatform() = MacAddress.fromBytes(ByteBuffer.allocate(8).run {
|
||||
order(ByteOrder.LITTLE_ENDIAN)
|
||||
putLong(addr)
|
||||
array().take(6)
|
||||
}
|
||||
|
||||
@RequiresApi(28)
|
||||
fun toPlatform() = MacAddress.fromBytes(toList().toByteArray())
|
||||
|
||||
override fun toString() = toList().joinToString(":") { "%02x".format(it) }
|
||||
|
||||
fun toOui() = toList().joinToString("") { "%02x".format(it) }.substring(0, 9)
|
||||
}.toByteArray())
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.net.LinkProperties
|
||||
import android.net.MacAddress
|
||||
import android.net.RouteInfo
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.system.Os
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor
|
||||
@@ -15,7 +13,9 @@ import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
|
||||
import be.mygod.vpnhotspot.room.AppDatabase
|
||||
import be.mygod.vpnhotspot.root.RootManager
|
||||
import be.mygod.vpnhotspot.root.RoutingCommands
|
||||
import be.mygod.vpnhotspot.util.*
|
||||
import be.mygod.vpnhotspot.util.RootSession
|
||||
import be.mygod.vpnhotspot.util.allInterfaceNames
|
||||
import be.mygod.vpnhotspot.util.allRoutes
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import timber.log.Timber
|
||||
@@ -125,7 +125,6 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
|
||||
*
|
||||
* Source: https://android.googlesource.com/platform/system/netd/+/3b47c793ff7ade843b1d85a9be8461c3b4dc693e
|
||||
*/
|
||||
@RequiresApi(28)
|
||||
Netd,
|
||||
}
|
||||
|
||||
@@ -155,14 +154,14 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
|
||||
* The only case when upstream is null is on API 23- and we are using system default rules.
|
||||
*/
|
||||
inner class Subrouting(priority: Int, val upstream: String) {
|
||||
val ifindex = if (upstream.isEmpty()) 0 else if_nametoindex(upstream).also {
|
||||
val ifindex = if (upstream.isEmpty()) 0 else Os.if_nametoindex(upstream).also {
|
||||
if (it <= 0) throw InterfaceGoneException(upstream)
|
||||
}
|
||||
val transaction = RootSession.beginTransaction().safeguard {
|
||||
if (upstream.isEmpty()) {
|
||||
ipRule("goto $RULE_PRIORITY_TETHERING", priority) // skip unreachable rule
|
||||
} else ipRuleLookup(ifindex, priority)
|
||||
@TargetApi(28) when (masqueradeMode) {
|
||||
when (masqueradeMode) {
|
||||
MasqueradeMode.None -> { } // nothing to be done here
|
||||
MasqueradeMode.Simple -> {
|
||||
// note: specifying -i wouldn't work for POSTROUTING
|
||||
@@ -225,16 +224,10 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
|
||||
updateDnsRoute()
|
||||
}
|
||||
}
|
||||
private val fallbackUpstream = object : Upstream(RULE_PRIORITY_UPSTREAM_FALLBACK) {
|
||||
@SuppressLint("NewApi")
|
||||
override fun onFallback() = onAvailable(LinkProperties().apply {
|
||||
interfaceName = ""
|
||||
setDnsServers(listOf(parseNumericAddress("8.8.8.8")))
|
||||
})
|
||||
}
|
||||
private val fallbackUpstream = Upstream(RULE_PRIORITY_UPSTREAM_FALLBACK)
|
||||
private val upstream = Upstream(RULE_PRIORITY_UPSTREAM)
|
||||
|
||||
private inner class Client(private val ip: Inet4Address, mac: MacAddressCompat) : AutoCloseable {
|
||||
private inner class Client(private val ip: Inet4Address, mac: MacAddress) : AutoCloseable {
|
||||
private val transaction = RootSession.beginTransaction().safeguard {
|
||||
val address = ip.hostAddress
|
||||
iptablesInsert("vpnhotspot_acl -i $downstream -s $address -j ACCEPT")
|
||||
@@ -287,7 +280,7 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
|
||||
* but may be broken when system tethering shutdown before local-only interfaces.
|
||||
*/
|
||||
fun ipForward() {
|
||||
if (Build.VERSION.SDK_INT >= 23) try {
|
||||
try {
|
||||
transaction.ndc("ipfwd", "ndc ipfwd enable vpnhotspot_$downstream",
|
||||
"ndc ipfwd disable vpnhotspot_$downstream")
|
||||
return
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.root.SettingsGlobalPut
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* It's hard to change tethering rules with Tethering hardware acceleration enabled for now.
|
||||
@@ -15,19 +13,6 @@ import timber.log.Timber
|
||||
* https://android.googlesource.com/platform/hardware/qcom/data/ipacfg-mgr/+/master/msm8998/ipacm/src/IPACM_OffloadManager.cpp
|
||||
*/
|
||||
object TetherOffloadManager {
|
||||
val supported by lazy {
|
||||
Build.VERSION.SDK_INT >= 27 || try {
|
||||
Settings.Global::class.java.getDeclaredField("TETHER_OFFLOAD_DISABLED").get(null).let {
|
||||
require(it == TETHER_OFFLOAD_DISABLED) { "Unknown field $it" }
|
||||
}
|
||||
true
|
||||
} catch (_: NoSuchFieldException) {
|
||||
false
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e)
|
||||
false
|
||||
}
|
||||
}
|
||||
private const val TETHER_OFFLOAD_DISABLED = "tether_offload_disabled"
|
||||
val enabled get() = Settings.Global.getInt(app.contentResolver, TETHER_OFFLOAD_DISABLED, 0) == 0
|
||||
suspend fun setEnabled(value: Boolean) = SettingsGlobalPut.int(TETHER_OFFLOAD_DISABLED, if (value) 0 else 1)
|
||||
|
||||
@@ -94,9 +94,7 @@ object TetheringManager {
|
||||
* https://android.googlesource.com/platform/frameworks/base.git/+/2a091d7aa0c174986387e5d56bf97a87fe075bdb%5E%21/services/java/com/android/server/connectivity/Tethering.java
|
||||
*/
|
||||
const val ACTION_TETHER_STATE_CHANGED = "android.net.conn.TETHER_STATE_CHANGED"
|
||||
@RequiresApi(26)
|
||||
private const val EXTRA_ACTIVE_LOCAL_ONLY_LEGACY = "localOnlyArray"
|
||||
private const val EXTRA_ACTIVE_TETHER_LEGACY = "activeArray"
|
||||
/**
|
||||
* gives a String[] listing all the interfaces currently in local-only
|
||||
* mode (ie, has DHCPv4+IPv6-ULA support and no packet forwarding)
|
||||
@@ -107,7 +105,6 @@ object TetheringManager {
|
||||
* gives a String[] listing all the interfaces currently tethered
|
||||
* (ie, has DHCPv4 support and packets potentially forwarded/NATed)
|
||||
*/
|
||||
@RequiresApi(26)
|
||||
private const val EXTRA_ACTIVE_TETHER = "tetherArray"
|
||||
/**
|
||||
* gives a String[] listing all the interfaces we tried to tether and
|
||||
@@ -131,7 +128,6 @@ object TetheringManager {
|
||||
* Wifi tethering type.
|
||||
* @see [startTethering].
|
||||
*/
|
||||
@RequiresApi(24)
|
||||
const val TETHERING_WIFI = 0
|
||||
/**
|
||||
* USB tethering type.
|
||||
@@ -141,7 +137,6 @@ object TetheringManager {
|
||||
* Source: https://android.googlesource.com/platform/frameworks/base/+/7ca5d3a/services/usb/java/com/android/server/usb/UsbService.java#389
|
||||
* @see startTethering
|
||||
*/
|
||||
@RequiresApi(24)
|
||||
const val TETHERING_USB = 1
|
||||
/**
|
||||
* Bluetooth tethering type.
|
||||
@@ -149,7 +144,6 @@ object TetheringManager {
|
||||
* Requires BLUETOOTH permission.
|
||||
* @see startTethering
|
||||
*/
|
||||
@RequiresApi(24)
|
||||
const val TETHERING_BLUETOOTH = 2
|
||||
/**
|
||||
* Ethernet tethering type.
|
||||
@@ -178,16 +172,13 @@ object TetheringManager {
|
||||
}
|
||||
}.first()
|
||||
|
||||
@get:RequiresApi(24)
|
||||
private val classOnStartTetheringCallback by lazy {
|
||||
Class.forName("android.net.ConnectivityManager\$OnStartTetheringCallback")
|
||||
}
|
||||
@get:RequiresApi(24)
|
||||
private val startTetheringLegacy by lazy @TargetApi(24) {
|
||||
private val startTetheringLegacy by lazy {
|
||||
ConnectivityManager::class.java.getDeclaredMethod("startTethering",
|
||||
Int::class.java, Boolean::class.java, classOnStartTetheringCallback, Handler::class.java)
|
||||
}
|
||||
@get:RequiresApi(24)
|
||||
private val stopTetheringLegacy by lazy {
|
||||
ConnectivityManager::class.java.getDeclaredMethod("stopTethering", Int::class.java)
|
||||
}
|
||||
@@ -232,7 +223,6 @@ object TetheringManager {
|
||||
private val stopTethering by lazy @TargetApi(30) { clazz.getDeclaredMethod("stopTethering", Int::class.java) }
|
||||
|
||||
@Deprecated("Legacy API")
|
||||
@RequiresApi(24)
|
||||
fun startTetheringLegacy(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
|
||||
handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) {
|
||||
val reference = WeakReference(callback)
|
||||
@@ -299,7 +289,6 @@ object TetheringManager {
|
||||
* configures tethering with the preferred local IPv4 link address to use.
|
||||
* *@see setStaticIpv4Addresses
|
||||
*/
|
||||
@RequiresApi(24)
|
||||
fun startTethering(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
|
||||
handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) {
|
||||
if (Build.VERSION.SDK_INT >= 30) try {
|
||||
@@ -371,12 +360,10 @@ object TetheringManager {
|
||||
* {@link ConnectivityManager.TETHERING_USB}, or
|
||||
* {@link ConnectivityManager.TETHERING_BLUETOOTH}.
|
||||
*/
|
||||
@RequiresApi(24)
|
||||
fun stopTethering(type: Int) {
|
||||
if (Build.VERSION.SDK_INT >= 30) stopTethering(instance, type)
|
||||
else stopTetheringLegacy(Services.connectivity, type)
|
||||
}
|
||||
@RequiresApi(24)
|
||||
fun stopTethering(type: Int, callback: (Exception) -> Unit) {
|
||||
try {
|
||||
stopTethering(type)
|
||||
@@ -625,10 +612,7 @@ object TetheringManager {
|
||||
@RequiresApi(30)
|
||||
const val TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14
|
||||
|
||||
val Intent.tetheredIfaces get() = getStringArrayListExtra(
|
||||
if (Build.VERSION.SDK_INT >= 26) EXTRA_ACTIVE_TETHER else EXTRA_ACTIVE_TETHER_LEGACY)
|
||||
val Intent.localOnlyTetheredIfaces get() = if (Build.VERSION.SDK_INT >= 26) {
|
||||
getStringArrayListExtra(
|
||||
val Intent.tetheredIfaces get() = getStringArrayListExtra(EXTRA_ACTIVE_TETHER)
|
||||
val Intent.localOnlyTetheredIfaces get() = getStringArrayListExtra(
|
||||
if (Build.VERSION.SDK_INT >= 30) EXTRA_ACTIVE_LOCAL_ONLY else EXTRA_ACTIVE_LOCAL_ONLY_LEGACY)
|
||||
} else emptyList<String>()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package be.mygod.vpnhotspot.net.monitor
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.LinkProperties
|
||||
import android.net.Network
|
||||
@@ -52,29 +51,10 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
|
||||
callback.onAvailable(currentLinkProperties)
|
||||
}
|
||||
} else {
|
||||
when (Build.VERSION.SDK_INT) {
|
||||
in 31..Int.MAX_VALUE -> @TargetApi(31) {
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
Services.connectivity.registerBestMatchingNetworkCallback(networkRequest, networkCallback,
|
||||
Services.mainHandler)
|
||||
}
|
||||
in 28..30 -> @TargetApi(28) {
|
||||
Services.connectivity.requestNetwork(networkRequest, networkCallback, Services.mainHandler)
|
||||
}
|
||||
in 26..27 -> @TargetApi(26) {
|
||||
Services.connectivity.registerDefaultNetworkCallback(networkCallback, Services.mainHandler)
|
||||
}
|
||||
in 24..25 -> @TargetApi(24) {
|
||||
Services.connectivity.registerDefaultNetworkCallback(networkCallback)
|
||||
}
|
||||
else -> try {
|
||||
Services.connectivity.requestNetwork(networkRequest, networkCallback)
|
||||
} catch (e: RuntimeException) {
|
||||
// SecurityException would be thrown in requestNetwork on Android 6.0 thanks to Google's stupid bug
|
||||
if (Build.VERSION.SDK_INT != 23) throw e
|
||||
GlobalScope.launch { callback.onFallback() }
|
||||
return
|
||||
}
|
||||
}
|
||||
} else Services.connectivity.requestNetwork(networkRequest, networkCallback, Services.mainHandler)
|
||||
registered = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ class InterfaceMonitor(private val ifaceRegex: String) : UpstreamMonitor() {
|
||||
callback.onAvailable(currentLinkProperties)
|
||||
}
|
||||
} else {
|
||||
Services.registerNetworkCallbackCompat(request, networkCallback)
|
||||
Services.registerNetworkCallback(request, networkCallback)
|
||||
registered = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import androidx.core.content.edit
|
||||
import be.mygod.librootkotlinx.RootServer
|
||||
import be.mygod.librootkotlinx.isEBADF
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.BuildConfig
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.net.Routing
|
||||
import be.mygod.vpnhotspot.root.ProcessData
|
||||
|
||||
@@ -33,13 +33,12 @@ class TetherTimeoutMonitor(private val timeout: Long = 0,
|
||||
private const val MIN_SOFT_AP_TIMEOUT_DELAY_MS = 600_000 // 10 minutes
|
||||
|
||||
@Deprecated("Use SoftApConfigurationCompat instead")
|
||||
@get:RequiresApi(28)
|
||||
val enabled get() = Settings.Global.getInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, 1) == 1
|
||||
@Deprecated("Use SoftApConfigurationCompat instead")
|
||||
suspend fun setEnabled(value: Boolean) = SettingsGlobalPut.int(SOFT_AP_TIMEOUT_ENABLED, if (value) 1 else 0)
|
||||
|
||||
val defaultTimeout: Int get() {
|
||||
val delay = if (Build.VERSION.SDK_INT >= 28) try {
|
||||
val delay = try {
|
||||
if (Build.VERSION.SDK_INT < 30) Resources.getSystem().run {
|
||||
getInteger(getIdentifier("config_wifi_framework_soft_ap_timeout_delay", "integer", "android"))
|
||||
} else {
|
||||
@@ -52,7 +51,7 @@ class TetherTimeoutMonitor(private val timeout: Long = 0,
|
||||
} catch (e: RuntimeException) {
|
||||
Timber.w(e)
|
||||
MIN_SOFT_AP_TIMEOUT_DELAY_MS
|
||||
} else MIN_SOFT_AP_TIMEOUT_DELAY_MS
|
||||
}
|
||||
return if (Build.VERSION.SDK_INT < 30 && delay < MIN_SOFT_AP_TIMEOUT_DELAY_MS) {
|
||||
Timber.w("Overriding timeout delay with minimum limit value: $delay < $MIN_SOFT_AP_TIMEOUT_DELAY_MS")
|
||||
MIN_SOFT_AP_TIMEOUT_DELAY_MS
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package be.mygod.vpnhotspot.net.monitor
|
||||
|
||||
import android.net.MacAddress
|
||||
import androidx.collection.LongSparseArray
|
||||
import androidx.collection.set
|
||||
import be.mygod.vpnhotspot.net.IpDev
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||
import be.mygod.vpnhotspot.net.Routing.Companion.IPTABLES
|
||||
import be.mygod.vpnhotspot.room.AppDatabase
|
||||
import be.mygod.vpnhotspot.room.TrafficRecord
|
||||
@@ -11,7 +11,12 @@ import be.mygod.vpnhotspot.util.Event2
|
||||
import be.mygod.vpnhotspot.util.RootSession
|
||||
import be.mygod.vpnhotspot.util.parseNumericAddress
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.net.InetAddress
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -23,8 +28,8 @@ object TrafficRecorder {
|
||||
private val records = mutableMapOf<IpDev, TrafficRecord>()
|
||||
val foregroundListeners = Event2<Collection<TrafficRecord>, LongSparseArray<TrafficRecord>>()
|
||||
|
||||
fun register(ip: InetAddress, downstream: String, mac: MacAddressCompat) {
|
||||
val record = TrafficRecord(mac = mac.addr, ip = ip, downstream = downstream)
|
||||
fun register(ip: InetAddress, downstream: String, mac: MacAddress) {
|
||||
val record = TrafficRecord(mac = mac, ip = ip, downstream = downstream)
|
||||
AppDatabase.instance.trafficRecordDao.insert(record)
|
||||
synchronized(this) {
|
||||
val key = IpDev(ip, downstream)
|
||||
@@ -156,5 +161,5 @@ object TrafficRecorder {
|
||||
/**
|
||||
* Possibly inefficient. Don't call this too often.
|
||||
*/
|
||||
fun isWorking(mac: MacAddressCompat) = records.values.any { it.mac == mac.addr }
|
||||
fun isWorking(mac: MacAddress) = records.values.any { it.mac == mac }
|
||||
}
|
||||
|
||||
@@ -46,15 +46,6 @@ abstract class UpstreamMonitor {
|
||||
* Called if some possibly stacked interface is available
|
||||
*/
|
||||
fun onAvailable(properties: LinkProperties? = null)
|
||||
/**
|
||||
* Called on API 23- from DefaultNetworkMonitor. This indicates that there isn't a good way of telling the
|
||||
* default network (see DefaultNetworkMonitor) and we are using rules at priority 22000
|
||||
* (RULE_PRIORITY_DEFAULT_NETWORK) as our fallback rules, which would work fine until Android 9.0 broke it in
|
||||
* commit: https://android.googlesource.com/platform/system/netd/+/758627c4d93392190b08e9aaea3bbbfb92a5f364
|
||||
*/
|
||||
fun onFallback() {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
|
||||
val callbacks = mutableSetOf<Callback>()
|
||||
|
||||
@@ -61,7 +61,7 @@ object VpnMonitor : UpstreamMonitor() {
|
||||
callback.onAvailable(currentLinkProperties)
|
||||
}
|
||||
} else {
|
||||
Services.registerNetworkCallbackCompat(request, networkCallback)
|
||||
Services.registerNetworkCallback(request, networkCallback)
|
||||
registered = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package be.mygod.vpnhotspot.net.wifi
|
||||
|
||||
import android.net.MacAddress
|
||||
import android.net.wifi.p2p.WifiP2pGroup
|
||||
import be.mygod.vpnhotspot.RepeaterService
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||
@@ -53,8 +54,8 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
|
||||
var bssids = listOfNotNull(group?.owner?.deviceAddress, ownerAddress)
|
||||
.distinct()
|
||||
.filter {
|
||||
val mac = MacAddress.fromString(it)
|
||||
try {
|
||||
val mac = MacAddressCompat.fromString(it)
|
||||
mac != MacAddressCompat.ALL_ZEROS_ADDRESS && mac != MacAddressCompat.ANY_ADDRESS
|
||||
} catch (_: IllegalArgumentException) {
|
||||
false
|
||||
@@ -126,7 +127,7 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
|
||||
add("\tmode=3")
|
||||
add("\tdisabled=2")
|
||||
add("}")
|
||||
if (target == null) target = this
|
||||
target = this
|
||||
})
|
||||
}
|
||||
content = Content(result, target!!, persistentMacLine, legacy)
|
||||
@@ -141,10 +142,10 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
|
||||
}
|
||||
val psk by lazy { group?.passphrase ?: content.target.psk!! }
|
||||
val bssid by lazy {
|
||||
content.target.bssid?.let { MacAddressCompat.fromString(it) }
|
||||
content.target.bssid?.let { MacAddress.fromString(it) }
|
||||
}
|
||||
|
||||
suspend fun update(ssid: String, psk: String, bssid: MacAddressCompat?) {
|
||||
suspend fun update(ssid: String, psk: String, bssid: MacAddress?) {
|
||||
val (lines, block, persistentMacLine, legacy) = content
|
||||
block[block.ssidLine!!] = "\tssid=" + ssid.toByteArray()
|
||||
.joinToString("") { (it.toInt() and 255).toString(16).padStart(2, '0') }
|
||||
|
||||
@@ -9,10 +9,9 @@ import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import android.util.SparseIntArray
|
||||
import androidx.annotation.RequiresApi
|
||||
import be.mygod.vpnhotspot.BuildConfig
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toCompat
|
||||
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
|
||||
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.requireSingleBand
|
||||
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.setChannel
|
||||
import be.mygod.vpnhotspot.util.ConstantLookup
|
||||
import be.mygod.vpnhotspot.util.UnblockCentral
|
||||
import kotlinx.parcelize.Parcelize
|
||||
@@ -22,8 +21,7 @@ import java.lang.reflect.InvocationTargetException
|
||||
@Parcelize
|
||||
data class SoftApConfigurationCompat(
|
||||
var ssid: String? = null,
|
||||
@Deprecated("Workaround for using inline class with Parcelize, use bssid")
|
||||
var bssidAddr: Long? = null,
|
||||
var bssid: MacAddress? = null,
|
||||
var passphrase: String? = null,
|
||||
var isHiddenSsid: Boolean = false,
|
||||
/**
|
||||
@@ -31,14 +29,11 @@ data class SoftApConfigurationCompat(
|
||||
* see also [android.net.wifi.WifiManager.isBridgedApConcurrencySupported].
|
||||
* Otherwise, use [requireSingleBand] and [setChannel].
|
||||
*/
|
||||
@TargetApi(23)
|
||||
var channels: SparseIntArray = SparseIntArray(1).apply { append(BAND_2GHZ, 0) },
|
||||
var securityType: Int = SoftApConfiguration.SECURITY_TYPE_OPEN,
|
||||
@TargetApi(30)
|
||||
var maxNumberOfClients: Int = 0,
|
||||
@TargetApi(28)
|
||||
var isAutoShutdownEnabled: Boolean = true,
|
||||
@TargetApi(28)
|
||||
var shutdownTimeoutMillis: Long = 0,
|
||||
@TargetApi(30)
|
||||
var isClientControlByUserEnabled: Boolean = false,
|
||||
@@ -166,7 +161,6 @@ data class SoftApConfigurationCompat(
|
||||
*
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/android-6.0.0_r1/wifi/java/android/net/wifi/WifiConfiguration.java#242
|
||||
*/
|
||||
@get:RequiresApi(23)
|
||||
@Suppress("DEPRECATION")
|
||||
/**
|
||||
* The band which AP resides on
|
||||
@@ -174,7 +168,6 @@ data class SoftApConfigurationCompat(
|
||||
* By default, 2G is chosen
|
||||
*/
|
||||
private val apBand by lazy { android.net.wifi.WifiConfiguration::class.java.getDeclaredField("apBand") }
|
||||
@get:RequiresApi(23)
|
||||
@Suppress("DEPRECATION")
|
||||
/**
|
||||
* The channel which AP resides on
|
||||
@@ -356,17 +349,17 @@ data class SoftApConfigurationCompat(
|
||||
@Suppress("DEPRECATION")
|
||||
fun android.net.wifi.WifiConfiguration.toCompat() = SoftApConfigurationCompat(
|
||||
SSID,
|
||||
BSSID?.let { MacAddressCompat.fromString(it) }?.addr,
|
||||
BSSID?.let { MacAddress.fromString(it) },
|
||||
preSharedKey,
|
||||
hiddenSSID,
|
||||
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/wifi/java/android/net/wifi/SoftApConfToXmlMigrationUtil.java;l=87;drc=aa6527cf41671d1ed417b8ebdb6b3aa614f62344
|
||||
SparseIntArray(1).also {
|
||||
if (Build.VERSION.SDK_INT >= 23) it.append(when (val band = apBand.getInt(this)) {
|
||||
it.append(when (val band = apBand.getInt(this)) {
|
||||
0 -> BAND_2GHZ
|
||||
1 -> BAND_5GHZ
|
||||
-1 -> BAND_LEGACY
|
||||
else -> throw IllegalArgumentException("Unexpected band $band")
|
||||
}, apChannel.getInt(this)) else it.append(BAND_LEGACY, 0)
|
||||
}, apChannel.getInt(this))
|
||||
},
|
||||
allowedKeyManagement.nextSetBit(0).let { selected ->
|
||||
require(allowedKeyManagement.nextSetBit(selected + 1) < 0) {
|
||||
@@ -389,14 +382,14 @@ data class SoftApConfigurationCompat(
|
||||
}
|
||||
}
|
||||
},
|
||||
isAutoShutdownEnabled = if (Build.VERSION.SDK_INT >= 28) TetherTimeoutMonitor.enabled else false,
|
||||
isAutoShutdownEnabled = TetherTimeoutMonitor.enabled,
|
||||
underlying = this)
|
||||
|
||||
@RequiresApi(30)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun SoftApConfiguration.toCompat() = SoftApConfigurationCompat(
|
||||
ssid,
|
||||
bssid?.toCompat()?.addr,
|
||||
bssid,
|
||||
passphrase,
|
||||
isHiddenSsid,
|
||||
if (Build.VERSION.SDK_INT >= 31) getChannels(this) as SparseIntArray else SparseIntArray(1).also {
|
||||
@@ -466,13 +459,6 @@ data class SoftApConfigurationCompat(
|
||||
setBridgedModeOpportunisticShutdownTimeoutMillis(staticBuilder, timeout)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
inline var bssid: MacAddressCompat?
|
||||
get() = bssidAddr?.let { MacAddressCompat(it) }
|
||||
set(value) {
|
||||
bssidAddr = value?.addr
|
||||
}
|
||||
|
||||
fun setChannel(channel: Int, band: Int = BAND_LEGACY) {
|
||||
channels = SparseIntArray(1).apply {
|
||||
append(when {
|
||||
@@ -500,18 +486,15 @@ data class SoftApConfigurationCompat(
|
||||
result.SSID = ssid
|
||||
result.preSharedKey = passphrase
|
||||
result.hiddenSSID = isHiddenSsid
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
apBand.setInt(result, when (band) {
|
||||
BAND_2GHZ -> 0
|
||||
BAND_5GHZ -> 1
|
||||
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(isLegacyEitherBand(band)) { "Specifying band is unsupported on this platform" }
|
||||
if (original?.securityType != securityType) {
|
||||
result.allowedKeyManagement.clear()
|
||||
result.allowedKeyManagement.set(when (securityType) {
|
||||
@@ -545,9 +528,8 @@ data class SoftApConfigurationCompat(
|
||||
else -> passphrase
|
||||
}, securityType)
|
||||
setChannelsCompat(builder, channels)
|
||||
setBssid(builder, bssid?.run {
|
||||
if (Build.VERSION.SDK_INT >= 31 && macRandomizationSetting != RANDOMIZATION_NONE) null else toPlatform()
|
||||
})
|
||||
setBssid(builder,
|
||||
if (Build.VERSION.SDK_INT < 31 || macRandomizationSetting == RANDOMIZATION_NONE) bssid else null)
|
||||
setMaxNumberOfClients(builder, maxNumberOfClients)
|
||||
try {
|
||||
setShutdownTimeoutMillis(builder, shutdownTimeoutMillis)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package be.mygod.vpnhotspot.net.wifi
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.content.ClipData
|
||||
import android.content.ClipDescription
|
||||
import android.content.DialogInterface
|
||||
import android.net.MacAddress
|
||||
import android.net.wifi.SoftApConfiguration
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
@@ -31,7 +31,6 @@ import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.RepeaterService
|
||||
import be.mygod.vpnhotspot.databinding.DialogWifiApBinding
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
|
||||
import be.mygod.vpnhotspot.util.QRCodeDialog
|
||||
import be.mygod.vpnhotspot.util.RangeInput
|
||||
@@ -68,17 +67,12 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
}
|
||||
private val p2pSafeOptions by lazy { genAutoOptions(SoftApConfigurationCompat.BAND_LEGACY) + channels5G }
|
||||
private val softApOptions by lazy {
|
||||
when (Build.VERSION.SDK_INT) {
|
||||
in 30..Int.MAX_VALUE -> {
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
genAutoOptions(SoftApConfigurationCompat.BAND_ANY_31) +
|
||||
channels5G +
|
||||
(1..253).map { ChannelOption(SoftApConfigurationCompat.BAND_6GHZ, it) } +
|
||||
(1..6).map { ChannelOption(SoftApConfigurationCompat.BAND_60GHZ, it) }
|
||||
}
|
||||
in 28 until 30 -> p2pSafeOptions
|
||||
else -> listOf(ChannelOption(SoftApConfigurationCompat.BAND_2GHZ),
|
||||
ChannelOption(SoftApConfigurationCompat.BAND_5GHZ)) + channels5G
|
||||
}
|
||||
} else p2pSafeOptions
|
||||
}
|
||||
|
||||
@get:RequiresApi(30)
|
||||
@@ -149,24 +143,24 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
securityType = dialogView.security.selectedItemPosition
|
||||
isHiddenSsid = dialogView.hiddenSsid.isChecked
|
||||
}
|
||||
if (full) @TargetApi(28) {
|
||||
if (full) {
|
||||
isAutoShutdownEnabled = dialogView.autoShutdown.isChecked
|
||||
shutdownTimeoutMillis = dialogView.timeout.text.let { text ->
|
||||
if (text.isNullOrEmpty()) 0 else text.toString().toLong()
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) channels = generateChannels()
|
||||
channels = generateChannels()
|
||||
maxNumberOfClients = dialogView.maxClient.text.let { text ->
|
||||
if (text.isNullOrEmpty()) 0 else text.toString().toInt()
|
||||
}
|
||||
isClientControlByUserEnabled = dialogView.clientUserControl.isChecked
|
||||
allowedClientList = (dialogView.allowedList.text ?: "").split(nonMacChars)
|
||||
.filter { it.isNotEmpty() }.map { MacAddressCompat.fromString(it).toPlatform() }
|
||||
.filter { it.isNotEmpty() }.map(MacAddress::fromString)
|
||||
blockedClientList = (dialogView.blockedList.text ?: "").split(nonMacChars)
|
||||
.filter { it.isNotEmpty() }.map { MacAddressCompat.fromString(it).toPlatform() }
|
||||
.filter { it.isNotEmpty() }.map(MacAddress::fromString)
|
||||
macRandomizationSetting = dialogView.macRandomization.selectedItemPosition
|
||||
bssid = if ((arg.p2pMode || Build.VERSION.SDK_INT < 31 && macRandomizationSetting ==
|
||||
SoftApConfigurationCompat.RANDOMIZATION_NONE) && dialogView.bssid.length() != 0) {
|
||||
MacAddressCompat.fromString(dialogView.bssid.text.toString())
|
||||
MacAddress.fromString(dialogView.bssid.text.toString())
|
||||
} else null
|
||||
isBridgedModeOpportunisticShutdownEnabled = dialogView.bridgedModeOpportunisticShutdown.isChecked
|
||||
isIeee80211axEnabled = dialogView.ieee80211ax.isChecked
|
||||
@@ -177,7 +171,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
}
|
||||
vendorElements = VendorElements.deserialize(dialogView.vendorElements.text)
|
||||
persistentRandomizedMacAddress = if (dialogView.persistentRandomizedMac.length() != 0) {
|
||||
MacAddressCompat.fromString(dialogView.persistentRandomizedMac.text.toString()).toPlatform()
|
||||
MacAddress.fromString(dialogView.persistentRandomizedMac.text.toString())
|
||||
} else null
|
||||
allowedAcsChannels = acsList.associate { (band, text, _) -> band to RangeInput.fromString(text.text) }
|
||||
if (!arg.p2pMode && Build.VERSION.SDK_INT >= 33) {
|
||||
@@ -227,7 +221,6 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
}
|
||||
}
|
||||
if (!arg.readOnly) dialogView.password.addTextChangedListener(this@WifiApDialogFragment)
|
||||
if (!arg.p2pMode && Build.VERSION.SDK_INT < 28) dialogView.autoShutdown.isGone = true
|
||||
if (arg.p2pMode || Build.VERSION.SDK_INT >= 30) {
|
||||
dialogView.timeoutWrapper.helperText = getString(R.string.wifi_hotspot_timeout_default,
|
||||
TetherTimeoutMonitor.defaultTimeout)
|
||||
@@ -239,12 +232,10 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
}
|
||||
if (!arg.readOnly) onItemSelectedListener = this@WifiApDialogFragment
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) {
|
||||
dialogView.bandPrimary.configure(currentChannels)
|
||||
if (Build.VERSION.SDK_INT >= 31 && !arg.p2pMode) {
|
||||
dialogView.bandSecondary.configure(listOf(ChannelOption.Disabled) + currentChannels)
|
||||
} else dialogView.bandSecondary.isGone = true
|
||||
} else dialogView.bandGroup.isGone = true
|
||||
if (arg.p2pMode || Build.VERSION.SDK_INT < 30) dialogView.accessControlGroup.isGone = true
|
||||
else if (!arg.readOnly) {
|
||||
dialogView.maxClient.addTextChangedListener(this@WifiApDialogFragment)
|
||||
@@ -307,12 +298,10 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
dialogView.password.setText(base.passphrase)
|
||||
dialogView.autoShutdown.isChecked = base.isAutoShutdownEnabled
|
||||
dialogView.timeout.setText(base.shutdownTimeoutMillis.let { if (it <= 0) "" else it.toString() })
|
||||
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) {
|
||||
dialogView.bandPrimary.setSelection(locate(0))
|
||||
if (Build.VERSION.SDK_INT >= 31 && !arg.p2pMode) {
|
||||
dialogView.bandSecondary.setSelection(if (base.channels.size() > 1) locate(1) + 1 else 0)
|
||||
}
|
||||
}
|
||||
dialogView.bssid.setText(base.bssid?.toString())
|
||||
dialogView.hiddenSsid.isChecked = base.isHiddenSsid
|
||||
dialogView.maxClient.setText(base.maxNumberOfClients.let { if (it == 0) "" else it.toString() })
|
||||
@@ -343,7 +332,6 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
validate()
|
||||
}
|
||||
|
||||
@TargetApi(28)
|
||||
private fun validate() {
|
||||
if (!started) return
|
||||
val ssidLength = dialogView.ssid.text.toString().toByteArray().size
|
||||
@@ -389,10 +377,8 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
dialogView.bssidWrapper.isGone = hideBssid
|
||||
dialogView.bssidWrapper.error = null
|
||||
val bssidValid = hideBssid || dialogView.bssid.length() == 0 || try {
|
||||
val mac = MacAddressCompat.fromString(dialogView.bssid.text.toString())
|
||||
if (Build.VERSION.SDK_INT >= 30 && !arg.p2pMode) {
|
||||
SoftApConfigurationCompat.testPlatformValidity(mac.toPlatform())
|
||||
}
|
||||
val mac = MacAddress.fromString(dialogView.bssid.text.toString())
|
||||
if (Build.VERSION.SDK_INT >= 30 && !arg.p2pMode) SoftApConfigurationCompat.testPlatformValidity(mac)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
dialogView.bssidWrapper.error = e.readableMessage
|
||||
@@ -409,15 +395,15 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
dialogView.maxClientWrapper.error = maxClientError
|
||||
val listsNoError = if (Build.VERSION.SDK_INT >= 30) {
|
||||
val (blockedList, blockedListError) = try {
|
||||
(dialogView.blockedList.text ?: "").split(nonMacChars)
|
||||
.filter { it.isNotEmpty() }.map { MacAddressCompat.fromString(it).toPlatform() }.toSet() to null
|
||||
(dialogView.blockedList.text ?: "").split(nonMacChars).filter { it.isNotEmpty() }
|
||||
.map(MacAddress::fromString).toSet() to null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
null to e.readableMessage
|
||||
}
|
||||
dialogView.blockedListWrapper.error = blockedListError
|
||||
val allowedListError = try {
|
||||
(dialogView.allowedList.text ?: "").split(nonMacChars).filter { it.isNotEmpty() }.forEach {
|
||||
val mac = MacAddressCompat.fromString(it).toPlatform()
|
||||
val mac = MacAddress.fromString(it)
|
||||
require(blockedList?.contains(mac) != true) { "A MAC address exists in both client lists" }
|
||||
}
|
||||
null
|
||||
@@ -449,9 +435,9 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
dialogView.vendorElementsWrapper.error = vendorElementsError
|
||||
dialogView.persistentRandomizedMacWrapper.error = null
|
||||
val persistentRandomizedMacValid = dialogView.persistentRandomizedMac.length() == 0 || try {
|
||||
MacAddressCompat.fromString(dialogView.persistentRandomizedMac.text.toString())
|
||||
MacAddress.fromString(dialogView.persistentRandomizedMac.text.toString())
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
} catch (e: IllegalArgumentException) {
|
||||
dialogView.persistentRandomizedMacWrapper.error = e.readableMessage
|
||||
false
|
||||
}
|
||||
@@ -496,9 +482,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
android.R.id.copy -> try {
|
||||
app.clipboard.setPrimaryClip(ClipData.newPlainText(null,
|
||||
Base64.encodeToString(generateConfig().toByteArray(), BASE64_FLAGS)).apply {
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
description.extras = persistableBundleOf(ClipDescription.EXTRA_IS_SENSITIVE to true)
|
||||
}
|
||||
})
|
||||
true
|
||||
} catch (e: RuntimeException) {
|
||||
|
||||
@@ -88,7 +88,6 @@ object WifiApManager {
|
||||
*
|
||||
* Source: https://android.googlesource.com/platform/frameworks/base/+/android-6.0.0_r1/wifi/java/android/net/wifi/WifiManager.java#210
|
||||
*/
|
||||
@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"
|
||||
/**
|
||||
@@ -98,7 +97,6 @@ object WifiApManager {
|
||||
*
|
||||
* Source: https://android.googlesource.com/platform/frameworks/base/+/android-8.0.0_r1/wifi/java/android/net/wifi/WifiManager.java#413
|
||||
*/
|
||||
@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"
|
||||
|
||||
@@ -177,7 +175,6 @@ object WifiApManager {
|
||||
setWifiApConfiguration(Services.wifi, value) as Boolean
|
||||
fun setConfiguration(value: SoftApConfiguration) = setSoftApConfiguration(Services.wifi, value) as Boolean
|
||||
|
||||
@RequiresApi(28)
|
||||
interface SoftApCallbackCompat {
|
||||
/**
|
||||
* Called when soft AP state changes.
|
||||
@@ -239,7 +236,6 @@ object WifiApManager {
|
||||
@RequiresApi(30)
|
||||
fun onBlockedClientConnecting(client: Parcelable, blockedReason: Int) { }
|
||||
}
|
||||
@RequiresApi(23)
|
||||
val failureReasonLookup = ConstantLookup<WifiManager>("SAP_START_FAILURE_", "GENERAL", "NO_CHANNEL")
|
||||
@get:RequiresApi(30)
|
||||
val clientBlockLookup by lazy { ConstantLookup<WifiManager>("SAP_CLIENT_") }
|
||||
@@ -255,7 +251,6 @@ object WifiApManager {
|
||||
WifiManager::class.java.getDeclaredMethod("unregisterSoftApCallback", interfaceSoftApCallback)
|
||||
}
|
||||
|
||||
@RequiresApi(28)
|
||||
fun registerSoftApCallback(callback: SoftApCallbackCompat, executor: Executor): Any {
|
||||
val proxy = Proxy.newProxyInstance(interfaceSoftApCallback.classLoader,
|
||||
arrayOf(interfaceSoftApCallback), object : InvocationHandler {
|
||||
@@ -270,7 +265,7 @@ object WifiApManager {
|
||||
method.matches("onStateChanged", Integer.TYPE, Integer.TYPE) -> {
|
||||
callback.onStateChanged(args!![0] as Int, args[1] as Int)
|
||||
}
|
||||
method.matches("onNumClientsChanged", Integer.TYPE) -> @Suppress("DEPRECATION") {
|
||||
method.matches("onNumClientsChanged", Integer.TYPE) -> {
|
||||
if (Build.VERSION.SDK_INT >= 30) Timber.w(Exception("Unexpected onNumClientsChanged"))
|
||||
callback.onNumClientsChanged(args!![0] as Int)
|
||||
}
|
||||
@@ -307,7 +302,6 @@ object WifiApManager {
|
||||
} else registerSoftApCallback(Services.wifi, proxy, null)
|
||||
return proxy
|
||||
}
|
||||
@RequiresApi(28)
|
||||
fun unregisterSoftApCallback(key: Any) = unregisterSoftApCallback(Services.wifi, key)
|
||||
|
||||
private val cancelLocalOnlyHotspotRequest by lazy {
|
||||
@@ -317,43 +311,5 @@ object WifiApManager {
|
||||
* This is the only way to unregister requests besides app exiting.
|
||||
* Therefore, we are happy with crashing the app if reflection fails.
|
||||
*/
|
||||
@RequiresApi(26)
|
||||
fun cancelLocalOnlyHotspotRequest() = cancelLocalOnlyHotspotRequest(Services.wifi)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private val setWifiApEnabled by lazy {
|
||||
WifiManager::class.java.getDeclaredMethod("setWifiApEnabled",
|
||||
android.net.wifi.WifiConfiguration::class.java, Boolean::class.java)
|
||||
}
|
||||
/**
|
||||
* Start AccessPoint mode with the specified
|
||||
* configuration. If the radio is already running in
|
||||
* AP mode, update the new configuration
|
||||
* Note that starting in access point mode disables station
|
||||
* mode operation
|
||||
* @param wifiConfig SSID, security and channel details as
|
||||
* part of WifiConfiguration
|
||||
* @return {@code true} if the operation succeeds, {@code false} otherwise
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
private fun WifiManager.setWifiApEnabled(wifiConfig: android.net.wifi.WifiConfiguration?, enabled: Boolean) =
|
||||
setWifiApEnabled(this, wifiConfig, enabled) as Boolean
|
||||
|
||||
/**
|
||||
* Although the functionalities were removed in API 26, it is already not functioning correctly on API 25.
|
||||
*
|
||||
* See also: https://android.googlesource.com/platform/frameworks/base/+/5c0b10a4a9eecc5307bb89a271221f2b20448797%5E%21/
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Not usable since API 26, malfunctioning on API 25")
|
||||
fun start(wifiConfig: android.net.wifi.WifiConfiguration? = null) {
|
||||
Services.wifi.isWifiEnabled = false
|
||||
Services.wifi.setWifiApEnabled(wifiConfig, true)
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Not usable since API 26")
|
||||
fun stop() {
|
||||
Services.wifi.setWifiApEnabled(null, false)
|
||||
Services.wifi.isWifiEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package be.mygod.vpnhotspot.net.wifi
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.net.MacAddress
|
||||
import android.net.wifi.ScanResult
|
||||
import android.net.wifi.WpsInfo
|
||||
import android.net.wifi.p2p.WifiP2pGroup
|
||||
@@ -9,9 +10,8 @@ import android.net.wifi.p2p.WifiP2pManager
|
||||
import androidx.annotation.RequiresApi
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
|
||||
import be.mygod.vpnhotspot.util.callSuper
|
||||
import be.mygod.vpnhotspot.util.matchesCompat
|
||||
import be.mygod.vpnhotspot.util.matches
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import java.lang.reflect.InvocationHandler
|
||||
import java.lang.reflect.Method
|
||||
@@ -128,7 +128,7 @@ object WifiP2pManagerHelper {
|
||||
requestPersistentGroupInfo(this, c, Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader,
|
||||
arrayOf(interfacePersistentGroupInfoListener), object : InvocationHandler {
|
||||
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? = when {
|
||||
method.matchesCompat("onPersistentGroupInfoAvailable", args, classWifiP2pGroupList) -> {
|
||||
method.matches("onPersistentGroupInfoAvailable", classWifiP2pGroupList) -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
result.complete(getGroupList(args!![0]) as Collection<WifiP2pGroup>)
|
||||
}
|
||||
@@ -142,11 +142,11 @@ object WifiP2pManagerHelper {
|
||||
CompletableDeferred<WifiP2pInfo?>().apply { requestConnectionInfo(c) { complete(it) } }.await()
|
||||
@SuppressLint("MissingPermission") // missing permission simply leads to null result
|
||||
@RequiresApi(29)
|
||||
suspend fun WifiP2pManager.requestDeviceAddress(c: WifiP2pManager.Channel): MacAddressCompat? {
|
||||
suspend fun WifiP2pManager.requestDeviceAddress(c: WifiP2pManager.Channel): MacAddress? {
|
||||
val future = CompletableDeferred<String?>()
|
||||
requestDeviceInfo(c) { future.complete(it?.deviceAddress) }
|
||||
return future.await()?.let {
|
||||
val address = if (it.isEmpty()) null else MacAddressCompat.fromString(it)
|
||||
val address = if (it.isEmpty()) null else MacAddress.fromString(it)
|
||||
if (address == MacAddressCompat.ANY_ADDRESS) null else address
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ class AutoCompleteNetworkPreferenceDialogFragment : EditTextPreferenceDialogFrag
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
Services.registerNetworkCallbackCompat(globalNetworkRequestBuilder().apply {
|
||||
Services.registerNetworkCallback(globalNetworkRequestBuilder().apply {
|
||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
|
||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||
|
||||
@@ -3,7 +3,6 @@ package be.mygod.vpnhotspot.preference
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.net.LinkProperties
|
||||
import android.os.Build
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import android.util.AttributeSet
|
||||
@@ -41,7 +40,7 @@ class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(co
|
||||
internet == true || try {
|
||||
route.matches(internetV4Address) || route.matches(internetV6Address)
|
||||
} catch (e: RuntimeException) {
|
||||
if (Build.VERSION.SDK_INT >= 23) Timber.w(e) else Timber.d(e)
|
||||
Timber.w(e)
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -52,12 +51,7 @@ class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(co
|
||||
}
|
||||
|
||||
private val primary = Monitor()
|
||||
private val fallback: Monitor = object : Monitor() {
|
||||
override fun onFallback() {
|
||||
currentInterfaces = mapOf("<default>" to true)
|
||||
onUpdate()
|
||||
}
|
||||
}
|
||||
private val fallback = Monitor()
|
||||
|
||||
init {
|
||||
(context as LifecycleOwner).lifecycle.addObserver(this)
|
||||
|
||||
@@ -1,37 +1,38 @@
|
||||
package be.mygod.vpnhotspot.room
|
||||
|
||||
import android.net.MacAddress
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.map
|
||||
import androidx.room.*
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toLong
|
||||
|
||||
@Entity
|
||||
data class ClientRecord(@PrimaryKey
|
||||
val mac: Long,
|
||||
val mac: MacAddress,
|
||||
var nickname: CharSequence = "",
|
||||
var blocked: Boolean = false,
|
||||
var macLookupPending: Boolean = true) {
|
||||
@androidx.room.Dao
|
||||
abstract class Dao {
|
||||
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
|
||||
protected abstract fun lookupBlocking(mac: Long): ClientRecord?
|
||||
fun lookupOrDefaultBlocking(mac: MacAddressCompat) = lookupBlocking(mac.addr) ?: ClientRecord(mac.addr)
|
||||
protected abstract fun lookupBlocking(mac: MacAddress): ClientRecord?
|
||||
fun lookupOrDefaultBlocking(mac: MacAddress) = lookupBlocking(mac) ?: ClientRecord(mac)
|
||||
|
||||
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
|
||||
protected abstract suspend fun lookup(mac: Long): ClientRecord?
|
||||
suspend fun lookupOrDefault(mac: Long) = lookup(mac) ?: ClientRecord(mac)
|
||||
protected abstract suspend fun lookup(mac: MacAddress): ClientRecord?
|
||||
suspend fun lookupOrDefault(mac: MacAddress) = lookup(mac) ?: ClientRecord(mac)
|
||||
|
||||
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
|
||||
protected abstract fun lookupSync(mac: Long): LiveData<ClientRecord?>
|
||||
fun lookupOrDefaultSync(mac: MacAddressCompat) = lookupSync(mac.addr).map { it ?: ClientRecord(mac.addr) }
|
||||
protected abstract fun lookupSync(mac: MacAddress): LiveData<ClientRecord?>
|
||||
fun lookupOrDefaultSync(mac: MacAddress) = lookupSync(mac).map { it ?: ClientRecord(mac) }
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
protected abstract suspend fun updateInternal(value: ClientRecord): Long
|
||||
suspend fun update(value: ClientRecord) = check(updateInternal(value) == value.mac)
|
||||
suspend fun update(value: ClientRecord) = check(updateInternal(value) == value.mac.toLong())
|
||||
|
||||
@Transaction
|
||||
open suspend fun upsert(mac: MacAddressCompat, operation: suspend ClientRecord.() -> Unit) = lookupOrDefault(
|
||||
mac.addr).apply {
|
||||
open suspend fun upsert(mac: MacAddress, operation: suspend ClientRecord.() -> Unit) = lookupOrDefault(
|
||||
mac).apply {
|
||||
operation()
|
||||
update(this)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package be.mygod.vpnhotspot.room
|
||||
|
||||
import android.net.MacAddress
|
||||
import android.text.TextUtils
|
||||
import androidx.room.TypeConverter
|
||||
import be.mygod.librootkotlinx.useParcel
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toLong
|
||||
import timber.log.Timber
|
||||
import java.net.InetAddress
|
||||
|
||||
@@ -27,6 +30,14 @@ object Converters {
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@TypeConverter
|
||||
fun persistMacAddress(address: MacAddress) = address.toLong()
|
||||
|
||||
@JvmStatic
|
||||
@TypeConverter
|
||||
fun unpersistMacAddress(address: Long) = MacAddressCompat(address).toPlatform()
|
||||
|
||||
@JvmStatic
|
||||
@TypeConverter
|
||||
fun persistInetAddress(address: InetAddress): ByteArray = address.address
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package be.mygod.vpnhotspot.room
|
||||
|
||||
import android.net.MacAddress
|
||||
import android.os.Parcelable
|
||||
import androidx.room.*
|
||||
import kotlinx.parcelize.Parcelize
|
||||
@@ -22,7 +23,7 @@ data class TrafficRecord(
|
||||
/**
|
||||
* Foreign key/ID for (possibly non-existent, i.e. default) entry in ClientRecord.
|
||||
*/
|
||||
val mac: Long,
|
||||
val mac: MacAddress,
|
||||
/**
|
||||
* For now only stats for IPv4 will be recorded. But I'm going to put the more general class here just in case.
|
||||
*/
|
||||
@@ -58,7 +59,7 @@ data class TrafficRecord(
|
||||
/* We only want to find the last record for each chain so that we don't double count */
|
||||
WHERE TrafficRecord.mac = :mac AND Next.id IS NULL
|
||||
""")
|
||||
abstract suspend fun queryStats(mac: Long): ClientStats
|
||||
abstract suspend fun queryStats(mac: MacAddress): ClientStats
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package be.mygod.vpnhotspot.root
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import android.os.RemoteException
|
||||
import android.provider.Settings
|
||||
@@ -30,18 +29,10 @@ fun ProcessBuilder.fixPath(redirect: Boolean = false) = apply {
|
||||
|
||||
@Parcelize
|
||||
data class Dump(val path: String, val cacheDir: File = app.deviceStorage.codeCacheDir) : RootCommandNoResult {
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
override suspend fun execute() = withContext(Dispatchers.IO) {
|
||||
FileOutputStream(path, true).use { out ->
|
||||
val process = ProcessBuilder("sh").fixPath(true).start()
|
||||
process.outputStream.bufferedWriter().use { commands ->
|
||||
// https://android.googlesource.com/platform/external/iptables/+/android-7.0.0_r1/iptables/Android.mk#34
|
||||
val iptablesSave = if (Build.VERSION.SDK_INT < 24) File(cacheDir, "iptables-save").absolutePath.also {
|
||||
commands.appendLine("ln -sf /system/bin/iptables $it")
|
||||
} else "iptables-save"
|
||||
val ip6tablesSave = if (Build.VERSION.SDK_INT < 24) File(cacheDir, "ip6tables-save").absolutePath.also {
|
||||
commands.appendLine("ln -sf /system/bin/ip6tables $it")
|
||||
} else "ip6tables-save"
|
||||
commands.appendLine("""
|
||||
|echo dumpsys ${Context.WIFI_P2P_SERVICE}
|
||||
|dumpsys ${Context.WIFI_P2P_SERVICE}
|
||||
@@ -50,13 +41,13 @@ data class Dump(val path: String, val cacheDir: File = app.deviceStorage.codeCac
|
||||
|dumpsys ${Context.CONNECTIVITY_SERVICE} tethering
|
||||
|echo
|
||||
|echo iptables -t filter
|
||||
|$iptablesSave -t filter
|
||||
|iptables-save -t filter
|
||||
|echo
|
||||
|echo iptables -t nat
|
||||
|$iptablesSave -t nat
|
||||
|iptables-save -t nat
|
||||
|echo
|
||||
|echo ip6tables-save
|
||||
|$ip6tablesSave
|
||||
|ip6tables-save
|
||||
|echo
|
||||
|echo ip rule
|
||||
|$IP rule
|
||||
@@ -125,7 +116,7 @@ class ProcessListener(private val terminateRegex: Regex,
|
||||
parent.join()
|
||||
} finally {
|
||||
parent.cancel()
|
||||
if (Build.VERSION.SDK_INT < 26) process.destroy() else if (process.isAlive) process.destroyForcibly()
|
||||
if (process.isAlive) process.destroyForcibly()
|
||||
parent.join()
|
||||
}
|
||||
}
|
||||
@@ -162,7 +153,6 @@ data class StartTethering(private val type: Int,
|
||||
|
||||
@Deprecated("Old API since API 30")
|
||||
@Parcelize
|
||||
@RequiresApi(24)
|
||||
@Suppress("DEPRECATION")
|
||||
data class StartTetheringLegacy(private val cacheDir: File, private val type: Int,
|
||||
private val showProvisioningUi: Boolean) : RootCommand<ParcelableBoolean> {
|
||||
@@ -184,7 +174,6 @@ data class StartTetheringLegacy(private val cacheDir: File, private val type: In
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
@RequiresApi(24)
|
||||
data class StopTethering(private val type: Int) : RootCommandNoResult {
|
||||
override suspend fun execute(): Parcelable? {
|
||||
TetheringManager.stopTethering(type)
|
||||
@@ -209,7 +198,6 @@ data class SettingsGlobalPut(val name: String, val value: String) : RootCommandN
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
override suspend fun execute() = withContext(Dispatchers.IO) {
|
||||
val process = ProcessBuilder("settings", "put", "global", name, value).fixPath(true).start()
|
||||
val error = process.inputStream.bufferedReader().readText()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package be.mygod.vpnhotspot.root
|
||||
|
||||
import android.net.MacAddress
|
||||
import android.net.wifi.ScanResult
|
||||
import android.net.wifi.p2p.WifiP2pManager
|
||||
import android.os.Looper
|
||||
@@ -37,10 +38,8 @@ object RepeaterCommands {
|
||||
|
||||
@Parcelize
|
||||
@RequiresApi(29)
|
||||
class RequestDeviceAddress : RootCommand<ParcelableLong?> {
|
||||
override suspend fun execute() = Services.p2p!!.run {
|
||||
requestDeviceAddress(obtainChannel())?.let { ParcelableLong(it.addr) }
|
||||
}
|
||||
class RequestDeviceAddress : RootCommand<MacAddress?> {
|
||||
override suspend fun execute() = Services.p2p!!.run { requestDeviceAddress(obtainChannel()) }
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package be.mygod.vpnhotspot.root
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import be.mygod.librootkotlinx.*
|
||||
@@ -34,7 +33,7 @@ object RootManager : RootSession(), Logger {
|
||||
})
|
||||
Logger.me = RootManager
|
||||
Services.init { systemContext }
|
||||
if (Build.VERSION.SDK_INT >= 28) UnblockCentral.needInit = false
|
||||
UnblockCentral.needInit = false
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import timber.log.Timber
|
||||
object RoutingCommands {
|
||||
@Parcelize
|
||||
class Clean : RootCommandNoResult {
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
override suspend fun execute() = withContext(Dispatchers.IO) {
|
||||
val process = ProcessBuilder("sh").fixPath(true).start()
|
||||
process.outputStream.bufferedWriter().use(Routing.Companion::appendCleanCommands)
|
||||
|
||||
@@ -20,7 +20,6 @@ import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
|
||||
object WifiApCommands {
|
||||
@RequiresApi(28)
|
||||
sealed class SoftApCallbackParcel : Parcelable {
|
||||
abstract fun dispatch(callback: WifiApManager.SoftApCallbackCompat)
|
||||
|
||||
@@ -60,7 +59,6 @@ object WifiApCommands {
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
@RequiresApi(28)
|
||||
class RegisterSoftApCallback : RootCommandChannel<SoftApCallbackParcel> {
|
||||
override fun create(scope: CoroutineScope) = scope.produce(capacity = capacity) {
|
||||
val finish = CompletableDeferred<Unit>()
|
||||
@@ -115,7 +113,6 @@ object WifiApCommands {
|
||||
private val callbacks = mutableSetOf<WifiApManager.SoftApCallbackCompat>()
|
||||
private val lastCallback = AutoFiringCallbacks()
|
||||
private var rootCallbackJob: Job? = null
|
||||
@RequiresApi(28)
|
||||
private suspend fun handleChannel(channel: ReceiveChannel<SoftApCallbackParcel>) = channel.consumeEach { parcel ->
|
||||
when (parcel) {
|
||||
is SoftApCallbackParcel.OnStateChanged -> synchronized(callbacks) { lastCallback.state = parcel }
|
||||
@@ -141,7 +138,6 @@ object WifiApCommands {
|
||||
}
|
||||
for (callback in synchronized(callbacks) { callbacks.toList() }) parcel.dispatch(callback)
|
||||
}
|
||||
@RequiresApi(28)
|
||||
fun registerSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) {
|
||||
val wasEmpty = callbacks.isEmpty()
|
||||
callbacks.add(callback)
|
||||
@@ -158,7 +154,6 @@ object WifiApCommands {
|
||||
null
|
||||
} else lastCallback
|
||||
}?.toSequence()?.forEach { it?.dispatch(callback) }
|
||||
@RequiresApi(28)
|
||||
fun unregisterSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) {
|
||||
if (callbacks.remove(callback) && callbacks.isEmpty()) {
|
||||
rootCallbackJob!!.cancel()
|
||||
|
||||
@@ -30,10 +30,8 @@ class ConstantLookup(private val prefix: String, private val lookup29: Array<out
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("FunctionName")
|
||||
fun ConstantLookup(prefix: String, vararg lookup29: String?, clazz: () -> Class<*>) =
|
||||
ConstantLookup(prefix, lookup29, clazz)
|
||||
@Suppress("FunctionName")
|
||||
inline fun <reified T> ConstantLookup(prefix: String, vararg lookup29: String?) =
|
||||
ConstantLookup(prefix, lookup29) { T::class.java }
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package be.mygod.vpnhotspot.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
|
||||
@SuppressLint("Registered")
|
||||
@TargetApi(24)
|
||||
class DeviceStorageApp(context: Context) : Application() {
|
||||
init {
|
||||
attachBaseContext(context.createDeviceProtectedStorageContext())
|
||||
|
||||
@@ -8,10 +8,8 @@ import android.os.DeadObjectException
|
||||
import android.os.IBinder
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.annotation.RequiresApi
|
||||
import be.mygod.vpnhotspot.BootReceiver
|
||||
|
||||
@RequiresApi(24)
|
||||
abstract class KillableTileService : TileService(), ServiceConnection {
|
||||
protected var tapPending = false
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ class RootSession : AutoCloseable {
|
||||
|
||||
private var server: RootServer? = runBlocking { RootManager.acquire() }
|
||||
override fun close() {
|
||||
server = null
|
||||
server?.let { runBlocking { RootManager.release(it) } }
|
||||
server = null
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.net.ConnectivityManager
|
||||
import android.net.NetworkRequest
|
||||
import android.net.wifi.WifiManager
|
||||
import android.net.wifi.p2p.WifiP2pManager
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.core.content.getSystemService
|
||||
@@ -30,7 +29,6 @@ object Services {
|
||||
}
|
||||
val wifi by lazy { context.getSystemService<WifiManager>()!! }
|
||||
|
||||
fun registerNetworkCallbackCompat(request: NetworkRequest, networkCallback: ConnectivityManager.NetworkCallback) =
|
||||
if (Build.VERSION.SDK_INT >= 26) connectivity.registerNetworkCallback(request, networkCallback, mainHandler)
|
||||
else connectivity.registerNetworkCallback(request, networkCallback)
|
||||
fun registerNetworkCallback(request: NetworkRequest, networkCallback: ConnectivityManager.NetworkCallback) =
|
||||
connectivity.registerNetworkCallback(request, networkCallback, mainHandler)
|
||||
}
|
||||
|
||||
@@ -13,9 +13,7 @@ import me.weishu.reflection.Reflection
|
||||
*
|
||||
* Lazy cannot be used directly as it will create inner classes.
|
||||
*/
|
||||
@RequiresApi(28)
|
||||
@SuppressLint("BlockedPrivateApi", "DiscouragedPrivateApi")
|
||||
@Suppress("FunctionName")
|
||||
object UnblockCentral {
|
||||
var needInit = true
|
||||
/**
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
package be.mygod.vpnhotspot.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.content.*
|
||||
import android.content.res.Resources
|
||||
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.*
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.databinding.BindingAdapter
|
||||
@@ -24,18 +19,11 @@ import androidx.fragment.app.FragmentManager
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.lang.invoke.MethodHandles
|
||||
import java.lang.reflect.InvocationHandler
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.lang.reflect.Method
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.net.SocketException
|
||||
@@ -57,23 +45,10 @@ fun Long.toPluralInt(): Int {
|
||||
return (this % 1000000000).toInt() + 1000000000
|
||||
}
|
||||
|
||||
@RequiresApi(26)
|
||||
fun Method.matches(name: String, vararg classes: Class<*>) = this.name == name && parameterCount == classes.size &&
|
||||
classes.indices.all { i -> parameters[i].type == classes[i] }
|
||||
@RequiresApi(26)
|
||||
inline fun <reified T> Method.matches1(name: String) = matches(name, T::class.java)
|
||||
|
||||
fun Method.matchesCompat(name: String, args: Array<out Any?>?, vararg classes: Class<*>) =
|
||||
if (Build.VERSION.SDK_INT < 26) {
|
||||
this.name == name && args?.size ?: 0 == classes.size && classes.indices.all { i ->
|
||||
args!![i]?.let { classes[i].isInstance(it) } != false
|
||||
}
|
||||
} else matches(name, *classes)
|
||||
|
||||
fun HttpURLConnection.disconnectCompat() {
|
||||
if (Build.VERSION.SDK_INT < 26) GlobalScope.launch(Dispatchers.IO) { disconnect() } else disconnect()
|
||||
}
|
||||
|
||||
fun Context.ensureReceiverUnregistered(receiver: BroadcastReceiver) {
|
||||
try {
|
||||
unregisterReceiver(receiver)
|
||||
@@ -167,7 +142,7 @@ fun makeMacSpan(mac: String) = if (app.hasTouch) SpannableString(mac).apply {
|
||||
|
||||
fun NetworkInterface.formatAddresses(macOnly: Boolean = false) = SpannableStringBuilder().apply {
|
||||
try {
|
||||
val address = hardwareAddress?.let(MacAddressCompat::fromBytes)
|
||||
val address = hardwareAddress?.let(MacAddress::fromBytes)
|
||||
if (address != null && address != MacAddressCompat.ANY_ADDRESS) appendLine(makeMacSpan(address.toString()))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.w(e)
|
||||
@@ -219,8 +194,7 @@ fun Resources.findIdentifier(name: String, defType: String, defPackage: String,
|
||||
if (alternativePackage != null && it == 0) getIdentifier(name, defType, alternativePackage) else it
|
||||
}
|
||||
|
||||
@get:RequiresApi(26)
|
||||
private val newLookup by lazy @TargetApi(26) {
|
||||
private val newLookup by lazy {
|
||||
MethodHandles.Lookup::class.java.getDeclaredConstructor(Class::class.java, Int::class.java).apply {
|
||||
isAccessible = true
|
||||
}
|
||||
@@ -232,7 +206,7 @@ private val newLookup by lazy @TargetApi(26) {
|
||||
* See also: https://stackoverflow.com/a/49532463/2245107
|
||||
*/
|
||||
fun InvocationHandler.callSuper(interfaceClass: Class<*>, proxy: Any, method: Method, args: Array<out Any?>?) = when {
|
||||
Build.VERSION.SDK_INT >= 26 && method.isDefault -> try {
|
||||
method.isDefault -> try {
|
||||
newLookup.newInstance(interfaceClass, 0xf) // ALL_MODES
|
||||
} catch (e: ReflectiveOperationException) {
|
||||
Timber.w(e)
|
||||
@@ -258,20 +232,5 @@ 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)
|
||||
} else try {
|
||||
File("/sys/class/net/$ifname/ifindex").inputStream().bufferedReader().use { it.readLine().trim().toInt() }
|
||||
} catch (_: FileNotFoundException) {
|
||||
NetworkInterface.getByName(ifname)?.index ?: 0
|
||||
} catch (e: IOException) {
|
||||
if ((e.cause as? ErrnoException)?.errno == OsConstants.ENODEV) 0 else throw e
|
||||
if (Build.VERSION.SDK_INT >= 31) setIncludeOtherUidNetworks(true)
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -53,7 +53,6 @@
|
||||
se il tethering VPN non funziona.</string>
|
||||
<string name="tethering_manage_usb">Tethering USB</string>
|
||||
<string name="tethering_manage_wifi">Hotspot Wi\u2011Fi</string>
|
||||
<string name="tethering_manage_wifi_legacy">Hotspot Wi\u2011Fi (legacy)</string>
|
||||
<string name="tethering_manage_bluetooth">Tethering Bluetooth</string>
|
||||
<string name="tethering_manage_ethernet" msgid="959743110824197356">"Tethering Ethernet"</string>
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@
|
||||
<string name="tethering_manage_offload_enabled">Por favor, desative a Aceleração de hardware de tethering nas configurações de desenvolvedor caso o VPN Hotspot não funcionar corretamente.</string>
|
||||
<string name="tethering_manage_usb">Tethering USB</string>
|
||||
<string name="tethering_manage_wifi">Ponto de acesso Wi\u2011Fi</string>
|
||||
<string name="tethering_manage_wifi_legacy">Ponto de acesso Wi\u2011Fi (legacy)</string>
|
||||
<string name="tethering_manage_bluetooth">Tethering Bluetooth</string>
|
||||
<string name="tethering_manage_ethernet">Tethering Ethernet</string>
|
||||
<string name="tethering_manage_ncm">Tethering USB (NCM)</string>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<bool name="api_lt_25">false</bool>
|
||||
</resources>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<bool name="api_ge_26">true</bool>
|
||||
</resources>
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string-array name="settings_service_masquerade" tools:ignore="InconsistentArrays">
|
||||
<item>@string/settings_service_masquerade_none</item>
|
||||
<item>@string/settings_service_masquerade_simple</item>
|
||||
<item>@string/settings_service_masquerade_netd</item>
|
||||
</string-array>
|
||||
<string-array name="settings_service_masquerade_values" tools:ignore="InconsistentArrays">
|
||||
<item>None</item>
|
||||
<item>Simple</item>
|
||||
<item>Netd</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -57,7 +57,6 @@
|
||||
-->
|
||||
<string name="tethering_manage_usb">USB 网络共享</string>
|
||||
<string name="tethering_manage_wifi">WLAN 热点</string>
|
||||
<string name="tethering_manage_wifi_legacy">WLAN 热点 (旧 API)</string>
|
||||
<string name="tethering_manage_bluetooth">蓝牙网络共享</string>
|
||||
<string name="tethering_manage_ethernet" msgid="959743110824197356">"以太网络共享"</string>
|
||||
<string name="tethering_manage_ncm">USB 网络共享 (NCM)</string>
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
-->
|
||||
<string name="tethering_manage_usb">USB 網路共用</string>
|
||||
<string name="tethering_manage_wifi">Wi\u2011Fi 無線基地台</string>
|
||||
<string name="tethering_manage_wifi_legacy">Wi\u2011Fi 無線基地台 (舊版)</string>
|
||||
<string name="tethering_manage_bluetooth">藍牙網路共用</string>
|
||||
<string name="tethering_manage_ethernet" msgid="959743110824197356">"乙太網路網路共用"</string>
|
||||
<string name="tethering_manage_ncm">USB 網路共用 (NCM)</string>
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
<string-array name="settings_service_masquerade" tools:ignore="InconsistentArrays">
|
||||
<item>@string/settings_service_masquerade_none</item>
|
||||
<item>@string/settings_service_masquerade_simple</item>
|
||||
<item>@string/settings_service_masquerade_netd</item>
|
||||
</string-array>
|
||||
<string-array name="settings_service_masquerade_values" tools:ignore="InconsistentArrays">
|
||||
<item>None</item>
|
||||
<item>Simple</item>
|
||||
<item>Netd</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="settings_service_wifi_lock">
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<bool name="api_ge_30">false</bool>
|
||||
<bool name="api_ge_26">false</bool>
|
||||
<bool name="api_lt_25">true</bool>
|
||||
<bool name="is_day">true</bool>
|
||||
</resources>
|
||||
|
||||
@@ -69,7 +69,6 @@
|
||||
if VPN tethering does not work.</string>
|
||||
<string name="tethering_manage_usb">USB tethering</string>
|
||||
<string name="tethering_manage_wifi">Wi\u2011Fi hotspot</string>
|
||||
<string name="tethering_manage_wifi_legacy">Wi\u2011Fi hotspot (legacy)</string>
|
||||
<string name="tethering_manage_bluetooth">Bluetooth tethering</string>
|
||||
<string name="tethering_manage_ethernet">Ethernet tethering</string>
|
||||
<string name="tethering_manage_ncm">USB tethering (NCM)</string>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<item name="android:navigationBarColor">@color/navigationBarColor</item>
|
||||
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">@bool/is_day</item>
|
||||
<item name="android:windowLightNavigationBar">@bool/is_day</item>
|
||||
<item name="actionBarStyle">@style/Widget.MaterialComponents.Light.ActionBar.Solid</item>
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
||||
class MacAddressCompatTest {
|
||||
@Test
|
||||
fun macSerialization() {
|
||||
for (test in listOf("01:23:45:67:89:ab", "DE:AD:88:88:BE:EF")) {
|
||||
assertTrue(test.equals(MacAddressCompat.fromString(test).toString(), true))
|
||||
}
|
||||
}
|
||||
}
|
||||