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.
- Choose "System default" to save battery life;
- 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 "On" (default) if repeater/hotspot turns itself off automatically or stops working after a while;
- (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.
* Network status monitor mode: This option controls how the app monitors connected devices as well as interface changes
(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 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/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;->onTetheringFailed()V,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#112696)
* (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;->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;->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;->stopTethering(I)V,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#112974)
* (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;->apChannel:I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#120724)
* [`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;->setWifiApConfiguration(Landroid/net/wifi/WifiConfiguration;)Z,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#121416)
* (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,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,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,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/+/3d07e5c/appcompat/hiddenapi-flags.csv#123309)
* (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,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/+/3d07e5c/appcompat/hiddenapi-flags.csv#131529)
* (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;,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,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`
* [`Landroid/net/wifi/p2p/WifiP2pGroup;->getNetworkId()I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#123194)
* [`Landroid/net/wifi/p2p/WifiP2pGroupList;->getGroupList()Ljava/util/Collection;,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#123239)
* [`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)
* [`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)
* [`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)
* [`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)
* [`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) [`Landroid/net/wifi/p2p/WifiP2pGroup;->getNetworkId()I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#134440)
* (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)
* (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)
* (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)
* (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/+/3d07e5c/appcompat/hiddenapi-flags.csv#134738)
* (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:
@@ -138,21 +150,22 @@ Undocumented system configurations:
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.
* Several constants in `ConnectivityManager` is assumed to be defined as in `TetheringManager.kt`;
* 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/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`;
- [`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`.
- (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.
For `ip rule` priorities, `RULE_PRIORITY_SECURE_VPN` and `RULE_PRIORITY_TETHERING` is assumed to be 12000 and 18000
respectively; `RULE_PRIORITY_DEFAULT_NETWORK` is assumed to be 22000 (or at least > 18000) for API 27-.
For `ip rule` priorities, `RULE_PRIORITY_SECURE_VPN` and `RULE_PRIORITY_TETHERING` is assumed to be 12000 and 18000 respectively;
(prior to API 24) `RULE_PRIORITY_DEFAULT_NETWORK` is assumed to be 22000 (or at least > 18000).
DHCP server like `dnsmasq` is assumed to run and send DHCP packets as root.
Undocumented system binaries are all bundled and executable:
* Since API 24: `iptables-save`;
* (since API 24) `iptables-save`;
* `echo`;
* `ip` (`link monitor neigh rule` with proper output format);
* `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.
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;
* Android system is expected to restart `wpa_supplicant` after it crashes.

View File

@@ -12,7 +12,7 @@ buildscript {
}
}
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.google.gms:google-services:4.2.0'
classpath 'io.fabric.tools:gradle:1.29.0'

View File

@@ -363,7 +363,7 @@ style:
OptionalUnit:
active: true
OptionalWhenBraces:
active: true
active: false
PreferToOverPairSyntax:
active: false
ProtectedMemberInFinalClass:

View File

@@ -10,7 +10,6 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
android.enableJetifier=true
android.enableR8=true
android.enableR8.fullMode=true
android.useAndroidX=true
org.gradle.jvmargs=-Xmx1536m

View File

@@ -9,8 +9,7 @@ if (!getGradle().getStartParameter().getTaskRequests().toString().contains("Fdro
}
android {
buildToolsVersion "28.0.3"
compileSdkVersion 28
compileSdkVersion 29
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
@@ -18,7 +17,7 @@ android {
defaultConfig {
applicationId "be.mygod.vpnhotspot"
minSdkVersion 21
targetSdkVersion 28
targetSdkVersion 29
resConfigs "ru", "zh-rCN"
versionCode 204
versionName '2.4.4'
@@ -65,25 +64,26 @@ androidExtensions {
}
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',
]
def lifecycleVersion = '2.0.0'
def roomVersion = '2.1.0-beta01'
def lifecycleVersion = '2.1.0-beta01'
def roomVersion = '2.1.0-rc01'
dependencies {
kapt "androidx.room:room-compiler:$roomVersion"
implementation fileTree(dir: 'libs', include: ['*.jar'])
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.lifecycle:lifecycle-common-java8:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-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 '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.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.linkedin.dexmaker:dexmaker:2.25.0'
implementation 'com.takisoft.preferencex:preferencex-simplemenu:1.0.0'
@@ -94,10 +94,9 @@ dependencies {
freedomImplementation dep
googleImplementation dep
}
testImplementation "androidx.arch.core:core-testing:$lifecycleVersion"
testImplementation 'junit:junit:4.12'
androidTestImplementation "androidx.room:room-testing:$roomVersion"
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.0'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.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"
tools:ignore="ProtectedPermissions"/>
<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
android:name=".App"

View File

@@ -8,13 +8,13 @@ import android.content.res.Configuration
import android.net.ConnectivityManager
import android.net.wifi.WifiManager
import android.os.Build
import android.preference.PreferenceManager
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.provider.FontRequest
import androidx.emoji.text.EmojiCompat
import androidx.emoji.text.FontRequestEmojiCompatConfig
import androidx.preference.PreferenceManager
import be.mygod.vpnhotspot.net.DhcpWorkaround
import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.util.DeviceStorageApp
@@ -33,7 +33,8 @@ class App : Application() {
app = this
if (Build.VERSION.SDK_INT >= 24) {
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)
} else deviceStorage = this
DebugHelper.init()
@@ -53,7 +54,7 @@ class App : Application() {
if (DhcpWorkaround.shouldEnable) DhcpWorkaround.enable(true)
}
override fun onConfigurationChanged(newConfig: Configuration?) {
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
ServiceNotification.updateNotificationChannels()
}

View File

@@ -41,7 +41,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService() {
private var routingManager: RoutingManager? = null
private var receiverRegistered = false
private val receiver = broadcastReceiver { _, intent ->
val ifaces = intent.localOnlyTetheredIfaces
val ifaces = intent.localOnlyTetheredIfaces ?: return@broadcastReceiver
DebugHelper.log(TAG, "onTetherStateChangedLocked: $ifaces")
check(ifaces.size <= 1)
val iface = ifaces.singleOrNull()

View File

@@ -7,13 +7,14 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.get
import androidx.lifecycle.observe
import be.mygod.vpnhotspot.client.ClientViewModel
import be.mygod.vpnhotspot.client.ClientsFragment
import be.mygod.vpnhotspot.databinding.ActivityMainBinding
import be.mygod.vpnhotspot.manage.TetheringFragment
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.widget.SmartSnackbar
import com.google.android.material.bottomnavigation.BottomNavigationView
@@ -29,14 +30,15 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
if (savedInstanceState == null) displayFragment(TetheringFragment())
val model = ViewModelProviders.of(this).get<ClientViewModel>()
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 {
backgroundColor = ContextCompat.getColor(this@MainActivity, R.color.colorSecondary)
badgeTextColor = ContextCompat.getColor(this@MainActivity, R.color.primary_text_default_material_light)
number = it.size
} else binding.navigation.removeBadge(R.id.navigation_clients)
})
}
SmartSnackbar.Register(lifecycle, binding.fragmentHolder)
WifiDoubleLock.ActivityListener(this)
}
override fun onNavigationItemSelected(item: MenuItem) = when (item.itemId) {

View File

@@ -1,18 +1,17 @@
package be.mygod.vpnhotspot
import android.annotation.SuppressLint
import android.app.Service
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Configuration
import android.net.NetworkInfo
import android.net.wifi.p2p.WifiP2pDevice
import android.net.wifi.p2p.WifiP2pGroup
import android.net.wifi.p2p.WifiP2pInfo
import android.net.wifi.p2p.WifiP2pManager
import android.net.wifi.WpsInfo
import android.net.wifi.p2p.*
import android.os.Build
import android.os.Handler
import android.os.Looper
import androidx.annotation.StringRes
import androidx.core.content.edit
import androidx.core.content.getSystemService
import be.mygod.vpnhotspot.App.Companion.app
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.setWifiP2pChannels
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps
import be.mygod.vpnhotspot.util.StickyEvent0
import be.mygod.vpnhotspot.util.StickyEvent1
import be.mygod.vpnhotspot.util.broadcastReceiver
import be.mygod.vpnhotspot.util.intentFilter
import be.mygod.vpnhotspot.net.wifi.configuration.channelToFrequency
import be.mygod.vpnhotspot.util.*
import be.mygod.vpnhotspot.widget.SmartSnackbar
import timber.log.Timber
import java.lang.reflect.InvocationTargetException
@@ -35,7 +32,14 @@ import java.lang.reflect.InvocationTargetException
class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPreferences.OnSharedPreferenceChangeListener {
companion object {
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.
@@ -49,14 +53,24 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
}
}
val supported get() = p2pManager != null
@Deprecated("Not initialized and no use at all since API 29")
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
get() {
val result = app.pref.getString(KEY_OPERATING_CHANNEL, null)?.toIntOrNull() ?: 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 {
@@ -73,16 +87,16 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
groupChanged(value)
}
val groupChanged = StickyEvent1 { group }
@Deprecated("Not initialized and no use at all since API 29")
var thisDevice: WifiP2pDevice? = null
@Deprecated("WPS was deprecated RIP")
fun startWps(pin: String? = null) {
val channel = channel
if (channel == null) SmartSnackbar.make(R.string.repeater_failure_disconnected).show()
else @Suppress("DEPRECATION") if (active) p2pManager.startWps(channel, android.net.wifi.WpsInfo().apply {
setup = if (pin == null) android.net.wifi.WpsInfo.PBC else {
else if (active) p2pManager.startWps(channel, WpsInfo().apply {
setup = if (pin == null) WpsInfo.PBC else {
this.pin = pin
android.net.wifi.WpsInfo.KEYPAD
WpsInfo.KEYPAD
}
}, object : WifiP2pManager.ActionListener {
override fun onSuccess() = SmartSnackbar.make(
@@ -109,11 +123,12 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
if (intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, 0) ==
WifiP2pManager.WIFI_P2P_STATE_DISABLED) clean() // ignore P2P enabled
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> onP2pConnectionChanged(
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_INFO)!!,
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP)!!)
}
}
@Deprecated("No longer used since API 29")
@Suppress("DEPRECATION")
private val deviceListener = broadcastReceiver { _, intent ->
when (intent.action) {
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 persistNextGroup = false
var status = Status.IDLE
private set(value) {
@@ -144,13 +160,17 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
override fun onCreate() {
super.onCreate()
onChannelDisconnected()
registerReceiver(deviceListener, intentFilter(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION,
WifiP2pManagerHelper.WIFI_P2P_PERSISTENT_GROUPS_CHANGED_ACTION))
app.pref.registerOnSharedPreferenceChangeListener(this)
if (Build.VERSION.SDK_INT < 29) @Suppress("DEPRECATION") {
registerReceiver(deviceListener, intentFilter(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION,
WifiP2pManagerHelper.WIFI_P2P_PERSISTENT_GROUPS_CHANGED_ACTION))
app.pref.registerOnSharedPreferenceChangeListener(this)
}
}
override fun onBind(intent: Intent) = binder
@Deprecated("No longer used since API 29")
@Suppress("DEPRECATION")
private fun setOperatingChannel(oc: Int = operatingChannel) = try {
val channel = channel
if (channel == null) SmartSnackbar.make(R.string.repeater_failure_disconnected).show()
@@ -173,17 +193,21 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
channel = null
if (status != Status.DESTROYED) try {
channel = p2pManager.initialize(this, Looper.getMainLooper(), this)
setOperatingChannel()
if (Build.VERSION.SDK_INT < 29) @Suppress("DEPRECATION") setOperatingChannel()
} catch (e: RuntimeException) {
Timber.w(e)
handler.postDelayed(this::onChannelDisconnected, 1000)
}
}
@Deprecated("No longer used since API 29")
@Suppress("DEPRECATION")
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (key == KEY_OPERATING_CHANNEL) setOperatingChannel()
}
@Deprecated("No longer used since API 29")
@Suppress("DEPRECATION")
private fun onPersistentGroupsChanged() {
val channel = channel ?: 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,
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION))
receiverRegistered = true
p2pManager.requestGroupInfo(channel) {
when {
it == null -> doStart()
it.isGroupOwner -> if (routingManager == null) doStart(it)
else -> {
Timber.i("Removing old group ($it)")
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
override fun onSuccess() = doStart()
override fun onFailure(reason: Int) =
startFailure(formatReason(R.string.repeater_remove_old_group_failure, reason))
})
try {
p2pManager.requestGroupInfo(channel) {
when {
it == null -> doStart()
it.isGroupOwner -> if (routingManager == null) doStart(it)
else -> {
Timber.i("Removing old group ($it)")
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
override fun onSuccess() = doStart()
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
}
/**
* startService Step 2 (if a group isn't already available)
*/
private fun doStart() = p2pManager.createGroup(channel, 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
})
private fun doStart() {
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
}
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
*/
private fun onP2pConnectionChanged(info: WifiP2pInfo, net: NetworkInfo?, group: WifiP2pGroup) {
DebugHelper.log(TAG, "P2P connection changed: $info\n$net\n$group")
private fun onP2pConnectionChanged(info: WifiP2pInfo, group: WifiP2pGroup) {
DebugHelper.log(TAG, "P2P connection changed: $info\n$group")
when {
!info.groupFormed || !info.isGroupOwner || !group.isGroupOwner -> {
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) {
binder.group = group
if (persistNextGroup) {
networkName = group.networkName
passphrase = group.passphrase
persistNextGroup = false
}
check(routingManager == null)
routingManager = RoutingManager.LocalOnly(this, group.`interface`!!).apply { start() }
status = Status.ACTIVE
@@ -310,8 +391,10 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
handler.removeCallbacksAndMessages(null)
if (status != Status.IDLE) binder.shutdown()
clean() // force clean to prevent leakage
app.pref.unregisterOnSharedPreferenceChangeListener(this)
unregisterReceiver(deviceListener)
if (Build.VERSION.SDK_INT < 29) @Suppress("DEPRECATION") {
app.pref.unregisterOnSharedPreferenceChangeListener(this)
unregisterReceiver(deviceListener)
}
status = Status.DESTROYED
if (Build.VERSION.SDK_INT >= 27) channel?.close()
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.monitor.IpMonitor
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.SharedPreferenceDataStore
import be.mygod.vpnhotspot.util.RootSession
@@ -26,6 +27,7 @@ import java.net.SocketException
class SettingsPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
WifiDoubleLock.mode = WifiDoubleLock.mode // handle complicated default value and possible system upgrades
preferenceManager.preferenceDataStore = SharedPreferenceDataStore(app.pref)
RoutingManager.masqueradeMode = RoutingManager.masqueradeMode // flush default value
addPreferencesFromResource(R.xml.pref_settings)

View File

@@ -48,7 +48,7 @@ class TetheringService : IpNeighbourMonitoringService() {
private val receiver = broadcastReceiver { _, intent ->
synchronized(downstreams) {
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
if (downstream.monitor) downstream.start()
}
@@ -90,7 +90,7 @@ class TetheringService : IpNeighbourMonitoringService() {
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]
if (downstream == null) Downstream(this, iface, true).apply {
start()
@@ -98,7 +98,7 @@ class TetheringService : IpNeighbourMonitoringService() {
downstreams[iface] = this
} 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
onDownstreamsChangedLocked()
} else if (downstreams.isEmpty()) stopSelf(startId)

View File

@@ -3,7 +3,7 @@ package be.mygod.vpnhotspot.client
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.StrikethroughSpan
import androidx.lifecycle.Transformations
import androidx.lifecycle.map
import androidx.recyclerview.widget.DiffUtil
import be.mygod.vpnhotspot.App.Companion.app
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 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 {
append('%')
append(iface)
@@ -37,24 +37,22 @@ open class Client(val mac: Long, val iface: String) {
val blocked get() = record.value?.blocked == true
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,
* as record might not be initialized in other more appropriate places
*/
SpannableStringBuilder(if (record?.nickname.isNullOrEmpty()) {
if (record?.macLookupPending != false) MacLookup.perform(mac)
SpannableStringBuilder(if (record.nickname.isEmpty()) {
if (record.macLookupPending) MacLookup.perform(mac)
macIface
} else emojize(record?.nickname)).apply {
if (record?.blocked == true) {
setSpan(StrikethroughSpan(), 0, length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
}
} else emojize(record.nickname)).apply {
if (record.blocked) setSpan(StrikethroughSpan(), 0, length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
}
}
val titleSelectable = Transformations.map(record) { it?.nickname.isNullOrEmpty() }
val description = Transformations.map(record) { record ->
val titleSelectable = record.map { it.nickname.isEmpty() }
val description = record.map { record ->
SpannableStringBuilder().apply {
if (!record?.nickname.isNullOrEmpty()) appendln(macIface)
if (record.nickname.isNotEmpty()) appendln(macIface)
ip.entries.forEach { (ip, state) ->
append(makeIpSpan(ip))
appendln(app.getText(when (state) {

View File

@@ -20,7 +20,8 @@ import be.mygod.vpnhotspot.util.broadcastReceiver
class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callback {
private var tetheredInterfaces = emptySet<String>()
private val receiver = broadcastReceiver { _, intent ->
tetheredInterfaces = intent.tetheredIfaces.toSet() + intent.localOnlyTetheredIfaces
tetheredInterfaces = (intent.tetheredIfaces ?: return@broadcastReceiver).toSet() +
(intent.localOnlyTetheredIfaces ?: return@broadcastReceiver)
populateClients()
}

View File

@@ -17,9 +17,9 @@ import androidx.appcompat.widget.PopupMenu
import androidx.databinding.BaseObservable
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.get
import androidx.lifecycle.observe
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
@@ -219,8 +219,9 @@ class ClientsFragment : Fragment() {
binding.swipeRefresher.setOnRefreshListener {
IpNeighbourMonitor.instance?.flush()
}
ViewModelProviders.of(requireActivity()).get<ClientViewModel>().clients.observe(this,
Observer { adapter.submitList(it.toMutableList()) })
ViewModelProviders.of(requireActivity()).get<ClientViewModel>().clients.observe(this) {
adapter.submitList(it.toMutableList())
}
return binding.root
}

View File

@@ -47,7 +47,7 @@ object MacLookup {
try {
val response = conn.inputStream.bufferedReader().readText()
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 match = extractCountry(mac, response, obj)
val result = if (match != null) {
@@ -71,9 +71,9 @@ object MacLookup {
}
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")
if (address.isNullOrBlank()) return null
if (address.isBlank()) return null
countryCodeRegex.find(address)?.also { return it }
Timber.w(UnexpectedError(mac, response))
return null

View File

@@ -27,6 +27,11 @@ import java.net.NetworkInterface
@TargetApi(26)
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),
View.OnClickListener {
init {
@@ -39,10 +44,8 @@ class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager()
val binder = manager.binder
if (binder?.iface != null) binder.stop() else {
val context = manager.parent.requireContext()
if (context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) !=
PackageManager.PERMISSION_GRANTED) {
manager.parent.requestPermissions(arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
TetheringFragment.START_LOCAL_ONLY_HOTSPOT)
if (context.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
manager.parent.requestPermissions(arrayOf(permission), TetheringFragment.START_LOCAL_ONLY_HOTSPOT)
return
}
/**

View File

@@ -1,10 +1,13 @@
package be.mygod.vpnhotspot.manage
import android.Manifest
import android.content.ComponentName
import android.content.DialogInterface
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.net.wifi.WifiConfiguration
import android.net.wifi.p2p.WifiP2pConfig
import android.net.wifi.p2p.WifiP2pGroup
import android.os.Build
import android.os.Bundle
@@ -50,6 +53,12 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
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() {
return try {
NetworkInterface.getByName(p2pInterface ?: return "")?.formatAddresses() ?: ""
@@ -65,6 +74,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
}
fun onGroupChanged(group: WifiP2pGroup? = null) {
p2pInterface = group?.`interface`
if (Build.VERSION.SDK_INT >= 29) notifyPropertyChanged(BR.title)
notifyPropertyChanged(BR.addresses)
}
@@ -73,6 +83,12 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
when (binder?.service?.status) {
RepeaterService.Status.IDLE -> {
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))
}
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() {
var config: P2pSupplicantConfiguration? = null
}
@@ -115,6 +133,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
private val data = Data()
internal var binder: RepeaterService.Binder? = null
private var p2pInterface: String? = null
@Suppress("DEPRECATION")
private val holder = ViewModelProviders.of(parent).get<ConfigHolder>()
override fun bindTo(viewHolder: RecyclerView.ViewHolder) {
@@ -144,25 +163,51 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
}
val configuration: WifiConfiguration? get() {
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
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
}
}
} catch (e: RuntimeException) {
Timber.w(e)
} else @Suppress("DEPRECATION") {
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()
return null
}
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 {
master.update(config.SSID, config.preSharedKey)
binder!!.group = null

View File

@@ -12,7 +12,7 @@ abstract class TetherListeningTileService : KillableTileService() {
protected var tethered: List<String>? = null
private val receiver = broadcastReceiver { _, intent ->
tethered = intent.tetheredIfaces
tethered = intent.tetheredIfaces ?: return@broadcastReceiver
updateTile()
}

View File

@@ -39,6 +39,7 @@ import java.net.SocketException
class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickListener {
companion object {
const val START_REPEATER = 4
const val START_LOCAL_ONLY_HOTSPOT = 1
const val REPEATER_WPS = 3
const val CONFIGURE_REPEATER = 2
@@ -98,8 +99,9 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
var binder: TetheringService.Binder? = null
private val adapter = ManagerAdapter()
private val receiver = broadcastReceiver { _, intent ->
adapter.update(intent.tetheredIfaces, intent.localOnlyTetheredIfaces,
intent.getStringArrayListExtra(TetheringManager.EXTRA_ERRORED_TETHER))
adapter.update(intent.tetheredIfaces ?: return@broadcastReceiver,
intent.localOnlyTetheredIfaces ?: return@broadcastReceiver,
intent.getStringArrayListExtra(TetheringManager.EXTRA_ERRORED_TETHER) ?: return@broadcastReceiver)
}
private fun updateMonitorList(canMonitor: List<String> = emptyList()) {
@@ -189,18 +191,27 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
WifiApManager.configuration = configuration
} catch (e: IllegalArgumentException) {
SmartSnackbar.make(R.string.configuration_rejected).show()
} catch (e: InvocationTargetException) {
SmartSnackbar.make(e.targetException).show()
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (requestCode == START_LOCAL_ONLY_HOTSPOT) @TargetApi(26) {
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
when (requestCode) {
START_REPEATER -> if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) @TargetApi(29) {
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?) {

View File

@@ -1,15 +1,14 @@
package be.mygod.vpnhotspot.net
import android.os.Build
import android.system.ErrnoException
import android.system.OsConstants
import androidx.core.os.BuildCompat
import be.mygod.vpnhotspot.room.macToLong
import be.mygod.vpnhotspot.util.parseNumericAddress
import timber.log.Timber
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.lang.NumberFormatException
import java.net.InetAddress
import java.net.NetworkInterface
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() }
.toList()
} 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)
}
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) {
val toRemove = HashSet(clients.keys)
for (neighbour in neighbours) {
if (neighbour.dev != downstream || neighbour.ip !is Inet4Address ||
runBlocking { AppDatabase.instance.clientRecordDao.lookup(neighbour.lladdr) }
?.blocked == true) continue
if (neighbour.dev != downstream || neighbour.ip !is Inet4Address || runBlocking {
AppDatabase.instance.clientRecordDao.lookupOrDefault(neighbour.lladdr)
}.blocked) continue
toRemove.remove(neighbour.ip)
try {
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/
*/
@Suppress("DEPRECATION")
@Deprecated("Not usable since API 26, malfunctioning on API 25")
fun start(wifiConfig: WifiConfiguration? = null) {
app.wifi.isWifiEnabled = false
app.wifi.setWifiApEnabled(wifiConfig, true)
}
@Suppress("DEPRECATION")
@Deprecated("Not usable since API 26")
fun stop() {
app.wifi.setWifiApEnabled(null, false)

View File

@@ -3,8 +3,15 @@ package be.mygod.vpnhotspot.net.wifi
import android.annotation.SuppressLint
import android.content.SharedPreferences
import android.net.wifi.WifiManager
import android.os.Build
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.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import be.mygod.vpnhotspot.App.Companion.app
/**
@@ -13,8 +20,12 @@ import be.mygod.vpnhotspot.App.Companion.app
class WifiDoubleLock(lockType: Int) : AutoCloseable {
companion object : SharedPreferences.OnSharedPreferenceChangeListener {
private const val KEY = "service.wifiLock"
private val lockType get() =
WifiDoubleLock.Mode.valueOf(app.pref.getString(KEY, WifiDoubleLock.Mode.Full.toString()) ?: "").lockType
var mode: Mode
@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 var holders = mutableSetOf<Any>()
@@ -23,7 +34,7 @@ class WifiDoubleLock(lockType: Int) : AutoCloseable {
fun acquire(holder: Any) = synchronized(this) {
if (holders.isEmpty()) {
app.pref.registerOnSharedPreferenceChangeListener(this)
val lockType = lockType
val lockType = mode.lockType
if (lockType != null) lock = WifiDoubleLock(lockType)
}
check(holders.add(holder))
@@ -40,14 +51,44 @@ class WifiDoubleLock(lockType: Int) : AutoCloseable {
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (key == KEY) synchronized(this) {
lock?.close()
val lockType = lockType
val lockType = mode.lockType
lock = if (lockType == null) null else WifiDoubleLock(lockType)
}
}
}
enum class Mode(val lockType: Int? = null) {
None, Full(WifiManager.WIFI_MODE_FULL), HighPerf(WifiManager.WIFI_MODE_FULL_HIGH_PERF)
enum class Mode(val lockType: Int? = null, val keepScreenOn: Boolean = false) {
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() }

View File

@@ -12,6 +12,7 @@ import java.lang.reflect.Proxy
object WifiP2pManagerHelper {
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"
/**
@@ -24,6 +25,7 @@ object WifiP2pManagerHelper {
WifiP2pManager::class.java.getDeclaredMethod("setWifiP2pChannels", WifiP2pManager.Channel::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,
listener: WifiP2pManager.ActionListener) {
try {
@@ -62,6 +64,7 @@ object WifiP2pManagerHelper {
WifiP2pManager::class.java.getDeclaredMethod("deletePersistentGroup",
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,
listener: WifiP2pManager.ActionListener) {
try {
@@ -88,6 +91,7 @@ object WifiP2pManagerHelper {
* @param c is the channel created at {@link #initialize}
* @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,
listener: (Collection<WifiP2pGroup>) -> Unit) {
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
*/
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
}

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/+/6fa46df/wpa_supplicant/config_file.c#182
*/
@Deprecated("No longer used since API 29")
class P2pSupplicantConfiguration(private val group: WifiP2pGroup, ownerAddress: String?) {
companion object {
private const val TAG = "P2pSupplicantConfiguration"

View File

@@ -26,7 +26,6 @@ import be.mygod.vpnhotspot.util.toParcelable
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.dialog_wifi_ap.view.*
import java.lang.IllegalStateException
import java.nio.charset.Charset
/**
@@ -114,7 +113,13 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
if (!arg.readOnly) dialogView.password.addTextChangedListener(this@WifiApDialogFragment)
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) dialogView.band.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)
add(BandOption.Band2GHz)
add(BandOption.Band5GHz)
@@ -172,8 +177,8 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
override fun onMenuItemClick(item: MenuItem?): Boolean {
return when (item?.itemId) {
android.R.id.copy -> {
app.clipboard.primaryClip = ClipData.newPlainText(null,
Base64.encodeToString(ret.configuration.toByteArray(), BASE64_FLAGS))
app.clipboard.setPrimaryClip(ClipData.newPlainText(null,
Base64.encodeToString(ret.configuration.toByteArray(), BASE64_FLAGS)))
true
}
android.R.id.paste -> try {
@@ -187,8 +192,13 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
false
}
R.id.share_qr -> {
QRCodeDialog().withArg(ret.configuration.toQRString())
.show(fragmentManager ?: return false, "QRCodeDialog")
val qrString = try {
ret.configuration.toQRString()
} catch (e: IllegalArgumentException) {
SmartSnackbar.make(e).show()
return false
}
QRCodeDialog().withArg(qrString).show(fragmentManager ?: return false, "QRCodeDialog")
true
}
else -> false

View File

@@ -64,10 +64,15 @@ fun channelToFrequency(channel: Int) = when (channel) {
in 15..165 -> 5000 + 5 * 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 ->
check(selected >= 0) { "No key management selected" }
val WifiConfiguration.apKeyManagement get() = allowedKeyManagement.nextSetBit(0).let { selected ->
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("([\\\\\":;,])")

View File

@@ -1,6 +1,7 @@
package be.mygod.vpnhotspot.room
import androidx.lifecycle.LiveData
import androidx.lifecycle.map
import androidx.room.*
@Entity
@@ -12,12 +13,12 @@ data class ClientRecord(@PrimaryKey
@androidx.room.Dao
abstract class Dao {
@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)
@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)
protected abstract suspend fun updateInternal(value: ClientRecord): Long

View File

@@ -57,7 +57,7 @@ object SpanFormatter {
i = m.start()
val exprEnd = m.end()
val argTerm = m.group(1)
val argTerm = m.group(1)!!
val modTerm = m.group(2)
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.content.*
import android.net.InetAddresses
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
@@ -93,12 +94,13 @@ fun NetworkInterface.formatAddresses(macOnly: Boolean = false) = SpannableString
}
}.trimEnd()
private val parseNumericAddress by lazy {
private val parseNumericAddress by lazy @SuppressLint("SoonBlockedPrivateApi") {
InetAddress::class.java.getDeclaredMethod("parseNumericAddress", String::class.java).apply {
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) {
if (app.hasTouch) try {

View File

@@ -41,7 +41,7 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/title_repeater"
android:text="@{data.title}"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/>
<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_settings">设置选项</string>
<string name="repeater_channel">无线中继 (%1$d MHz, 频道 %2$d)</string>
<string name="repeater_wps">WPS不安全</string>
<string name="repeater_wps_dialog_title">输入 PIN</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_full"></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_monitor">Netlink 监听</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_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_dialog_title">Enter PIN</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_full">On</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_monitor">Netlink monitor</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:entries="@array/settings_service_wifi_lock"
app:entryValues="@array/settings_service_wifi_lock_values"
app:defaultValue="Full"
app:title="@string/settings_service_wifi_lock"
app:useSimpleSummaryProvider="true"/>
<SwitchPreference