Merge pull request #90 from Mygod/q-beta
Support Android Q beta 4/Android 10.
This commit is contained in:
69
README.md
69
README.md
@@ -65,8 +65,20 @@ Default settings are picked to suit general use cases and maximize compatibility
|
|||||||
|
|
||||||
* Keep Wi-Fi alive: Acquire Wi-Fi locks when repeater, temporary hotspot or system VPN hotspot is activated.
|
* Keep Wi-Fi alive: Acquire Wi-Fi locks when repeater, temporary hotspot or system VPN hotspot is activated.
|
||||||
- Choose "System default" to save battery life;
|
- Choose "System default" to save battery life;
|
||||||
- Choose "On" (default) if repeater/hotspot turns itself off automatically or stops working after a while;
|
- (prior to Android 10) Choose "On" (default) if repeater/hotspot turns itself off automatically or stops working after a while;
|
||||||
- Choose "High Performance Mode" to minimize packet loss and latency (will consume more power).
|
- (prior to Android 10) Choose "High Performance Mode" to minimize packet loss and latency (will consume more power);
|
||||||
|
- (since Android 10) Choose "Disable power save" to decrease packet latency.
|
||||||
|
An example use case is when a voice connection needs to be kept active even after the device screen goes off.
|
||||||
|
Using this mode may improve the call quality.
|
||||||
|
Requires support from the hardware.
|
||||||
|
- (since Android 10) Choose "Low latency mode" to optimize for reduced packet latency, and this might result in:
|
||||||
|
1. Reduced battery life.
|
||||||
|
2. Reduced throughput.
|
||||||
|
3. Reduced frequency of Wi-Fi scanning.
|
||||||
|
This may cause the device not roaming or switching to the AP with highest signal quality, and location accuracy may be reduced.
|
||||||
|
Example use cases are real time gaming or virtual reality applications where low latency is a key factor for user experience.
|
||||||
|
Requires support from the hardware.
|
||||||
|
Note: Requires this app running in foreground with screen on.
|
||||||
* Start repeater on boot: Self explanatory.
|
* Start repeater on boot: Self explanatory.
|
||||||
* Network status monitor mode: This option controls how the app monitors connected devices as well as interface changes
|
* Network status monitor mode: This option controls how the app monitors connected devices as well as interface changes
|
||||||
(when custom upstream is used).
|
(when custom upstream is used).
|
||||||
@@ -107,27 +119,27 @@ _a.k.a. things that can go wrong if this app doesn't work._
|
|||||||
This is a list of stuff that might impact this app's functionality if unavailable.
|
This is a list of stuff that might impact this app's functionality if unavailable.
|
||||||
This is only meant to be an index. You can read more in the source code.
|
This is only meant to be an index. You can read more in the source code.
|
||||||
|
|
||||||
Undocumented API list:
|
Non-public API list:
|
||||||
|
|
||||||
* (since API 24) [`Landroid/bluetooth/BluetoothPan;->isTetheringOn()Z,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#28703)
|
* (since API 24) [`Landroid/bluetooth/BluetoothPan;->isTetheringOn()Z,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#32103)
|
||||||
* (since API 24) [`Landroid/net/ConnectivityManager$OnStartTetheringCallback;-><init>()V,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#112695)
|
* (since API 24) [`Landroid/net/ConnectivityManager$OnStartTetheringCallback;-><init>()V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#123103)
|
||||||
* (since API 24) [`Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringFailed()V,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#112696)
|
* (since API 24) [`Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringFailed()V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#123104)
|
||||||
* (since API 24) [`Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringStarted()V,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#112697)
|
* (since API 24) [`Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringStarted()V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#123105)
|
||||||
* (since API 24) [`Landroid/net/ConnectivityManager;->getLastTetherError(Ljava/lang/String;)I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#112882)
|
* (since API 24) [`Landroid/net/ConnectivityManager;->getLastTetherError(Ljava/lang/String;)I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#123309)
|
||||||
* (since API 24) [`Landroid/net/ConnectivityManager;->startTethering(IZLandroid/net/ConnectivityManager$OnStartTetheringCallback;Landroid/os/Handler;)V,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#112972)
|
* (since API 24) [`Landroid/net/ConnectivityManager;->startTethering(IZLandroid/net/ConnectivityManager$OnStartTetheringCallback;Landroid/os/Handler;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#123408)
|
||||||
* (since API 24) [`Landroid/net/ConnectivityManager;->stopTethering(I)V,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#112974)
|
* (since API 24) [`Landroid/net/ConnectivityManager;->stopTethering(I)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#123410)
|
||||||
* (since API 23) [`Landroid/net/wifi/WifiConfiguration;->apBand:I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#120723)
|
* (since API 23) [`Landroid/net/wifi/WifiConfiguration;->apBand:I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#131529)
|
||||||
* (since API 23) [`Landroid/net/wifi/WifiConfiguration;->apChannel:I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#120724)
|
* (since API 23) [`Landroid/net/wifi/WifiConfiguration;->apChannel:I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#131530)
|
||||||
* [`Landroid/net/wifi/WifiManager;->getWifiApConfiguration()Landroid/net/wifi/WifiConfiguration;,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#121357)
|
* [`Landroid/net/wifi/WifiManager;->getWifiApConfiguration()Landroid/net/wifi/WifiConfiguration;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#132289)
|
||||||
* [`Landroid/net/wifi/WifiManager;->setWifiApConfiguration(Landroid/net/wifi/WifiConfiguration;)Z,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#121416)
|
* [`Landroid/net/wifi/WifiManager;->setWifiApConfiguration(Landroid/net/wifi/WifiConfiguration;)Z,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#132358)
|
||||||
* (deprecated since API 26) `Landroid/net/wifi/WifiManager;->setWifiApEnabled(Landroid/net/wifi/WifiConfiguration;Z)Z`
|
* (deprecated since API 26) `Landroid/net/wifi/WifiManager;->setWifiApEnabled(Landroid/net/wifi/WifiConfiguration;Z)Z`
|
||||||
* [`Landroid/net/wifi/p2p/WifiP2pGroup;->getNetworkId()I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#123194)
|
* (prior to API 29) [`Landroid/net/wifi/p2p/WifiP2pGroup;->getNetworkId()I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#134440)
|
||||||
* [`Landroid/net/wifi/p2p/WifiP2pGroupList;->getGroupList()Ljava/util/Collection;,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#123239)
|
* (prior to API 29) [`Landroid/net/wifi/p2p/WifiP2pGroupList;->getGroupList()Ljava/util/Collection;,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#134487)
|
||||||
* [`Landroid/net/wifi/p2p/WifiP2pManager;->deletePersistentGroup(Landroid/net/wifi/p2p/WifiP2pManager$Channel;ILandroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#123431)
|
* (prior to API 29) [`Landroid/net/wifi/p2p/WifiP2pManager;->deletePersistentGroup(Landroid/net/wifi/p2p/WifiP2pManager$Channel;ILandroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#134703)
|
||||||
* [`Landroid/net/wifi/p2p/WifiP2pManager;->requestPersistentGroupInfo(Landroid/net/wifi/p2p/WifiP2pManager$Channel;Landroid/net/wifi/p2p/WifiP2pManager$PersistentGroupInfoListener;)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#123450)
|
* (prior to API 29) [`Landroid/net/wifi/p2p/WifiP2pManager;->requestPersistentGroupInfo(Landroid/net/wifi/p2p/WifiP2pManager$Channel;Landroid/net/wifi/p2p/WifiP2pManager$PersistentGroupInfoListener;)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#134728)
|
||||||
* [`Landroid/net/wifi/p2p/WifiP2pManager;->setWifiP2pChannels(Landroid/net/wifi/p2p/WifiP2pManager$Channel;IILandroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#123458)
|
* (prior to API 29) [`Landroid/net/wifi/p2p/WifiP2pManager;->setWifiP2pChannels(Landroid/net/wifi/p2p/WifiP2pManager$Channel;IILandroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#134737)
|
||||||
* [`Landroid/net/wifi/p2p/WifiP2pManager;->startWps(Landroid/net/wifi/p2p/WifiP2pManager$Channel;Landroid/net/wifi/WpsInfo;Landroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#123459)
|
* [`Landroid/net/wifi/p2p/WifiP2pManager;->startWps(Landroid/net/wifi/p2p/WifiP2pManager$Channel;Landroid/net/wifi/WpsInfo;Landroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#134738)
|
||||||
* [`Ljava/net/InetAddress;->parseNumericAddress(Ljava/lang/String;)Ljava/net/InetAddress;,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#299587)
|
* (prior to API 29) [`Ljava/net/InetAddress;->parseNumericAddress(Ljava/lang/String;)Ljava/net/InetAddress;,core-platform-api,greylist-max-p`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#335306)
|
||||||
|
|
||||||
Undocumented system configurations:
|
Undocumented system configurations:
|
||||||
|
|
||||||
@@ -138,21 +150,22 @@ Undocumented system configurations:
|
|||||||
|
|
||||||
Other:
|
Other:
|
||||||
|
|
||||||
* (since API 27) [`Landroid/provider/Settings$Global;->TETHER_OFFLOAD_DISABLED:Ljava/lang/String;,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#144760) is assumed to be `"tether_offload_disabled"`.
|
* (since API 29) `android.net.wifi.p2p.WifiP2pConfig` needs to be parcelized in a very specific order.
|
||||||
|
* (since API 27) [`Landroid/provider/Settings$Global;->TETHER_OFFLOAD_DISABLED:Ljava/lang/String;,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#158331) is assumed to be `"tether_offload_disabled"`.
|
||||||
* (since API 27) `com.android.server.connectivity.tethering.OffloadHardwareInterface.DEFAULT_TETHER_OFFLOAD_DISABLED` is assumed to be 0.
|
* (since API 27) `com.android.server.connectivity.tethering.OffloadHardwareInterface.DEFAULT_TETHER_OFFLOAD_DISABLED` is assumed to be 0.
|
||||||
* Several constants in `ConnectivityManager` is assumed to be defined as in `TetheringManager.kt`;
|
* Several constants in `ConnectivityManager` is assumed to be defined as in `TetheringManager.kt`;
|
||||||
* Following broadcasts are assumed to be sticky:
|
* Following broadcasts are assumed to be sticky:
|
||||||
- [`Landroid/net/ConnectivityManager;->ACTION_TETHER_STATE_CHANGED:Ljava/lang/String;,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#112743) is assumed to be `android.net.conn.TETHER_STATE_CHANGED`.
|
- [`Landroid/net/ConnectivityManager;->ACTION_TETHER_STATE_CHANGED:Ljava/lang/String;,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#123163) is assumed to be `android.net.conn.TETHER_STATE_CHANGED`.
|
||||||
- [`Landroid/net/wifi/p2p/WifiP2pManager;->WIFI_P2P_PERSISTENT_GROUPS_CHANGED_ACTION:Ljava/lang/String;,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#123415) is assumed to be `android.net.wifi.p2p.PERSISTENT_GROUPS_CHANGED`;
|
- (prior to API 29) [`Landroid/net/wifi/p2p/WifiP2pManager;->WIFI_P2P_PERSISTENT_GROUPS_CHANGED_ACTION:Ljava/lang/String;,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#134686) is assumed to be `android.net.wifi.p2p.PERSISTENT_GROUPS_CHANGED`;
|
||||||
* Activity `com.android.settings/.Settings$TetherSettingsActivity` is assumed to be exported.
|
* 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
|
For `ip rule` priorities, `RULE_PRIORITY_SECURE_VPN` and `RULE_PRIORITY_TETHERING` is assumed to be 12000 and 18000 respectively;
|
||||||
respectively; `RULE_PRIORITY_DEFAULT_NETWORK` is assumed to be 22000 (or at least > 18000) for API 27-.
|
(prior to API 24) `RULE_PRIORITY_DEFAULT_NETWORK` is assumed to be 22000 (or at least > 18000).
|
||||||
DHCP server like `dnsmasq` is assumed to run and send DHCP packets as root.
|
DHCP server like `dnsmasq` is assumed to run and send DHCP packets as root.
|
||||||
|
|
||||||
Undocumented system binaries are all bundled and executable:
|
Undocumented system binaries are all bundled and executable:
|
||||||
|
|
||||||
* Since API 24: `iptables-save`;
|
* (since API 24) `iptables-save`;
|
||||||
* `echo`;
|
* `echo`;
|
||||||
* `ip` (`link monitor neigh rule` with proper output format);
|
* `ip` (`link monitor neigh rule` with proper output format);
|
||||||
* `ndc` (`ipfwd` with proper output format since API 23, `nat`);
|
* `ndc` (`ipfwd` with proper output format since API 23, `nat`);
|
||||||
@@ -161,7 +174,7 @@ Undocumented system binaries are all bundled and executable:
|
|||||||
|
|
||||||
If some of these are unavailable, you can alternatively install a recent version (v1.28.1 or higher) of Busybox.
|
If some of these are unavailable, you can alternatively install a recent version (v1.28.1 or higher) of Busybox.
|
||||||
|
|
||||||
Wi-Fi driver `wpa_supplicant`:
|
(prior to API 29) Wi-Fi driver `wpa_supplicant`:
|
||||||
|
|
||||||
* P2P configuration file is assumed to be saved to [`/data/vendor/wifi/wpa/p2p_supplicant.conf` or `/data/misc/wifi/p2p_supplicant.conf`](https://android.googlesource.com/platform/external/wpa_supplicant_8/+/0b4856b6dc451e290f1f64f6af17e010be78c073/wpa_supplicant/hidl/1.1/supplicant.cpp#26) and have reasonable format;
|
* P2P configuration file is assumed to be saved to [`/data/vendor/wifi/wpa/p2p_supplicant.conf` or `/data/misc/wifi/p2p_supplicant.conf`](https://android.googlesource.com/platform/external/wpa_supplicant_8/+/0b4856b6dc451e290f1f64f6af17e010be78c073/wpa_supplicant/hidl/1.1/supplicant.cpp#26) and have reasonable format;
|
||||||
* Android system is expected to restart `wpa_supplicant` after it crashes.
|
* Android system is expected to restart `wpa_supplicant` after it crashes.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ buildscript {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.4.0'
|
classpath 'com.android.tools.build:gradle:3.5.0-beta04'
|
||||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.21.0'
|
classpath 'com.github.ben-manes:gradle-versions-plugin:0.21.0'
|
||||||
classpath 'com.google.gms:google-services:4.2.0'
|
classpath 'com.google.gms:google-services:4.2.0'
|
||||||
classpath 'io.fabric.tools:gradle:1.29.0'
|
classpath 'io.fabric.tools:gradle:1.29.0'
|
||||||
|
|||||||
@@ -363,7 +363,7 @@ style:
|
|||||||
OptionalUnit:
|
OptionalUnit:
|
||||||
active: true
|
active: true
|
||||||
OptionalWhenBraces:
|
OptionalWhenBraces:
|
||||||
active: true
|
active: false
|
||||||
PreferToOverPairSyntax:
|
PreferToOverPairSyntax:
|
||||||
active: false
|
active: false
|
||||||
ProtectedMemberInFinalClass:
|
ProtectedMemberInFinalClass:
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
# Specifies the JVM arguments used for the daemon process.
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
android.enableR8=true
|
|
||||||
android.enableR8.fullMode=true
|
android.enableR8.fullMode=true
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
org.gradle.jvmargs=-Xmx1536m
|
org.gradle.jvmargs=-Xmx1536m
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ if (!getGradle().getStartParameter().getTaskRequests().toString().contains("Fdro
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
buildToolsVersion "28.0.3"
|
compileSdkVersion 29
|
||||||
compileSdkVersion 28
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility 1.8
|
sourceCompatibility 1.8
|
||||||
targetCompatibility 1.8
|
targetCompatibility 1.8
|
||||||
@@ -18,7 +17,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "be.mygod.vpnhotspot"
|
applicationId "be.mygod.vpnhotspot"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 28
|
targetSdkVersion 29
|
||||||
resConfigs "ru", "zh-rCN"
|
resConfigs "ru", "zh-rCN"
|
||||||
versionCode 204
|
versionCode 204
|
||||||
versionName '2.4.4'
|
versionName '2.4.4'
|
||||||
@@ -65,25 +64,26 @@ androidExtensions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def aux = [
|
def aux = [
|
||||||
'com.crashlytics.sdk.android:crashlytics:2.10.0',
|
'com.crashlytics.sdk.android:crashlytics:2.10.1',
|
||||||
'com.google.firebase:firebase-core:16.0.9',
|
'com.google.firebase:firebase-core:16.0.9',
|
||||||
]
|
]
|
||||||
def lifecycleVersion = '2.0.0'
|
def lifecycleVersion = '2.1.0-beta01'
|
||||||
def roomVersion = '2.1.0-beta01'
|
def roomVersion = '2.1.0-rc01'
|
||||||
dependencies {
|
dependencies {
|
||||||
kapt "androidx.room:room-compiler:$roomVersion"
|
kapt "androidx.room:room-compiler:$roomVersion"
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
implementation 'androidx.browser:browser:1.0.0'
|
implementation 'androidx.browser:browser:1.0.0'
|
||||||
implementation 'androidx.core:core-ktx:1.0.2'
|
implementation 'androidx.core:core-ktx:1.1.0-rc01'
|
||||||
implementation 'androidx.emoji:emoji:1.0.0'
|
implementation 'androidx.emoji:emoji:1.0.0'
|
||||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
|
||||||
implementation 'androidx.preference:preference:1.1.0-alpha05'
|
implementation 'androidx.preference:preference:1.1.0-beta01'
|
||||||
implementation "androidx.room:room-ktx:$roomVersion"
|
implementation "androidx.room:room-ktx:$roomVersion"
|
||||||
implementation 'com.android.billingclient:billing:2.0.0'
|
implementation 'com.android.billingclient:billing:2.0.1'
|
||||||
implementation 'com.github.topjohnwu.libsu:core:2.5.0'
|
implementation 'com.github.topjohnwu.libsu:core:2.5.0'
|
||||||
implementation 'com.google.android.material:material:1.1.0-alpha06'
|
implementation 'com.google.android.material:material:1.1.0-alpha07'
|
||||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||||
implementation 'com.linkedin.dexmaker:dexmaker:2.25.0'
|
implementation 'com.linkedin.dexmaker:dexmaker:2.25.0'
|
||||||
implementation 'com.takisoft.preferencex:preferencex-simplemenu:1.0.0'
|
implementation 'com.takisoft.preferencex:preferencex-simplemenu:1.0.0'
|
||||||
@@ -94,10 +94,9 @@ dependencies {
|
|||||||
freedomImplementation dep
|
freedomImplementation dep
|
||||||
googleImplementation dep
|
googleImplementation dep
|
||||||
}
|
}
|
||||||
testImplementation "androidx.arch.core:core-testing:$lifecycleVersion"
|
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
androidTestImplementation "androidx.room:room-testing:$roomVersion"
|
androidTestImplementation "androidx.room:room-testing:$roomVersion"
|
||||||
androidTestImplementation 'androidx.test:runner:1.1.1'
|
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.0'
|
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.1-beta01'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,8 @@
|
|||||||
<uses-permission android:name="android.permission.WRITE_SETTINGS"
|
<uses-permission android:name="android.permission.WRITE_SETTINGS"
|
||||||
tools:ignore="ProtectedPermissions"/>
|
tools:ignore="ProtectedPermissions"/>
|
||||||
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||||
|
<!-- Required since API 29 -->
|
||||||
|
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ import android.content.res.Configuration
|
|||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.wifi.WifiManager
|
import android.net.wifi.WifiManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.preference.PreferenceManager
|
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.provider.FontRequest
|
import androidx.core.provider.FontRequest
|
||||||
import androidx.emoji.text.EmojiCompat
|
import androidx.emoji.text.EmojiCompat
|
||||||
import androidx.emoji.text.FontRequestEmojiCompatConfig
|
import androidx.emoji.text.FontRequestEmojiCompatConfig
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
import be.mygod.vpnhotspot.net.DhcpWorkaround
|
import be.mygod.vpnhotspot.net.DhcpWorkaround
|
||||||
import be.mygod.vpnhotspot.room.AppDatabase
|
import be.mygod.vpnhotspot.room.AppDatabase
|
||||||
import be.mygod.vpnhotspot.util.DeviceStorageApp
|
import be.mygod.vpnhotspot.util.DeviceStorageApp
|
||||||
@@ -33,7 +33,8 @@ class App : Application() {
|
|||||||
app = this
|
app = this
|
||||||
if (Build.VERSION.SDK_INT >= 24) {
|
if (Build.VERSION.SDK_INT >= 24) {
|
||||||
deviceStorage = DeviceStorageApp(this)
|
deviceStorage = DeviceStorageApp(this)
|
||||||
deviceStorage.moveSharedPreferencesFrom(this, PreferenceManager.getDefaultSharedPreferencesName(this))
|
// alternative to PreferenceManager.getDefaultSharedPreferencesName(this)
|
||||||
|
deviceStorage.moveSharedPreferencesFrom(this, PreferenceManager(this).sharedPreferencesName)
|
||||||
deviceStorage.moveDatabaseFrom(this, AppDatabase.DB_NAME)
|
deviceStorage.moveDatabaseFrom(this, AppDatabase.DB_NAME)
|
||||||
} else deviceStorage = this
|
} else deviceStorage = this
|
||||||
DebugHelper.init()
|
DebugHelper.init()
|
||||||
@@ -53,7 +54,7 @@ class App : Application() {
|
|||||||
if (DhcpWorkaround.shouldEnable) DhcpWorkaround.enable(true)
|
if (DhcpWorkaround.shouldEnable) DhcpWorkaround.enable(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration?) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
super.onConfigurationChanged(newConfig)
|
super.onConfigurationChanged(newConfig)
|
||||||
ServiceNotification.updateNotificationChannels()
|
ServiceNotification.updateNotificationChannels()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService() {
|
|||||||
private var routingManager: RoutingManager? = null
|
private var routingManager: RoutingManager? = null
|
||||||
private var receiverRegistered = false
|
private var receiverRegistered = false
|
||||||
private val receiver = broadcastReceiver { _, intent ->
|
private val receiver = broadcastReceiver { _, intent ->
|
||||||
val ifaces = intent.localOnlyTetheredIfaces
|
val ifaces = intent.localOnlyTetheredIfaces ?: return@broadcastReceiver
|
||||||
DebugHelper.log(TAG, "onTetherStateChangedLocked: $ifaces")
|
DebugHelper.log(TAG, "onTetherStateChangedLocked: $ifaces")
|
||||||
check(ifaces.size <= 1)
|
check(ifaces.size <= 1)
|
||||||
val iface = ifaces.singleOrNull()
|
val iface = ifaces.singleOrNull()
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
import androidx.lifecycle.get
|
import androidx.lifecycle.get
|
||||||
|
import androidx.lifecycle.observe
|
||||||
import be.mygod.vpnhotspot.client.ClientViewModel
|
import be.mygod.vpnhotspot.client.ClientViewModel
|
||||||
import be.mygod.vpnhotspot.client.ClientsFragment
|
import be.mygod.vpnhotspot.client.ClientsFragment
|
||||||
import be.mygod.vpnhotspot.databinding.ActivityMainBinding
|
import be.mygod.vpnhotspot.databinding.ActivityMainBinding
|
||||||
import be.mygod.vpnhotspot.manage.TetheringFragment
|
import be.mygod.vpnhotspot.manage.TetheringFragment
|
||||||
|
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
|
||||||
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
@@ -29,14 +30,15 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
|||||||
if (savedInstanceState == null) displayFragment(TetheringFragment())
|
if (savedInstanceState == null) displayFragment(TetheringFragment())
|
||||||
val model = ViewModelProviders.of(this).get<ClientViewModel>()
|
val model = ViewModelProviders.of(this).get<ClientViewModel>()
|
||||||
if (RepeaterService.supported) ServiceForegroundConnector(this, model, RepeaterService::class)
|
if (RepeaterService.supported) ServiceForegroundConnector(this, model, RepeaterService::class)
|
||||||
model.clients.observe(this, Observer {
|
model.clients.observe(this) {
|
||||||
if (it.isNotEmpty()) binding.navigation.showBadge(R.id.navigation_clients).apply {
|
if (it.isNotEmpty()) binding.navigation.showBadge(R.id.navigation_clients).apply {
|
||||||
backgroundColor = ContextCompat.getColor(this@MainActivity, R.color.colorSecondary)
|
backgroundColor = ContextCompat.getColor(this@MainActivity, R.color.colorSecondary)
|
||||||
badgeTextColor = ContextCompat.getColor(this@MainActivity, R.color.primary_text_default_material_light)
|
badgeTextColor = ContextCompat.getColor(this@MainActivity, R.color.primary_text_default_material_light)
|
||||||
number = it.size
|
number = it.size
|
||||||
} else binding.navigation.removeBadge(R.id.navigation_clients)
|
} else binding.navigation.removeBadge(R.id.navigation_clients)
|
||||||
})
|
}
|
||||||
SmartSnackbar.Register(lifecycle, binding.fragmentHolder)
|
SmartSnackbar.Register(lifecycle, binding.fragmentHolder)
|
||||||
|
WifiDoubleLock.ActivityListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigationItemSelected(item: MenuItem) = when (item.itemId) {
|
override fun onNavigationItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
package be.mygod.vpnhotspot
|
package be.mygod.vpnhotspot
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.net.NetworkInfo
|
import android.net.wifi.WpsInfo
|
||||||
import android.net.wifi.p2p.WifiP2pDevice
|
import android.net.wifi.p2p.*
|
||||||
import android.net.wifi.p2p.WifiP2pGroup
|
|
||||||
import android.net.wifi.p2p.WifiP2pInfo
|
|
||||||
import android.net.wifi.p2p.WifiP2pManager
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.core.content.edit
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper
|
||||||
@@ -21,10 +20,8 @@ import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.netId
|
|||||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupInfo
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupInfo
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps
|
||||||
import be.mygod.vpnhotspot.util.StickyEvent0
|
import be.mygod.vpnhotspot.net.wifi.configuration.channelToFrequency
|
||||||
import be.mygod.vpnhotspot.util.StickyEvent1
|
import be.mygod.vpnhotspot.util.*
|
||||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
|
||||||
import be.mygod.vpnhotspot.util.intentFilter
|
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
@@ -35,7 +32,14 @@ import java.lang.reflect.InvocationTargetException
|
|||||||
class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPreferences.OnSharedPreferenceChangeListener {
|
class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "RepeaterService"
|
private const val TAG = "RepeaterService"
|
||||||
const val KEY_OPERATING_CHANNEL = "service.repeater.oc"
|
private const val KEY_NETWORK_NAME = "service.repeater.networkName"
|
||||||
|
private const val KEY_PASSPHRASE = "service.repeater.passphrase"
|
||||||
|
private const val KEY_OPERATING_BAND = "service.repeater.band"
|
||||||
|
private const val KEY_OPERATING_CHANNEL = "service.repeater.oc"
|
||||||
|
/**
|
||||||
|
* Placeholder for bypassing networkName check.
|
||||||
|
*/
|
||||||
|
private const val PLACEHOLDER_NETWORK_NAME = "DIRECT-00-VPNHotspot"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is only a "ServiceConnection" to system service and its impact on system is minimal.
|
* This is only a "ServiceConnection" to system service and its impact on system is minimal.
|
||||||
@@ -49,14 +53,24 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val supported get() = p2pManager != null
|
val supported get() = p2pManager != null
|
||||||
|
@Deprecated("Not initialized and no use at all since API 29")
|
||||||
var persistentSupported = false
|
var persistentSupported = false
|
||||||
|
|
||||||
|
var networkName: String?
|
||||||
|
get() = app.pref.getString(KEY_NETWORK_NAME, null)
|
||||||
|
set(value) = app.pref.edit { putString(KEY_NETWORK_NAME, value) }
|
||||||
|
var passphrase: String?
|
||||||
|
get() = app.pref.getString(KEY_PASSPHRASE, null)
|
||||||
|
set(value) = app.pref.edit { putString(KEY_PASSPHRASE, value) }
|
||||||
|
var operatingBand: Int
|
||||||
|
@SuppressLint("InlinedApi") get() = app.pref.getInt(KEY_OPERATING_BAND, WifiP2pConfig.GROUP_OWNER_BAND_AUTO)
|
||||||
|
set(value) = app.pref.edit { putInt(KEY_OPERATING_BAND, value) }
|
||||||
var operatingChannel: Int
|
var operatingChannel: Int
|
||||||
get() {
|
get() {
|
||||||
val result = app.pref.getString(KEY_OPERATING_CHANNEL, null)?.toIntOrNull() ?: 0
|
val result = app.pref.getString(KEY_OPERATING_CHANNEL, null)?.toIntOrNull() ?: 0
|
||||||
return if (result in 1..165) result else 0
|
return if (result in 1..165) result else 0
|
||||||
}
|
}
|
||||||
set(value) = app.pref.edit().putString(KEY_OPERATING_CHANNEL, value.toString()).apply()
|
set(value) = app.pref.edit { putString(KEY_OPERATING_CHANNEL, value.toString()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Status {
|
enum class Status {
|
||||||
@@ -73,16 +87,16 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
|||||||
groupChanged(value)
|
groupChanged(value)
|
||||||
}
|
}
|
||||||
val groupChanged = StickyEvent1 { group }
|
val groupChanged = StickyEvent1 { group }
|
||||||
|
@Deprecated("Not initialized and no use at all since API 29")
|
||||||
var thisDevice: WifiP2pDevice? = null
|
var thisDevice: WifiP2pDevice? = null
|
||||||
|
|
||||||
@Deprecated("WPS was deprecated RIP")
|
|
||||||
fun startWps(pin: String? = null) {
|
fun startWps(pin: String? = null) {
|
||||||
val channel = channel
|
val channel = channel
|
||||||
if (channel == null) SmartSnackbar.make(R.string.repeater_failure_disconnected).show()
|
if (channel == null) SmartSnackbar.make(R.string.repeater_failure_disconnected).show()
|
||||||
else @Suppress("DEPRECATION") if (active) p2pManager.startWps(channel, android.net.wifi.WpsInfo().apply {
|
else if (active) p2pManager.startWps(channel, WpsInfo().apply {
|
||||||
setup = if (pin == null) android.net.wifi.WpsInfo.PBC else {
|
setup = if (pin == null) WpsInfo.PBC else {
|
||||||
this.pin = pin
|
this.pin = pin
|
||||||
android.net.wifi.WpsInfo.KEYPAD
|
WpsInfo.KEYPAD
|
||||||
}
|
}
|
||||||
}, object : WifiP2pManager.ActionListener {
|
}, object : WifiP2pManager.ActionListener {
|
||||||
override fun onSuccess() = SmartSnackbar.make(
|
override fun onSuccess() = SmartSnackbar.make(
|
||||||
@@ -109,11 +123,12 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
|||||||
if (intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, 0) ==
|
if (intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, 0) ==
|
||||||
WifiP2pManager.WIFI_P2P_STATE_DISABLED) clean() // ignore P2P enabled
|
WifiP2pManager.WIFI_P2P_STATE_DISABLED) clean() // ignore P2P enabled
|
||||||
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> onP2pConnectionChanged(
|
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> onP2pConnectionChanged(
|
||||||
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO),
|
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO)!!,
|
||||||
intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO),
|
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP)!!)
|
||||||
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@Deprecated("No longer used since API 29")
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
private val deviceListener = broadcastReceiver { _, intent ->
|
private val deviceListener = broadcastReceiver { _, intent ->
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION -> binder.thisDevice =
|
WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION -> binder.thisDevice =
|
||||||
@@ -122,6 +137,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
private var routingManager: RoutingManager? = null
|
private var routingManager: RoutingManager? = null
|
||||||
|
private var persistNextGroup = false
|
||||||
|
|
||||||
var status = Status.IDLE
|
var status = Status.IDLE
|
||||||
private set(value) {
|
private set(value) {
|
||||||
@@ -144,13 +160,17 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
onChannelDisconnected()
|
onChannelDisconnected()
|
||||||
|
if (Build.VERSION.SDK_INT < 29) @Suppress("DEPRECATION") {
|
||||||
registerReceiver(deviceListener, intentFilter(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION,
|
registerReceiver(deviceListener, intentFilter(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION,
|
||||||
WifiP2pManagerHelper.WIFI_P2P_PERSISTENT_GROUPS_CHANGED_ACTION))
|
WifiP2pManagerHelper.WIFI_P2P_PERSISTENT_GROUPS_CHANGED_ACTION))
|
||||||
app.pref.registerOnSharedPreferenceChangeListener(this)
|
app.pref.registerOnSharedPreferenceChangeListener(this)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent) = binder
|
override fun onBind(intent: Intent) = binder
|
||||||
|
|
||||||
|
@Deprecated("No longer used since API 29")
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
private fun setOperatingChannel(oc: Int = operatingChannel) = try {
|
private fun setOperatingChannel(oc: Int = operatingChannel) = try {
|
||||||
val channel = channel
|
val channel = channel
|
||||||
if (channel == null) SmartSnackbar.make(R.string.repeater_failure_disconnected).show()
|
if (channel == null) SmartSnackbar.make(R.string.repeater_failure_disconnected).show()
|
||||||
@@ -173,17 +193,21 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
|||||||
channel = null
|
channel = null
|
||||||
if (status != Status.DESTROYED) try {
|
if (status != Status.DESTROYED) try {
|
||||||
channel = p2pManager.initialize(this, Looper.getMainLooper(), this)
|
channel = p2pManager.initialize(this, Looper.getMainLooper(), this)
|
||||||
setOperatingChannel()
|
if (Build.VERSION.SDK_INT < 29) @Suppress("DEPRECATION") setOperatingChannel()
|
||||||
} catch (e: RuntimeException) {
|
} catch (e: RuntimeException) {
|
||||||
Timber.w(e)
|
Timber.w(e)
|
||||||
handler.postDelayed(this::onChannelDisconnected, 1000)
|
handler.postDelayed(this::onChannelDisconnected, 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("No longer used since API 29")
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||||
if (key == KEY_OPERATING_CHANNEL) setOperatingChannel()
|
if (key == KEY_OPERATING_CHANNEL) setOperatingChannel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("No longer used since API 29")
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
private fun onPersistentGroupsChanged() {
|
private fun onPersistentGroupsChanged() {
|
||||||
val channel = channel ?: return
|
val channel = channel ?: return
|
||||||
val device = binder.thisDevice ?: return
|
val device = binder.thisDevice ?: return
|
||||||
@@ -222,6 +246,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
|||||||
registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
|
registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
|
||||||
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION))
|
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION))
|
||||||
receiverRegistered = true
|
receiverRegistered = true
|
||||||
|
try {
|
||||||
p2pManager.requestGroupInfo(channel) {
|
p2pManager.requestGroupInfo(channel) {
|
||||||
when {
|
when {
|
||||||
it == null -> doStart()
|
it == null -> doStart()
|
||||||
@@ -236,20 +261,71 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Timber.w(e)
|
||||||
|
startFailure(e.readableMessage)
|
||||||
|
}
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* startService Step 2 (if a group isn't already available)
|
* startService Step 2 (if a group isn't already available)
|
||||||
*/
|
*/
|
||||||
private fun doStart() = p2pManager.createGroup(channel, object : WifiP2pManager.ActionListener {
|
private fun doStart() {
|
||||||
override fun onFailure(reason: Int) = startFailure(formatReason(R.string.repeater_create_group_failure, reason))
|
val listener = object : WifiP2pManager.ActionListener {
|
||||||
|
override fun onFailure(reason: Int) {
|
||||||
|
startFailure(formatReason(R.string.repeater_create_group_failure, reason))
|
||||||
|
}
|
||||||
override fun onSuccess() { } // wait for WIFI_P2P_CONNECTION_CHANGED_ACTION to fire to go to step 3
|
override fun onSuccess() { } // wait for WIFI_P2P_CONNECTION_CHANGED_ACTION to fire to go to step 3
|
||||||
})
|
}
|
||||||
|
val channel = channel ?: return listener.onFailure(WifiP2pManager.BUSY)
|
||||||
|
val networkName = networkName
|
||||||
|
val passphrase = passphrase
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT < 29 || networkName == null || passphrase == null) {
|
||||||
|
persistNextGroup = true
|
||||||
|
p2pManager.createGroup(channel, listener)
|
||||||
|
} else p2pManager.createGroup(channel, WifiP2pConfig.Builder().apply {
|
||||||
|
setNetworkName(PLACEHOLDER_NETWORK_NAME)
|
||||||
|
setPassphrase(passphrase)
|
||||||
|
operatingChannel.let { oc ->
|
||||||
|
if (oc == 0) setGroupOperatingBand(operatingBand)
|
||||||
|
else setGroupOperatingFrequency(channelToFrequency(oc))
|
||||||
|
}
|
||||||
|
}.build().run {
|
||||||
|
useParcel { p ->
|
||||||
|
p.writeParcelable(this, 0)
|
||||||
|
val end = p.dataPosition()
|
||||||
|
p.setDataPosition(0)
|
||||||
|
val creator = p.readString()
|
||||||
|
val deviceAddress = p.readString()
|
||||||
|
val wps = p.readParcelable<WpsInfo>(javaClass.classLoader)
|
||||||
|
val long = p.readLong()
|
||||||
|
check(p.readString() == PLACEHOLDER_NETWORK_NAME)
|
||||||
|
check(p.readString() == passphrase)
|
||||||
|
val int = p.readInt()
|
||||||
|
check(p.dataPosition() == end)
|
||||||
|
p.setDataPosition(0)
|
||||||
|
p.writeString(creator)
|
||||||
|
p.writeString(deviceAddress)
|
||||||
|
p.writeParcelable(wps, 0)
|
||||||
|
p.writeLong(long)
|
||||||
|
p.writeString(networkName)
|
||||||
|
p.writeString(passphrase)
|
||||||
|
p.writeInt(int)
|
||||||
|
p.setDataPosition(0)
|
||||||
|
p.readParcelable<WifiP2pConfig>(javaClass.classLoader)
|
||||||
|
}
|
||||||
|
}, listener)
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Timber.w(e)
|
||||||
|
startFailure(e.readableMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Used during step 2, also called when connection changed
|
* Used during step 2, also called when connection changed
|
||||||
*/
|
*/
|
||||||
private fun onP2pConnectionChanged(info: WifiP2pInfo, net: NetworkInfo?, group: WifiP2pGroup) {
|
private fun onP2pConnectionChanged(info: WifiP2pInfo, group: WifiP2pGroup) {
|
||||||
DebugHelper.log(TAG, "P2P connection changed: $info\n$net\n$group")
|
DebugHelper.log(TAG, "P2P connection changed: $info\n$group")
|
||||||
when {
|
when {
|
||||||
!info.groupFormed || !info.isGroupOwner || !group.isGroupOwner -> {
|
!info.groupFormed || !info.isGroupOwner || !group.isGroupOwner -> {
|
||||||
if (routingManager != null) clean() // P2P shutdown, else other groups changing before start, ignore
|
if (routingManager != null) clean() // P2P shutdown, else other groups changing before start, ignore
|
||||||
@@ -266,6 +342,11 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
|||||||
*/
|
*/
|
||||||
private fun doStart(group: WifiP2pGroup) {
|
private fun doStart(group: WifiP2pGroup) {
|
||||||
binder.group = group
|
binder.group = group
|
||||||
|
if (persistNextGroup) {
|
||||||
|
networkName = group.networkName
|
||||||
|
passphrase = group.passphrase
|
||||||
|
persistNextGroup = false
|
||||||
|
}
|
||||||
check(routingManager == null)
|
check(routingManager == null)
|
||||||
routingManager = RoutingManager.LocalOnly(this, group.`interface`!!).apply { start() }
|
routingManager = RoutingManager.LocalOnly(this, group.`interface`!!).apply { start() }
|
||||||
status = Status.ACTIVE
|
status = Status.ACTIVE
|
||||||
@@ -310,8 +391,10 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
|||||||
handler.removeCallbacksAndMessages(null)
|
handler.removeCallbacksAndMessages(null)
|
||||||
if (status != Status.IDLE) binder.shutdown()
|
if (status != Status.IDLE) binder.shutdown()
|
||||||
clean() // force clean to prevent leakage
|
clean() // force clean to prevent leakage
|
||||||
|
if (Build.VERSION.SDK_INT < 29) @Suppress("DEPRECATION") {
|
||||||
app.pref.unregisterOnSharedPreferenceChangeListener(this)
|
app.pref.unregisterOnSharedPreferenceChangeListener(this)
|
||||||
unregisterReceiver(deviceListener)
|
unregisterReceiver(deviceListener)
|
||||||
|
}
|
||||||
status = Status.DESTROYED
|
status = Status.DESTROYED
|
||||||
if (Build.VERSION.SDK_INT >= 27) channel?.close()
|
if (Build.VERSION.SDK_INT >= 27) channel?.close()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import be.mygod.vpnhotspot.App.Companion.app
|
|||||||
import be.mygod.vpnhotspot.net.Routing.Companion.IPTABLES
|
import be.mygod.vpnhotspot.net.Routing.Companion.IPTABLES
|
||||||
import be.mygod.vpnhotspot.net.monitor.IpMonitor
|
import be.mygod.vpnhotspot.net.monitor.IpMonitor
|
||||||
import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
|
import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
|
||||||
|
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
|
||||||
import be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat
|
import be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat
|
||||||
import be.mygod.vpnhotspot.preference.SharedPreferenceDataStore
|
import be.mygod.vpnhotspot.preference.SharedPreferenceDataStore
|
||||||
import be.mygod.vpnhotspot.util.RootSession
|
import be.mygod.vpnhotspot.util.RootSession
|
||||||
@@ -26,6 +27,7 @@ import java.net.SocketException
|
|||||||
|
|
||||||
class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
WifiDoubleLock.mode = WifiDoubleLock.mode // handle complicated default value and possible system upgrades
|
||||||
preferenceManager.preferenceDataStore = SharedPreferenceDataStore(app.pref)
|
preferenceManager.preferenceDataStore = SharedPreferenceDataStore(app.pref)
|
||||||
RoutingManager.masqueradeMode = RoutingManager.masqueradeMode // flush default value
|
RoutingManager.masqueradeMode = RoutingManager.masqueradeMode // flush default value
|
||||||
addPreferencesFromResource(R.xml.pref_settings)
|
addPreferencesFromResource(R.xml.pref_settings)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class TetheringService : IpNeighbourMonitoringService() {
|
|||||||
private val receiver = broadcastReceiver { _, intent ->
|
private val receiver = broadcastReceiver { _, intent ->
|
||||||
synchronized(downstreams) {
|
synchronized(downstreams) {
|
||||||
val toRemove = downstreams.toMutableMap() // make a copy
|
val toRemove = downstreams.toMutableMap() // make a copy
|
||||||
for (iface in intent.tetheredIfaces) {
|
for (iface in intent.tetheredIfaces ?: return@synchronized) {
|
||||||
val downstream = toRemove.remove(iface) ?: continue
|
val downstream = toRemove.remove(iface) ?: continue
|
||||||
if (downstream.monitor) downstream.start()
|
if (downstream.monitor) downstream.start()
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@ class TetheringService : IpNeighbourMonitoringService() {
|
|||||||
if (start()) check(downstreams.put(iface, this) == null) else destroy()
|
if (start()) check(downstreams.put(iface, this) == null) else destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
intent.getStringExtra(EXTRA_ADD_INTERFACE_MONITOR)?.let { iface ->
|
intent.getStringExtra(EXTRA_ADD_INTERFACE_MONITOR)?.also { iface ->
|
||||||
val downstream = downstreams[iface]
|
val downstream = downstreams[iface]
|
||||||
if (downstream == null) Downstream(this, iface, true).apply {
|
if (downstream == null) Downstream(this, iface, true).apply {
|
||||||
start()
|
start()
|
||||||
@@ -98,7 +98,7 @@ class TetheringService : IpNeighbourMonitoringService() {
|
|||||||
downstreams[iface] = this
|
downstreams[iface] = this
|
||||||
} else downstream.monitor = true
|
} else downstream.monitor = true
|
||||||
}
|
}
|
||||||
downstreams.remove(intent.getStringExtra(EXTRA_REMOVE_INTERFACE))?.destroy()
|
intent.getStringExtra(EXTRA_REMOVE_INTERFACE)?.also { downstreams.remove(it)?.destroy() }
|
||||||
updateNotification() // call this first just in case we are shutting down immediately
|
updateNotification() // call this first just in case we are shutting down immediately
|
||||||
onDownstreamsChangedLocked()
|
onDownstreamsChangedLocked()
|
||||||
} else if (downstreams.isEmpty()) stopSelf(startId)
|
} else if (downstreams.isEmpty()) stopSelf(startId)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package be.mygod.vpnhotspot.client
|
|||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import android.text.style.StrikethroughSpan
|
import android.text.style.StrikethroughSpan
|
||||||
import androidx.lifecycle.Transformations
|
import androidx.lifecycle.map
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.R
|
import be.mygod.vpnhotspot.R
|
||||||
@@ -27,7 +27,7 @@ open class Client(val mac: Long, val iface: String) {
|
|||||||
|
|
||||||
val ip = TreeMap<InetAddress, IpNeighbour.State>(InetAddressComparator)
|
val ip = TreeMap<InetAddress, IpNeighbour.State>(InetAddressComparator)
|
||||||
val macString by lazy { mac.macToString() }
|
val macString by lazy { mac.macToString() }
|
||||||
private val record = AppDatabase.instance.clientRecordDao.lookupSync(mac)
|
private val record = AppDatabase.instance.clientRecordDao.lookupOrDefaultSync(mac)
|
||||||
private val macIface get() = SpannableStringBuilder(makeMacSpan(macString)).apply {
|
private val macIface get() = SpannableStringBuilder(makeMacSpan(macString)).apply {
|
||||||
append('%')
|
append('%')
|
||||||
append(iface)
|
append(iface)
|
||||||
@@ -37,24 +37,22 @@ open class Client(val mac: Long, val iface: String) {
|
|||||||
val blocked get() = record.value?.blocked == true
|
val blocked get() = record.value?.blocked == true
|
||||||
|
|
||||||
open val icon get() = TetherType.ofInterface(iface).icon
|
open val icon get() = TetherType.ofInterface(iface).icon
|
||||||
val title = Transformations.map(record) { record ->
|
val title = record.map { record ->
|
||||||
/**
|
/**
|
||||||
* we hijack the get title process to check if we need to perform MacLookup,
|
* we hijack the get title process to check if we need to perform MacLookup,
|
||||||
* as record might not be initialized in other more appropriate places
|
* as record might not be initialized in other more appropriate places
|
||||||
*/
|
*/
|
||||||
SpannableStringBuilder(if (record?.nickname.isNullOrEmpty()) {
|
SpannableStringBuilder(if (record.nickname.isEmpty()) {
|
||||||
if (record?.macLookupPending != false) MacLookup.perform(mac)
|
if (record.macLookupPending) MacLookup.perform(mac)
|
||||||
macIface
|
macIface
|
||||||
} else emojize(record?.nickname)).apply {
|
} else emojize(record.nickname)).apply {
|
||||||
if (record?.blocked == true) {
|
if (record.blocked) setSpan(StrikethroughSpan(), 0, length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
|
||||||
setSpan(StrikethroughSpan(), 0, length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
val titleSelectable = record.map { it.nickname.isEmpty() }
|
||||||
val titleSelectable = Transformations.map(record) { it?.nickname.isNullOrEmpty() }
|
val description = record.map { record ->
|
||||||
val description = Transformations.map(record) { record ->
|
|
||||||
SpannableStringBuilder().apply {
|
SpannableStringBuilder().apply {
|
||||||
if (!record?.nickname.isNullOrEmpty()) appendln(macIface)
|
if (record.nickname.isNotEmpty()) appendln(macIface)
|
||||||
ip.entries.forEach { (ip, state) ->
|
ip.entries.forEach { (ip, state) ->
|
||||||
append(makeIpSpan(ip))
|
append(makeIpSpan(ip))
|
||||||
appendln(app.getText(when (state) {
|
appendln(app.getText(when (state) {
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ import be.mygod.vpnhotspot.util.broadcastReceiver
|
|||||||
class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callback {
|
class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callback {
|
||||||
private var tetheredInterfaces = emptySet<String>()
|
private var tetheredInterfaces = emptySet<String>()
|
||||||
private val receiver = broadcastReceiver { _, intent ->
|
private val receiver = broadcastReceiver { _, intent ->
|
||||||
tetheredInterfaces = intent.tetheredIfaces.toSet() + intent.localOnlyTetheredIfaces
|
tetheredInterfaces = (intent.tetheredIfaces ?: return@broadcastReceiver).toSet() +
|
||||||
|
(intent.localOnlyTetheredIfaces ?: return@broadcastReceiver)
|
||||||
populateClients()
|
populateClients()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ import androidx.appcompat.widget.PopupMenu
|
|||||||
import androidx.databinding.BaseObservable
|
import androidx.databinding.BaseObservable
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
import androidx.lifecycle.get
|
import androidx.lifecycle.get
|
||||||
|
import androidx.lifecycle.observe
|
||||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
@@ -219,8 +219,9 @@ class ClientsFragment : Fragment() {
|
|||||||
binding.swipeRefresher.setOnRefreshListener {
|
binding.swipeRefresher.setOnRefreshListener {
|
||||||
IpNeighbourMonitor.instance?.flush()
|
IpNeighbourMonitor.instance?.flush()
|
||||||
}
|
}
|
||||||
ViewModelProviders.of(requireActivity()).get<ClientViewModel>().clients.observe(this,
|
ViewModelProviders.of(requireActivity()).get<ClientViewModel>().clients.observe(this) {
|
||||||
Observer { adapter.submitList(it.toMutableList()) })
|
adapter.submitList(it.toMutableList())
|
||||||
|
}
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ object MacLookup {
|
|||||||
try {
|
try {
|
||||||
val response = conn.inputStream.bufferedReader().readText()
|
val response = conn.inputStream.bufferedReader().readText()
|
||||||
val obj = JSONObject(response).getJSONObject("result")
|
val obj = JSONObject(response).getJSONObject("result")
|
||||||
obj.optString("error", null)?.also { throw UnexpectedError(mac, it) }
|
obj.opt("error")?.also { throw UnexpectedError(mac, it.toString()) }
|
||||||
val company = obj.getString("company")
|
val company = obj.getString("company")
|
||||||
val match = extractCountry(mac, response, obj)
|
val match = extractCountry(mac, response, obj)
|
||||||
val result = if (match != null) {
|
val result = if (match != null) {
|
||||||
@@ -71,9 +71,9 @@ object MacLookup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun extractCountry(mac: Long, response: String, obj: JSONObject): MatchResult? {
|
private fun extractCountry(mac: Long, response: String, obj: JSONObject): MatchResult? {
|
||||||
obj.optString("country")?.let { countryCodeRegex.matchEntire(it) }?.also { return it }
|
countryCodeRegex.matchEntire(obj.optString("country"))?.also { return it }
|
||||||
val address = obj.optString("address")
|
val address = obj.optString("address")
|
||||||
if (address.isNullOrBlank()) return null
|
if (address.isBlank()) return null
|
||||||
countryCodeRegex.find(address)?.also { return it }
|
countryCodeRegex.find(address)?.also { return it }
|
||||||
Timber.w(UnexpectedError(mac, response))
|
Timber.w(UnexpectedError(mac, response))
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ import java.net.NetworkInterface
|
|||||||
|
|
||||||
@TargetApi(26)
|
@TargetApi(26)
|
||||||
class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager(), ServiceConnection {
|
class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager(), ServiceConnection {
|
||||||
|
companion object {
|
||||||
|
val permission = if (Build.VERSION.SDK_INT >= 29)
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION else Manifest.permission.ACCESS_COARSE_LOCATION
|
||||||
|
}
|
||||||
|
|
||||||
class ViewHolder(val binding: ListitemInterfaceBinding) : RecyclerView.ViewHolder(binding.root),
|
class ViewHolder(val binding: ListitemInterfaceBinding) : RecyclerView.ViewHolder(binding.root),
|
||||||
View.OnClickListener {
|
View.OnClickListener {
|
||||||
init {
|
init {
|
||||||
@@ -39,10 +44,8 @@ class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager()
|
|||||||
val binder = manager.binder
|
val binder = manager.binder
|
||||||
if (binder?.iface != null) binder.stop() else {
|
if (binder?.iface != null) binder.stop() else {
|
||||||
val context = manager.parent.requireContext()
|
val context = manager.parent.requireContext()
|
||||||
if (context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) !=
|
if (context.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
|
||||||
PackageManager.PERMISSION_GRANTED) {
|
manager.parent.requestPermissions(arrayOf(permission), TetheringFragment.START_LOCAL_ONLY_HOTSPOT)
|
||||||
manager.parent.requestPermissions(arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
|
|
||||||
TetheringFragment.START_LOCAL_ONLY_HOTSPOT)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package be.mygod.vpnhotspot.manage
|
package be.mygod.vpnhotspot.manage
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.net.wifi.WifiConfiguration
|
import android.net.wifi.WifiConfiguration
|
||||||
|
import android.net.wifi.p2p.WifiP2pConfig
|
||||||
import android.net.wifi.p2p.WifiP2pGroup
|
import android.net.wifi.p2p.WifiP2pGroup
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -50,6 +53,12 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
|||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val title: CharSequence @Bindable get() {
|
||||||
|
if (Build.VERSION.SDK_INT >= 29) binder?.group?.frequency?.let {
|
||||||
|
return parent.getString(R.string.repeater_channel, it, frequencyToChannel(it))
|
||||||
|
}
|
||||||
|
return parent.getString(R.string.title_repeater)
|
||||||
|
}
|
||||||
val addresses: CharSequence @Bindable get() {
|
val addresses: CharSequence @Bindable get() {
|
||||||
return try {
|
return try {
|
||||||
NetworkInterface.getByName(p2pInterface ?: return "")?.formatAddresses() ?: ""
|
NetworkInterface.getByName(p2pInterface ?: return "")?.formatAddresses() ?: ""
|
||||||
@@ -65,6 +74,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
|||||||
}
|
}
|
||||||
fun onGroupChanged(group: WifiP2pGroup? = null) {
|
fun onGroupChanged(group: WifiP2pGroup? = null) {
|
||||||
p2pInterface = group?.`interface`
|
p2pInterface = group?.`interface`
|
||||||
|
if (Build.VERSION.SDK_INT >= 29) notifyPropertyChanged(BR.title)
|
||||||
notifyPropertyChanged(BR.addresses)
|
notifyPropertyChanged(BR.addresses)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +83,12 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
|||||||
when (binder?.service?.status) {
|
when (binder?.service?.status) {
|
||||||
RepeaterService.Status.IDLE -> {
|
RepeaterService.Status.IDLE -> {
|
||||||
val context = parent.requireContext()
|
val context = parent.requireContext()
|
||||||
|
if (Build.VERSION.SDK_INT >= 29 && context.checkSelfPermission(
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
parent.requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
|
||||||
|
TetheringFragment.START_REPEATER)
|
||||||
|
return
|
||||||
|
}
|
||||||
ContextCompat.startForegroundService(context, Intent(context, RepeaterService::class.java))
|
ContextCompat.startForegroundService(context, Intent(context, RepeaterService::class.java))
|
||||||
}
|
}
|
||||||
RepeaterService.Status.ACTIVE -> binder.shutdown()
|
RepeaterService.Status.ACTIVE -> binder.shutdown()
|
||||||
@@ -103,6 +119,8 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("No longer used since API 29")
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
class ConfigHolder : ViewModel() {
|
class ConfigHolder : ViewModel() {
|
||||||
var config: P2pSupplicantConfiguration? = null
|
var config: P2pSupplicantConfiguration? = null
|
||||||
}
|
}
|
||||||
@@ -115,6 +133,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
|||||||
private val data = Data()
|
private val data = Data()
|
||||||
internal var binder: RepeaterService.Binder? = null
|
internal var binder: RepeaterService.Binder? = null
|
||||||
private var p2pInterface: String? = null
|
private var p2pInterface: String? = null
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
private val holder = ViewModelProviders.of(parent).get<ConfigHolder>()
|
private val holder = ViewModelProviders.of(parent).get<ConfigHolder>()
|
||||||
|
|
||||||
override fun bindTo(viewHolder: RecyclerView.ViewHolder) {
|
override fun bindTo(viewHolder: RecyclerView.ViewHolder) {
|
||||||
@@ -144,6 +163,22 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
|||||||
}
|
}
|
||||||
|
|
||||||
val configuration: WifiConfiguration? get() {
|
val configuration: WifiConfiguration? get() {
|
||||||
|
if (Build.VERSION.SDK_INT >= 29) {
|
||||||
|
val networkName = RepeaterService.networkName
|
||||||
|
val passphrase = RepeaterService.passphrase
|
||||||
|
if (networkName != null && passphrase != null) {
|
||||||
|
return newWifiApConfiguration(networkName, passphrase).apply {
|
||||||
|
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK) // is not actually used
|
||||||
|
apBand = when (RepeaterService.operatingBand) {
|
||||||
|
WifiP2pConfig.GROUP_OWNER_BAND_AUTO -> AP_BAND_ANY
|
||||||
|
WifiP2pConfig.GROUP_OWNER_BAND_2GHZ -> AP_BAND_2GHZ
|
||||||
|
WifiP2pConfig.GROUP_OWNER_BAND_5GHZ -> AP_BAND_5GHZ
|
||||||
|
else -> throw IllegalArgumentException("Unknown operatingBand")
|
||||||
|
}
|
||||||
|
apChannel = RepeaterService.operatingChannel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else @Suppress("DEPRECATION") {
|
||||||
val group = binder?.group
|
val group = binder?.group
|
||||||
if (group != null) try {
|
if (group != null) try {
|
||||||
val config = P2pSupplicantConfiguration(group, binder?.thisDevice?.deviceAddress)
|
val config = P2pSupplicantConfiguration(group, binder?.thisDevice?.deviceAddress)
|
||||||
@@ -158,11 +193,21 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
|||||||
} catch (e: RuntimeException) {
|
} catch (e: RuntimeException) {
|
||||||
Timber.w(e)
|
Timber.w(e)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
SmartSnackbar.make(R.string.repeater_configure_failure).show()
|
SmartSnackbar.make(R.string.repeater_configure_failure).show()
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
fun updateConfiguration(config: WifiConfiguration) {
|
fun updateConfiguration(config: WifiConfiguration) {
|
||||||
holder.config?.let { master ->
|
if (Build.VERSION.SDK_INT >= 29) {
|
||||||
|
RepeaterService.networkName = config.SSID
|
||||||
|
RepeaterService.passphrase = config.preSharedKey
|
||||||
|
RepeaterService.operatingBand = when (config.apBand) {
|
||||||
|
AP_BAND_ANY -> WifiP2pConfig.GROUP_OWNER_BAND_AUTO
|
||||||
|
AP_BAND_2GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_2GHZ
|
||||||
|
AP_BAND_5GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_5GHZ
|
||||||
|
else -> throw IllegalArgumentException("Unknown apBand")
|
||||||
|
}
|
||||||
|
} else @Suppress("DEPRECATION") holder.config?.let { master ->
|
||||||
if (binder?.group?.networkName != config.SSID || master.psk != config.preSharedKey) try {
|
if (binder?.group?.networkName != config.SSID || master.psk != config.preSharedKey) try {
|
||||||
master.update(config.SSID, config.preSharedKey)
|
master.update(config.SSID, config.preSharedKey)
|
||||||
binder!!.group = null
|
binder!!.group = null
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ abstract class TetherListeningTileService : KillableTileService() {
|
|||||||
protected var tethered: List<String>? = null
|
protected var tethered: List<String>? = null
|
||||||
|
|
||||||
private val receiver = broadcastReceiver { _, intent ->
|
private val receiver = broadcastReceiver { _, intent ->
|
||||||
tethered = intent.tetheredIfaces
|
tethered = intent.tetheredIfaces ?: return@broadcastReceiver
|
||||||
updateTile()
|
updateTile()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import java.net.SocketException
|
|||||||
|
|
||||||
class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickListener {
|
class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickListener {
|
||||||
companion object {
|
companion object {
|
||||||
|
const val START_REPEATER = 4
|
||||||
const val START_LOCAL_ONLY_HOTSPOT = 1
|
const val START_LOCAL_ONLY_HOTSPOT = 1
|
||||||
const val REPEATER_WPS = 3
|
const val REPEATER_WPS = 3
|
||||||
const val CONFIGURE_REPEATER = 2
|
const val CONFIGURE_REPEATER = 2
|
||||||
@@ -98,8 +99,9 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
|||||||
var binder: TetheringService.Binder? = null
|
var binder: TetheringService.Binder? = null
|
||||||
private val adapter = ManagerAdapter()
|
private val adapter = ManagerAdapter()
|
||||||
private val receiver = broadcastReceiver { _, intent ->
|
private val receiver = broadcastReceiver { _, intent ->
|
||||||
adapter.update(intent.tetheredIfaces, intent.localOnlyTetheredIfaces,
|
adapter.update(intent.tetheredIfaces ?: return@broadcastReceiver,
|
||||||
intent.getStringArrayListExtra(TetheringManager.EXTRA_ERRORED_TETHER))
|
intent.localOnlyTetheredIfaces ?: return@broadcastReceiver,
|
||||||
|
intent.getStringArrayListExtra(TetheringManager.EXTRA_ERRORED_TETHER) ?: return@broadcastReceiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMonitorList(canMonitor: List<String> = emptyList()) {
|
private fun updateMonitorList(canMonitor: List<String> = emptyList()) {
|
||||||
@@ -189,18 +191,27 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
|||||||
WifiApManager.configuration = configuration
|
WifiApManager.configuration = configuration
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
SmartSnackbar.make(R.string.configuration_rejected).show()
|
SmartSnackbar.make(R.string.configuration_rejected).show()
|
||||||
|
} catch (e: InvocationTargetException) {
|
||||||
|
SmartSnackbar.make(e.targetException).show()
|
||||||
}
|
}
|
||||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
if (requestCode == START_LOCAL_ONLY_HOTSPOT) @TargetApi(26) {
|
when (requestCode) {
|
||||||
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
|
START_REPEATER -> if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) @TargetApi(29) {
|
||||||
|
val context = requireContext()
|
||||||
|
context.startForegroundService(Intent(context, RepeaterService::class.java))
|
||||||
|
}
|
||||||
|
START_LOCAL_ONLY_HOTSPOT -> {
|
||||||
|
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) @TargetApi(26) {
|
||||||
val context = requireContext()
|
val context = requireContext()
|
||||||
context.startForegroundService(Intent(context, LocalOnlyHotspotService::class.java))
|
context.startForegroundService(Intent(context, LocalOnlyHotspotService::class.java))
|
||||||
}
|
}
|
||||||
} else super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
}
|
||||||
|
else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
package be.mygod.vpnhotspot.net
|
package be.mygod.vpnhotspot.net
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import android.system.ErrnoException
|
import android.system.ErrnoException
|
||||||
import android.system.OsConstants
|
import android.system.OsConstants
|
||||||
import androidx.core.os.BuildCompat
|
|
||||||
import be.mygod.vpnhotspot.room.macToLong
|
import be.mygod.vpnhotspot.room.macToLong
|
||||||
import be.mygod.vpnhotspot.util.parseNumericAddress
|
import be.mygod.vpnhotspot.util.parseNumericAddress
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.lang.NumberFormatException
|
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.NetworkInterface
|
import java.net.NetworkInterface
|
||||||
import java.net.SocketException
|
import java.net.SocketException
|
||||||
@@ -98,7 +97,7 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: Long, v
|
|||||||
.filter { it.size >= 6 && mac.matcher(it[ARP_HW_ADDRESS]).matches() }
|
.filter { it.size >= 6 && mac.matcher(it[ARP_HW_ADDRESS]).matches() }
|
||||||
.toList()
|
.toList()
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
if (e !is FileNotFoundException || !BuildCompat.isAtLeastQ() ||
|
if (e !is FileNotFoundException || Build.VERSION.SDK_INT < 29 ||
|
||||||
(e.cause as? ErrnoException)?.errno != OsConstants.EACCES) Timber.w(e)
|
(e.cause as? ErrnoException)?.errno != OsConstants.EACCES) Timber.w(e)
|
||||||
}
|
}
|
||||||
return arpCache
|
return arpCache
|
||||||
|
|||||||
@@ -200,9 +200,9 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
|
|||||||
override fun onIpNeighbourAvailable(neighbours: List<IpNeighbour>) = synchronized(this) {
|
override fun onIpNeighbourAvailable(neighbours: List<IpNeighbour>) = synchronized(this) {
|
||||||
val toRemove = HashSet(clients.keys)
|
val toRemove = HashSet(clients.keys)
|
||||||
for (neighbour in neighbours) {
|
for (neighbour in neighbours) {
|
||||||
if (neighbour.dev != downstream || neighbour.ip !is Inet4Address ||
|
if (neighbour.dev != downstream || neighbour.ip !is Inet4Address || runBlocking {
|
||||||
runBlocking { AppDatabase.instance.clientRecordDao.lookup(neighbour.lladdr) }
|
AppDatabase.instance.clientRecordDao.lookupOrDefault(neighbour.lladdr)
|
||||||
?.blocked == true) continue
|
}.blocked) continue
|
||||||
toRemove.remove(neighbour.ip)
|
toRemove.remove(neighbour.ip)
|
||||||
try {
|
try {
|
||||||
clients.computeIfAbsentCompat(neighbour.ip) { Client(neighbour.ip, neighbour.lladdr) }
|
clients.computeIfAbsentCompat(neighbour.ip) { Client(neighbour.ip, neighbour.lladdr) }
|
||||||
|
|||||||
@@ -40,11 +40,13 @@ object WifiApManager {
|
|||||||
*
|
*
|
||||||
* See also: https://android.googlesource.com/platform/frameworks/base/+/5c0b10a4a9eecc5307bb89a271221f2b20448797%5E%21/
|
* See also: https://android.googlesource.com/platform/frameworks/base/+/5c0b10a4a9eecc5307bb89a271221f2b20448797%5E%21/
|
||||||
*/
|
*/
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
@Deprecated("Not usable since API 26, malfunctioning on API 25")
|
@Deprecated("Not usable since API 26, malfunctioning on API 25")
|
||||||
fun start(wifiConfig: WifiConfiguration? = null) {
|
fun start(wifiConfig: WifiConfiguration? = null) {
|
||||||
app.wifi.isWifiEnabled = false
|
app.wifi.isWifiEnabled = false
|
||||||
app.wifi.setWifiApEnabled(wifiConfig, true)
|
app.wifi.setWifiApEnabled(wifiConfig, true)
|
||||||
}
|
}
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
@Deprecated("Not usable since API 26")
|
@Deprecated("Not usable since API 26")
|
||||||
fun stop() {
|
fun stop() {
|
||||||
app.wifi.setWifiApEnabled(null, false)
|
app.wifi.setWifiApEnabled(null, false)
|
||||||
|
|||||||
@@ -3,8 +3,15 @@ package be.mygod.vpnhotspot.net.wifi
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.net.wifi.WifiManager
|
import android.net.wifi.WifiManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
|
import android.view.WindowManager
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.content.edit
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,8 +20,12 @@ import be.mygod.vpnhotspot.App.Companion.app
|
|||||||
class WifiDoubleLock(lockType: Int) : AutoCloseable {
|
class WifiDoubleLock(lockType: Int) : AutoCloseable {
|
||||||
companion object : SharedPreferences.OnSharedPreferenceChangeListener {
|
companion object : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
private const val KEY = "service.wifiLock"
|
private const val KEY = "service.wifiLock"
|
||||||
private val lockType get() =
|
var mode: Mode
|
||||||
WifiDoubleLock.Mode.valueOf(app.pref.getString(KEY, WifiDoubleLock.Mode.Full.toString()) ?: "").lockType
|
@Suppress("DEPRECATION")
|
||||||
|
get() = Mode.valueOf(app.pref.getString(KEY, Mode.Full.toString()) ?: "").let {
|
||||||
|
if (it == Mode.Full && Build.VERSION.SDK_INT >= 29) Mode.None else it
|
||||||
|
}
|
||||||
|
set(value) = app.pref.edit { putString(KEY, value.toString()) }
|
||||||
private val service by lazy { app.getSystemService<PowerManager>()!! }
|
private val service by lazy { app.getSystemService<PowerManager>()!! }
|
||||||
|
|
||||||
private var holders = mutableSetOf<Any>()
|
private var holders = mutableSetOf<Any>()
|
||||||
@@ -23,7 +34,7 @@ class WifiDoubleLock(lockType: Int) : AutoCloseable {
|
|||||||
fun acquire(holder: Any) = synchronized(this) {
|
fun acquire(holder: Any) = synchronized(this) {
|
||||||
if (holders.isEmpty()) {
|
if (holders.isEmpty()) {
|
||||||
app.pref.registerOnSharedPreferenceChangeListener(this)
|
app.pref.registerOnSharedPreferenceChangeListener(this)
|
||||||
val lockType = lockType
|
val lockType = mode.lockType
|
||||||
if (lockType != null) lock = WifiDoubleLock(lockType)
|
if (lockType != null) lock = WifiDoubleLock(lockType)
|
||||||
}
|
}
|
||||||
check(holders.add(holder))
|
check(holders.add(holder))
|
||||||
@@ -40,14 +51,44 @@ class WifiDoubleLock(lockType: Int) : AutoCloseable {
|
|||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||||
if (key == KEY) synchronized(this) {
|
if (key == KEY) synchronized(this) {
|
||||||
lock?.close()
|
lock?.close()
|
||||||
val lockType = lockType
|
val lockType = mode.lockType
|
||||||
lock = if (lockType == null) null else WifiDoubleLock(lockType)
|
lock = if (lockType == null) null else WifiDoubleLock(lockType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Mode(val lockType: Int? = null) {
|
enum class Mode(val lockType: Int? = null, val keepScreenOn: Boolean = false) {
|
||||||
None, Full(WifiManager.WIFI_MODE_FULL), HighPerf(WifiManager.WIFI_MODE_FULL_HIGH_PERF)
|
None,
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
@Deprecated("This constant was deprecated in API level Q.\n" +
|
||||||
|
"This API is non-functional and will have no impact.")
|
||||||
|
Full(WifiManager.WIFI_MODE_FULL),
|
||||||
|
HighPerf(WifiManager.WIFI_MODE_FULL_HIGH_PERF),
|
||||||
|
@RequiresApi(29)
|
||||||
|
LowLatency(WifiManager.WIFI_MODE_FULL_LOW_LATENCY, true),
|
||||||
|
}
|
||||||
|
|
||||||
|
class ActivityListener(private val activity: ComponentActivity) :
|
||||||
|
DefaultLifecycleObserver, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
private var keepScreenOn: Boolean = false
|
||||||
|
set(value) {
|
||||||
|
if (field == value) return
|
||||||
|
field = value
|
||||||
|
if (value) activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
else activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
activity.lifecycle.addObserver(this)
|
||||||
|
app.pref.registerOnSharedPreferenceChangeListener(this)
|
||||||
|
keepScreenOn = mode.keepScreenOn
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||||
|
if (key == KEY) keepScreenOn = mode.keepScreenOn
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy(owner: LifecycleOwner) = app.pref.unregisterOnSharedPreferenceChangeListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val wifi = app.wifi.createWifiLock(lockType, "vpnhotspot:wifi").apply { acquire() }
|
private val wifi = app.wifi.createWifiLock(lockType, "vpnhotspot:wifi").apply { acquire() }
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import java.lang.reflect.Proxy
|
|||||||
|
|
||||||
object WifiP2pManagerHelper {
|
object WifiP2pManagerHelper {
|
||||||
const val UNSUPPORTED = -2
|
const val UNSUPPORTED = -2
|
||||||
|
@Deprecated("No longer used since API 29")
|
||||||
const val WIFI_P2P_PERSISTENT_GROUPS_CHANGED_ACTION = "android.net.wifi.p2p.PERSISTENT_GROUPS_CHANGED"
|
const val WIFI_P2P_PERSISTENT_GROUPS_CHANGED_ACTION = "android.net.wifi.p2p.PERSISTENT_GROUPS_CHANGED"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,6 +25,7 @@ object WifiP2pManagerHelper {
|
|||||||
WifiP2pManager::class.java.getDeclaredMethod("setWifiP2pChannels", WifiP2pManager.Channel::class.java,
|
WifiP2pManager::class.java.getDeclaredMethod("setWifiP2pChannels", WifiP2pManager.Channel::class.java,
|
||||||
Int::class.java, Int::class.java, WifiP2pManager.ActionListener::class.java)
|
Int::class.java, Int::class.java, WifiP2pManager.ActionListener::class.java)
|
||||||
}
|
}
|
||||||
|
@Deprecated("No longer used since API 29")
|
||||||
fun WifiP2pManager.setWifiP2pChannels(c: WifiP2pManager.Channel, lc: Int, oc: Int,
|
fun WifiP2pManager.setWifiP2pChannels(c: WifiP2pManager.Channel, lc: Int, oc: Int,
|
||||||
listener: WifiP2pManager.ActionListener) {
|
listener: WifiP2pManager.ActionListener) {
|
||||||
try {
|
try {
|
||||||
@@ -62,6 +64,7 @@ object WifiP2pManagerHelper {
|
|||||||
WifiP2pManager::class.java.getDeclaredMethod("deletePersistentGroup",
|
WifiP2pManager::class.java.getDeclaredMethod("deletePersistentGroup",
|
||||||
WifiP2pManager.Channel::class.java, Int::class.java, WifiP2pManager.ActionListener::class.java)
|
WifiP2pManager.Channel::class.java, Int::class.java, WifiP2pManager.ActionListener::class.java)
|
||||||
}
|
}
|
||||||
|
@Deprecated("No longer used since API 29")
|
||||||
fun WifiP2pManager.deletePersistentGroup(c: WifiP2pManager.Channel, netId: Int,
|
fun WifiP2pManager.deletePersistentGroup(c: WifiP2pManager.Channel, netId: Int,
|
||||||
listener: WifiP2pManager.ActionListener) {
|
listener: WifiP2pManager.ActionListener) {
|
||||||
try {
|
try {
|
||||||
@@ -88,6 +91,7 @@ object WifiP2pManagerHelper {
|
|||||||
* @param c is the channel created at {@link #initialize}
|
* @param c is the channel created at {@link #initialize}
|
||||||
* @param listener for callback when persistent group info list is available. Can be null.
|
* @param listener for callback when persistent group info list is available. Can be null.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("No longer used since API 29")
|
||||||
fun WifiP2pManager.requestPersistentGroupInfo(c: WifiP2pManager.Channel,
|
fun WifiP2pManager.requestPersistentGroupInfo(c: WifiP2pManager.Channel,
|
||||||
listener: (Collection<WifiP2pGroup>) -> Unit) {
|
listener: (Collection<WifiP2pGroup>) -> Unit) {
|
||||||
val proxy = Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader,
|
val proxy = Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader,
|
||||||
@@ -110,5 +114,6 @@ object WifiP2pManagerHelper {
|
|||||||
* Source: https://android.googlesource.com/platform/frameworks/base/+/android-4.2_r1/wifi/java/android/net/wifi/p2p/WifiP2pGroup.java#253
|
* Source: https://android.googlesource.com/platform/frameworks/base/+/android-4.2_r1/wifi/java/android/net/wifi/p2p/WifiP2pGroup.java#253
|
||||||
*/
|
*/
|
||||||
private val getNetworkId by lazy { WifiP2pGroup::class.java.getDeclaredMethod("getNetworkId") }
|
private val getNetworkId by lazy { WifiP2pGroup::class.java.getDeclaredMethod("getNetworkId") }
|
||||||
|
@Deprecated("No longer used since API 29")
|
||||||
val WifiP2pGroup.netId get() = getNetworkId.invoke(this) as Int
|
val WifiP2pGroup.netId get() = getNetworkId.invoke(this) as Int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import java.lang.IllegalStateException
|
|||||||
* https://android.googlesource.com/platform/external/wpa_supplicant_8/+/d2986c2/wpa_supplicant/config.c#488
|
* https://android.googlesource.com/platform/external/wpa_supplicant_8/+/d2986c2/wpa_supplicant/config.c#488
|
||||||
* https://android.googlesource.com/platform/external/wpa_supplicant_8/+/6fa46df/wpa_supplicant/config_file.c#182
|
* https://android.googlesource.com/platform/external/wpa_supplicant_8/+/6fa46df/wpa_supplicant/config_file.c#182
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("No longer used since API 29")
|
||||||
class P2pSupplicantConfiguration(private val group: WifiP2pGroup, ownerAddress: String?) {
|
class P2pSupplicantConfiguration(private val group: WifiP2pGroup, ownerAddress: String?) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "P2pSupplicantConfiguration"
|
private const val TAG = "P2pSupplicantConfiguration"
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import be.mygod.vpnhotspot.util.toParcelable
|
|||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
import kotlinx.android.synthetic.main.dialog_wifi_ap.view.*
|
import kotlinx.android.synthetic.main.dialog_wifi_ap.view.*
|
||||||
import java.lang.IllegalStateException
|
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -114,7 +113,13 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
|||||||
if (!arg.readOnly) dialogView.password.addTextChangedListener(this@WifiApDialogFragment)
|
if (!arg.readOnly) dialogView.password.addTextChangedListener(this@WifiApDialogFragment)
|
||||||
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) dialogView.band.apply {
|
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) dialogView.band.apply {
|
||||||
bandOptions = mutableListOf<BandOption>().apply {
|
bandOptions = mutableListOf<BandOption>().apply {
|
||||||
if (arg.p2pMode) add(BandOption.BandAny) else {
|
if (arg.p2pMode) {
|
||||||
|
add(BandOption.BandAny)
|
||||||
|
if (Build.VERSION.SDK_INT >= 29) {
|
||||||
|
add(BandOption.Band2GHz)
|
||||||
|
add(BandOption.Band5GHz)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (Build.VERSION.SDK_INT >= 28) add(BandOption.BandAny)
|
if (Build.VERSION.SDK_INT >= 28) add(BandOption.BandAny)
|
||||||
add(BandOption.Band2GHz)
|
add(BandOption.Band2GHz)
|
||||||
add(BandOption.Band5GHz)
|
add(BandOption.Band5GHz)
|
||||||
@@ -172,8 +177,8 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
|||||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
||||||
return when (item?.itemId) {
|
return when (item?.itemId) {
|
||||||
android.R.id.copy -> {
|
android.R.id.copy -> {
|
||||||
app.clipboard.primaryClip = ClipData.newPlainText(null,
|
app.clipboard.setPrimaryClip(ClipData.newPlainText(null,
|
||||||
Base64.encodeToString(ret.configuration.toByteArray(), BASE64_FLAGS))
|
Base64.encodeToString(ret.configuration.toByteArray(), BASE64_FLAGS)))
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
android.R.id.paste -> try {
|
android.R.id.paste -> try {
|
||||||
@@ -187,8 +192,13 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
R.id.share_qr -> {
|
R.id.share_qr -> {
|
||||||
QRCodeDialog().withArg(ret.configuration.toQRString())
|
val qrString = try {
|
||||||
.show(fragmentManager ?: return false, "QRCodeDialog")
|
ret.configuration.toQRString()
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
SmartSnackbar.make(e).show()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
QRCodeDialog().withArg(qrString).show(fragmentManager ?: return false, "QRCodeDialog")
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> false
|
else -> false
|
||||||
|
|||||||
@@ -64,10 +64,15 @@ fun channelToFrequency(channel: Int) = when (channel) {
|
|||||||
in 15..165 -> 5000 + 5 * channel
|
in 15..165 -> 5000 + 5 * channel
|
||||||
else -> throw IllegalArgumentException("Invalid channel $channel")
|
else -> throw IllegalArgumentException("Invalid channel $channel")
|
||||||
}
|
}
|
||||||
|
fun frequencyToChannel(frequency: Int) = when (frequency % 5) {
|
||||||
|
2 -> ((frequency - 2407) / 5).also { check(it in 1..14) { "Invalid 2.4 GHz frequency $frequency" } }
|
||||||
|
0 -> ((frequency - 5000) / 5).also { check(it in 15..165) { "Invalid 5 GHz frequency $frequency" } }
|
||||||
|
else -> throw IllegalArgumentException("Invalid frequency $frequency")
|
||||||
|
}
|
||||||
|
|
||||||
val WifiConfiguration.apKeyManagement get() = allowedKeyManagement.nextSetBit(0).also { selected ->
|
val WifiConfiguration.apKeyManagement get() = allowedKeyManagement.nextSetBit(0).let { selected ->
|
||||||
check(selected >= 0) { "No key management selected" }
|
|
||||||
check(allowedKeyManagement.nextSetBit(selected + 1) < 0) { "More than 1 key managements supplied" }
|
check(allowedKeyManagement.nextSetBit(selected + 1) < 0) { "More than 1 key managements supplied" }
|
||||||
|
if (selected < 0) WifiConfiguration.KeyMgmt.NONE else selected // getAuthType returns NONE if nothing is selected
|
||||||
}
|
}
|
||||||
|
|
||||||
private val qrSanitizer = Regex("([\\\\\":;,])")
|
private val qrSanitizer = Regex("([\\\\\":;,])")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package be.mygod.vpnhotspot.room
|
package be.mygod.vpnhotspot.room
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.map
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@@ -12,12 +13,12 @@ data class ClientRecord(@PrimaryKey
|
|||||||
@androidx.room.Dao
|
@androidx.room.Dao
|
||||||
abstract class Dao {
|
abstract class Dao {
|
||||||
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
|
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
|
||||||
abstract suspend fun lookup(mac: Long): ClientRecord?
|
protected abstract suspend fun lookup(mac: Long): ClientRecord?
|
||||||
|
|
||||||
suspend fun lookupOrDefault(mac: Long) = lookup(mac) ?: ClientRecord(mac)
|
suspend fun lookupOrDefault(mac: Long) = lookup(mac) ?: ClientRecord(mac)
|
||||||
|
|
||||||
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
|
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
|
||||||
abstract fun lookupSync(mac: Long): LiveData<ClientRecord>
|
protected abstract fun lookupSync(mac: Long): LiveData<ClientRecord?>
|
||||||
|
fun lookupOrDefaultSync(mac: Long) = lookupSync(mac).map { it ?: ClientRecord(mac) }
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
protected abstract suspend fun updateInternal(value: ClientRecord): Long
|
protected abstract suspend fun updateInternal(value: ClientRecord): Long
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ object SpanFormatter {
|
|||||||
i = m.start()
|
i = m.start()
|
||||||
val exprEnd = m.end()
|
val exprEnd = m.end()
|
||||||
|
|
||||||
val argTerm = m.group(1)
|
val argTerm = m.group(1)!!
|
||||||
val modTerm = m.group(2)
|
val modTerm = m.group(2)
|
||||||
|
|
||||||
val cookedArg = when (val typeTerm = m.group(3)) {
|
val cookedArg = when (val typeTerm = m.group(3)) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package be.mygod.vpnhotspot.util
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.*
|
import android.content.*
|
||||||
|
import android.net.InetAddresses
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
@@ -93,12 +94,13 @@ fun NetworkInterface.formatAddresses(macOnly: Boolean = false) = SpannableString
|
|||||||
}
|
}
|
||||||
}.trimEnd()
|
}.trimEnd()
|
||||||
|
|
||||||
private val parseNumericAddress by lazy {
|
private val parseNumericAddress by lazy @SuppressLint("SoonBlockedPrivateApi") {
|
||||||
InetAddress::class.java.getDeclaredMethod("parseNumericAddress", String::class.java).apply {
|
InetAddress::class.java.getDeclaredMethod("parseNumericAddress", String::class.java).apply {
|
||||||
isAccessible = true
|
isAccessible = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun parseNumericAddress(address: String) = parseNumericAddress.invoke(null, address) as InetAddress
|
fun parseNumericAddress(address: String) = if (Build.VERSION.SDK_INT >= 29)
|
||||||
|
InetAddresses.parseNumericAddress(address) else parseNumericAddress.invoke(null, address) as InetAddress
|
||||||
|
|
||||||
fun Context.launchUrl(url: String) {
|
fun Context.launchUrl(url: String) {
|
||||||
if (app.hasTouch) try {
|
if (app.hasTouch) try {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/title_repeater"
|
android:text="@{data.title}"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/>
|
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/>
|
||||||
|
|
||||||
<be.mygod.vpnhotspot.widget.AutoCollapseTextView
|
<be.mygod.vpnhotspot.widget.AutoCollapseTextView
|
||||||
|
|||||||
13
mobile/src/main/res/values-v29/arrays.xml
Normal file
13
mobile/src/main/res/values-v29/arrays.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string-array name="settings_service_wifi_lock">
|
||||||
|
<item>@string/settings_service_wifi_lock_none</item>
|
||||||
|
<item>@string/settings_service_wifi_lock_high_perf_v29</item>
|
||||||
|
<item>@string/settings_service_wifi_lock_low_latency</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="settings_service_wifi_lock_values">
|
||||||
|
<item>None</item>
|
||||||
|
<item>HighPerf</item>
|
||||||
|
<item>LowLatency</item>
|
||||||
|
</string-array>
|
||||||
|
</resources>
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
<string name="title_clients">已连设备</string>
|
<string name="title_clients">已连设备</string>
|
||||||
<string name="title_settings">设置选项</string>
|
<string name="title_settings">设置选项</string>
|
||||||
|
|
||||||
|
<string name="repeater_channel">无线中继 (%1$d MHz, 频道 %2$d)</string>
|
||||||
<string name="repeater_wps">WPS(不安全)</string>
|
<string name="repeater_wps">WPS(不安全)</string>
|
||||||
<string name="repeater_wps_dialog_title">输入 PIN</string>
|
<string name="repeater_wps_dialog_title">输入 PIN</string>
|
||||||
<string name="repeater_wps_dialog_pbc">一键加密</string>
|
<string name="repeater_wps_dialog_pbc">一键加密</string>
|
||||||
@@ -89,6 +90,8 @@
|
|||||||
<string name="settings_service_wifi_lock_none">系统默认</string>
|
<string name="settings_service_wifi_lock_none">系统默认</string>
|
||||||
<string name="settings_service_wifi_lock_full">开</string>
|
<string name="settings_service_wifi_lock_full">开</string>
|
||||||
<string name="settings_service_wifi_lock_high_perf">高性能模式</string>
|
<string name="settings_service_wifi_lock_high_perf">高性能模式</string>
|
||||||
|
<string name="settings_service_wifi_lock_high_perf_v29">禁用省电</string>
|
||||||
|
<string name="settings_service_wifi_lock_low_latency">低延迟模式</string>
|
||||||
<string name="settings_service_ip_monitor">网络状态监听模式</string>
|
<string name="settings_service_ip_monitor">网络状态监听模式</string>
|
||||||
<string name="settings_service_ip_monitor_monitor">Netlink 监听</string>
|
<string name="settings_service_ip_monitor_monitor">Netlink 监听</string>
|
||||||
<string name="settings_service_ip_monitor_monitor_root">Netlink 监听 (root)</string>
|
<string name="settings_service_ip_monitor_monitor_root">Netlink 监听 (root)</string>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<string name="title_clients">Clients</string>
|
<string name="title_clients">Clients</string>
|
||||||
<string name="title_settings">Settings</string>
|
<string name="title_settings">Settings</string>
|
||||||
|
|
||||||
|
<string name="repeater_channel">Repeater (%1$d MHz, channel %2$d)</string>
|
||||||
<string name="repeater_wps">WPS (insecure)</string>
|
<string name="repeater_wps">WPS (insecure)</string>
|
||||||
<string name="repeater_wps_dialog_title">Enter PIN</string>
|
<string name="repeater_wps_dialog_title">Enter PIN</string>
|
||||||
<string name="repeater_wps_dialog_pbc">Push Button</string>
|
<string name="repeater_wps_dialog_pbc">Push Button</string>
|
||||||
@@ -94,6 +95,8 @@
|
|||||||
<string name="settings_service_wifi_lock_none">System default</string>
|
<string name="settings_service_wifi_lock_none">System default</string>
|
||||||
<string name="settings_service_wifi_lock_full">On</string>
|
<string name="settings_service_wifi_lock_full">On</string>
|
||||||
<string name="settings_service_wifi_lock_high_perf">High Performance Mode</string>
|
<string name="settings_service_wifi_lock_high_perf">High Performance Mode</string>
|
||||||
|
<string name="settings_service_wifi_lock_high_perf_v29">Disable power save</string>
|
||||||
|
<string name="settings_service_wifi_lock_low_latency">Low latency mode</string>
|
||||||
<string name="settings_service_ip_monitor">Network status monitor mode</string>
|
<string name="settings_service_ip_monitor">Network status monitor mode</string>
|
||||||
<string name="settings_service_ip_monitor_monitor">Netlink monitor</string>
|
<string name="settings_service_ip_monitor_monitor">Netlink monitor</string>
|
||||||
<string name="settings_service_ip_monitor_monitor_root">Netlink monitor with root</string>
|
<string name="settings_service_ip_monitor_monitor_root">Netlink monitor with root</string>
|
||||||
|
|||||||
@@ -41,7 +41,6 @@
|
|||||||
app:icon="@drawable/ic_device_wifi_lock"
|
app:icon="@drawable/ic_device_wifi_lock"
|
||||||
app:entries="@array/settings_service_wifi_lock"
|
app:entries="@array/settings_service_wifi_lock"
|
||||||
app:entryValues="@array/settings_service_wifi_lock_values"
|
app:entryValues="@array/settings_service_wifi_lock_values"
|
||||||
app:defaultValue="Full"
|
|
||||||
app:title="@string/settings_service_wifi_lock"
|
app:title="@string/settings_service_wifi_lock"
|
||||||
app:useSimpleSummaryProvider="true"/>
|
app:useSimpleSummaryProvider="true"/>
|
||||||
<SwitchPreference
|
<SwitchPreference
|
||||||
|
|||||||
Reference in New Issue
Block a user