Merge pull request #90 from Mygod/q-beta

Support Android Q beta 4/Android 10.
This commit is contained in:
Mygod
2019-06-10 22:25:33 +08:00
committed by GitHub
36 changed files with 409 additions and 165 deletions

View File

@@ -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.

View File

@@ -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'

View File

@@ -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:

View File

@@ -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

View File

@@ -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'
} }

View File

@@ -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"

View File

@@ -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()
} }

View File

@@ -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()

View File

@@ -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) {

View File

@@ -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()
registerReceiver(deviceListener, intentFilter(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION, if (Build.VERSION.SDK_INT < 29) @Suppress("DEPRECATION") {
WifiP2pManagerHelper.WIFI_P2P_PERSISTENT_GROUPS_CHANGED_ACTION)) registerReceiver(deviceListener, intentFilter(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION,
app.pref.registerOnSharedPreferenceChangeListener(this) WifiP2pManagerHelper.WIFI_P2P_PERSISTENT_GROUPS_CHANGED_ACTION))
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,34 +246,86 @@ 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
p2pManager.requestGroupInfo(channel) { try {
when { p2pManager.requestGroupInfo(channel) {
it == null -> doStart() when {
it.isGroupOwner -> if (routingManager == null) doStart(it) it == null -> doStart()
else -> { it.isGroupOwner -> if (routingManager == null) doStart(it)
Timber.i("Removing old group ($it)") else -> {
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener { Timber.i("Removing old group ($it)")
override fun onSuccess() = doStart() p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
override fun onFailure(reason: Int) = override fun onSuccess() = doStart()
startFailure(formatReason(R.string.repeater_remove_old_group_failure, reason)) override fun onFailure(reason: Int) =
}) startFailure(formatReason(R.string.repeater_remove_old_group_failure, reason))
})
}
} }
} }
} 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 onSuccess() { } // wait for WIFI_P2P_CONNECTION_CHANGED_ACTION to fire to go to step 3 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
}
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
app.pref.unregisterOnSharedPreferenceChangeListener(this) if (Build.VERSION.SDK_INT < 29) @Suppress("DEPRECATION") {
unregisterReceiver(deviceListener) app.pref.unregisterOnSharedPreferenceChangeListener(this)
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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 = Transformations.map(record) { it?.nickname.isNullOrEmpty() } val titleSelectable = record.map { it.nickname.isEmpty() }
val description = Transformations.map(record) { record -> val description = record.map { 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) {

View File

@@ -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()
} }

View File

@@ -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
} }

View File

@@ -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

View File

@@ -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
} }
/** /**

View File

@@ -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,25 +163,51 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
} }
val configuration: WifiConfiguration? get() { val configuration: WifiConfiguration? get() {
val group = binder?.group if (Build.VERSION.SDK_INT >= 29) {
if (group != null) try { val networkName = RepeaterService.networkName
val config = P2pSupplicantConfiguration(group, binder?.thisDevice?.deviceAddress) val passphrase = RepeaterService.passphrase
holder.config = config if (networkName != null && passphrase != null) {
return newWifiApConfiguration(group.networkName, config.psk).apply { return newWifiApConfiguration(networkName, passphrase).apply {
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK) // is not actually used allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK) // is not actually used
if (Build.VERSION.SDK_INT >= 23) { apBand = when (RepeaterService.operatingBand) {
apBand = AP_BAND_ANY 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 apChannel = RepeaterService.operatingChannel
} }
} }
} catch (e: RuntimeException) { } else @Suppress("DEPRECATION") {
Timber.w(e) val group = binder?.group
if (group != null) try {
val config = P2pSupplicantConfiguration(group, binder?.thisDevice?.deviceAddress)
holder.config = config
return newWifiApConfiguration(group.networkName, config.psk).apply {
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK) // is not actually used
if (Build.VERSION.SDK_INT >= 23) {
apBand = AP_BAND_ANY
apChannel = RepeaterService.operatingChannel
}
}
} catch (e: RuntimeException) {
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

View File

@@ -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()
} }

View File

@@ -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() val context = requireContext()
context.startForegroundService(Intent(context, LocalOnlyHotspotService::class.java)) context.startForegroundService(Intent(context, RepeaterService::class.java))
} }
} else super.onRequestPermissionsResult(requestCode, permissions, grantResults) START_LOCAL_ONLY_HOTSPOT -> {
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) @TargetApi(26) {
val context = requireContext()
context.startForegroundService(Intent(context, LocalOnlyHotspotService::class.java))
}
}
else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
} }
override fun onServiceConnected(name: ComponentName?, service: IBinder?) { override fun onServiceConnected(name: ComponentName?, service: IBinder?) {

View File

@@ -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

View File

@@ -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) }

View File

@@ -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)

View File

@@ -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() }

View File

@@ -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
} }

View File

@@ -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"

View File

@@ -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

View File

@@ -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("([\\\\\":;,])")

View File

@@ -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

View File

@@ -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)) {

View File

@@ -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 {

View File

@@ -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

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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