Draft for supporting using system configuration for temporary hotspot

Attempt at addressing #166.
This commit is contained in:
Mygod
2021-05-31 01:21:40 -04:00
parent 229b190c22
commit bc25cdb0bb
11 changed files with 204 additions and 42 deletions

View File

@@ -302,6 +302,7 @@ Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded
* (since API 28) `Landroid/net/wifi/WifiManager;->registerSoftApCallback(Ljava/util/concurrent/Executor;Landroid/net/wifi/WifiManager$SoftApCallback;)V,sdk,system-api,test-api` * (since API 28) `Landroid/net/wifi/WifiManager;->registerSoftApCallback(Ljava/util/concurrent/Executor;Landroid/net/wifi/WifiManager$SoftApCallback;)V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/WifiManager;->setSoftApConfiguration(Landroid/net/wifi/SoftApConfiguration;)Z,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/WifiManager;->setSoftApConfiguration(Landroid/net/wifi/SoftApConfiguration;)Z,sdk,system-api,test-api`
* (prior to API 30) `Landroid/net/wifi/WifiManager;->setWifiApConfiguration(Landroid/net/wifi/WifiConfiguration;)Z,sdk,system-api,test-api` * (prior to API 30) `Landroid/net/wifi/WifiManager;->setWifiApConfiguration(Landroid/net/wifi/WifiConfiguration;)Z,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/WifiManager;->startLocalOnlyHotspot(Landroid/net/wifi/SoftApConfiguration;Ljava/util/concurrent/Executor;Landroid/net/wifi/WifiManager$LocalOnlyHotspotCallback;)V,sdk,system-api,test-api`
* (since API 28) `Landroid/net/wifi/WifiManager;->unregisterSoftApCallback(Landroid/net/wifi/WifiManager$SoftApCallback;)V,sdk,system-api,test-api` * (since API 28) `Landroid/net/wifi/WifiManager;->unregisterSoftApCallback(Landroid/net/wifi/WifiManager$SoftApCallback;)V,sdk,system-api,test-api`
* `Landroid/net/wifi/p2p/WifiP2pGroupList;->getGroupList()Ljava/util/List;,sdk,system-api,test-api` * `Landroid/net/wifi/p2p/WifiP2pGroupList;->getGroupList()Ljava/util/List;,sdk,system-api,test-api`
* `Landroid/net/wifi/p2p/WifiP2pManager$PersistentGroupInfoListener;->onPersistentGroupInfoAvailable(Landroid/net/wifi/p2p/WifiP2pGroupList;)V,sdk,system-api,test-api` * `Landroid/net/wifi/p2p/WifiP2pManager$PersistentGroupInfoListener;->onPersistentGroupInfoAvailable(Landroid/net/wifi/p2p/WifiP2pGroupList;)V,sdk,system-api,test-api`

View File

@@ -5,14 +5,20 @@ import android.content.IntentFilter
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import be.mygod.librootkotlinx.RootServer
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.IpNeighbour import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
import be.mygod.vpnhotspot.net.wifi.WifiApManager import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.root.LocalOnlyHotspotCallbacks
import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.root.WifiApCommands
import be.mygod.vpnhotspot.util.Services import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.StickyEvent1 import be.mygod.vpnhotspot.util.StickyEvent1
import be.mygod.vpnhotspot.util.broadcastReceiver import be.mygod.vpnhotspot.util.broadcastReceiver
@@ -23,6 +29,10 @@ import java.net.Inet4Address
@RequiresApi(26) @RequiresApi(26)
class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope { class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
companion object {
const val KEY_USE_SYSTEM = "service.tempHotspot.useSystem"
}
inner class Binder : android.os.Binder() { inner class Binder : android.os.Binder() {
/** /**
* null represents IDLE, "" represents CONNECTING, "something" represents CONNECTED. * null represents IDLE, "" represents CONNECTING, "something" represents CONNECTED.
@@ -33,10 +43,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
ifaceChanged(value) ifaceChanged(value)
} }
val ifaceChanged = StickyEvent1 { iface } val ifaceChanged = StickyEvent1 { iface }
val configuration get() = reservation?.configuration
val configuration get() = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
reservation?.wifiConfiguration?.toCompat()
} else reservation?.softApConfiguration?.toCompat()
fun stop() { fun stop() {
when (iface) { when (iface) {
@@ -47,8 +54,65 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
} }
} }
interface Reservation : AutoCloseable {
val configuration: SoftApConfigurationCompat?
}
class Framework(private val reservation: WifiManager.LocalOnlyHotspotReservation) : Reservation {
override val configuration get() = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
reservation.wifiConfiguration?.toCompat()
} else reservation.softApConfiguration.toCompat()
override fun close() = reservation.close()
}
@RequiresApi(30)
inner class Root(rootServer: RootServer) : Reservation {
private val channel = rootServer.create(WifiApCommands.StartLocalOnlyHotspot(), this@LocalOnlyHotspotService)
override var configuration: SoftApConfigurationCompat? = null
private set
override fun close() = channel.cancel()
suspend fun work() {
for (callback in channel) when (callback) {
is LocalOnlyHotspotCallbacks.OnStarted -> configuration = callback.config.toCompat()
is LocalOnlyHotspotCallbacks.OnStopped -> reservation = null
is LocalOnlyHotspotCallbacks.OnFailed -> onFrameworkFailed(callback.reason)
}
}
}
private val binder = Binder() private val binder = Binder()
private var reservation: WifiManager.LocalOnlyHotspotReservation? = null private var reservation: Reservation? = null
set(value) {
field = value
if (value != null && !receiverRegistered) {
val configuration = binder.configuration
if (Build.VERSION.SDK_INT < 30 && configuration!!.isAutoShutdownEnabled) {
timeoutMonitor = TetherTimeoutMonitor(configuration.shutdownTimeoutMillis, coroutineContext) {
value.close()
}
}
registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
receiverRegistered = true
}
}
private fun onFrameworkFailed(reason: Int) {
SmartSnackbar.make(getString(R.string.tethering_temp_hotspot_failure, when (reason) {
WifiManager.LocalOnlyHotspotCallback.ERROR_NO_CHANNEL -> {
getString(R.string.tethering_temp_hotspot_failure_no_channel)
}
WifiManager.LocalOnlyHotspotCallback.ERROR_GENERIC -> {
getString(R.string.tethering_temp_hotspot_failure_generic)
}
WifiManager.LocalOnlyHotspotCallback.ERROR_INCOMPATIBLE_MODE -> {
getString(R.string.tethering_temp_hotspot_failure_incompatible_mode)
}
WifiManager.LocalOnlyHotspotCallback.ERROR_TETHERING_DISALLOWED -> {
getString(R.string.tethering_temp_hotspot_failure_tethering_disallowed)
}
else -> getString(R.string.failure_reason_unknown, reason)
})).show()
stopService()
}
/** /**
* Writes and critical reads to routingManager should be protected with this context. * Writes and critical reads to routingManager should be protected with this context.
*/ */
@@ -82,40 +146,35 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
if (binder.iface != null) return START_STICKY if (binder.iface != null) return START_STICKY
binder.iface = "" binder.iface = ""
updateNotification() // show invisible foreground notification to avoid being killed updateNotification() // show invisible foreground notification to avoid being killed
launch(start = CoroutineStart.UNDISPATCHED) { doStart() }
return START_STICKY
}
private suspend fun doStart() {
if (Build.VERSION.SDK_INT >= 30 && app.pref.getBoolean(KEY_USE_SYSTEM, false)) try {
RootManager.use {
Root(it).apply {
reservation = this
work()
}
}
return
} catch (_: CancellationException) {
return
} catch (e: Exception) {
Timber.w(e)
SmartSnackbar.make(e).show()
}
try { try {
Services.wifi.startLocalOnlyHotspot(object : WifiManager.LocalOnlyHotspotCallback() { Services.wifi.startLocalOnlyHotspot(object : WifiManager.LocalOnlyHotspotCallback() {
override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) { override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) {
if (reservation == null) onFailed(-2) else { if (reservation == null) onFailed(-2) else {
this@LocalOnlyHotspotService.reservation = reservation this@LocalOnlyHotspotService.reservation = Framework(reservation)
if (!receiverRegistered) {
val configuration = binder.configuration!!
if (Build.VERSION.SDK_INT < 30 && configuration.isAutoShutdownEnabled) {
timeoutMonitor = TetherTimeoutMonitor(configuration.shutdownTimeoutMillis,
coroutineContext) { reservation.close() }
}
registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
receiverRegistered = true
}
} }
} }
override fun onStopped() { override fun onStopped() {
Timber.d("LOHCallback.onStopped")
reservation = null reservation = null
} }
override fun onFailed(reason: Int) = onFrameworkFailed(reason)
override fun onFailed(reason: Int) {
SmartSnackbar.make(getString(R.string.tethering_temp_hotspot_failure, when (reason) {
ERROR_NO_CHANNEL -> getString(R.string.tethering_temp_hotspot_failure_no_channel)
ERROR_GENERIC -> getString(R.string.tethering_temp_hotspot_failure_generic)
ERROR_INCOMPATIBLE_MODE -> getString(R.string.tethering_temp_hotspot_failure_incompatible_mode)
ERROR_TETHERING_DISALLOWED -> {
getString(R.string.tethering_temp_hotspot_failure_tethering_disallowed)
}
else -> getString(R.string.failure_reason_unknown, reason)
})).show()
stopService()
}
}, null) }, null)
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
// throws IllegalStateException if the caller attempts to start the LocalOnlyHotspot while they // throws IllegalStateException if the caller attempts to start the LocalOnlyHotspot while they
@@ -128,7 +187,6 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
SmartSnackbar.make(e).show() SmartSnackbar.make(e).show()
stopService() stopService()
} }
return START_STICKY
} }
override fun onIpNeighbourAvailable(neighbours: Collection<IpNeighbour>) { override fun onIpNeighbourAvailable(neighbours: Collection<IpNeighbour>) {

View File

@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot package be.mygod.vpnhotspot
import android.annotation.TargetApi
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -38,6 +39,8 @@ import java.io.PrintWriter
import kotlin.system.exitProcess import kotlin.system.exitProcess
class SettingsPreferenceFragment : PreferenceFragmentCompat() { class SettingsPreferenceFragment : PreferenceFragmentCompat() {
private fun Preference.remove() = parent!!.removePreference(this)
@TargetApi(26)
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
// handle complicated default value and possible system upgrades // handle complicated default value and possible system upgrades
WifiDoubleLock.mode = WifiDoubleLock.mode WifiDoubleLock.mode = WifiDoubleLock.mode
@@ -65,7 +68,7 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
} }
false false
} }
} else parent!!.removePreference(this) } else remove()
} }
val boot = findPreference<SwitchPreference>("service.repeater.startOnBoot")!! val boot = findPreference<SwitchPreference>("service.repeater.startOnBoot")!!
if (Services.p2p != null) { if (Services.p2p != null) {
@@ -74,11 +77,12 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
true true
} }
boot.isChecked = BootReceiver.enabled boot.isChecked = BootReceiver.enabled
} else boot.parent!!.removePreference(boot) } else boot.remove()
if (Services.p2p == null || !RepeaterService.safeModeConfigurable) { if (Services.p2p == null || !RepeaterService.safeModeConfigurable) {
val safeMode = findPreference<Preference>(RepeaterService.KEY_SAFE_MODE)!! val safeMode = findPreference<Preference>(RepeaterService.KEY_SAFE_MODE)!!
safeMode.parent!!.removePreference(safeMode) safeMode.remove()
} }
if (Build.VERSION.SDK_INT < 30) findPreference<Preference>(LocalOnlyHotspotService.KEY_USE_SYSTEM)!!.remove()
findPreference<Preference>("service.clean")!!.setOnPreferenceClickListener { findPreference<Preference>("service.clean")!!.setOnPreferenceClickListener {
GlobalScope.launch { RoutingManager.clean() } GlobalScope.launch { RoutingManager.clean() }
true true

View File

@@ -191,7 +191,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
apConfigurationRunning = true apConfigurationRunning = true
viewLifecycleOwner.lifecycleScope.launchWhenCreated { viewLifecycleOwner.lifecycleScope.launchWhenCreated {
try { try {
WifiApManager.configuration WifiApManager.configurationCompat
} catch (e: InvocationTargetException) { } catch (e: InvocationTargetException) {
if (e.targetException !is SecurityException) Timber.w(e) if (e.targetException !is SecurityException) Timber.w(e)
try { try {
@@ -237,7 +237,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
SmartSnackbar.make(e).show() SmartSnackbar.make(e).show()
} }
val success = try { val success = try {
WifiApManager.setConfiguration(configuration) WifiApManager.setConfigurationCompat(configuration)
} catch (e: InvocationTargetException) { } catch (e: InvocationTargetException) {
try { try {
RootManager.use { it.execute(WifiApCommands.SetConfiguration(configuration)) } RootManager.use { it.execute(WifiApCommands.SetConfiguration(configuration)) }

View File

@@ -72,16 +72,20 @@ object WifiApManager {
WifiManager::class.java.getDeclaredMethod("setSoftApConfiguration", SoftApConfiguration::class.java) WifiManager::class.java.getDeclaredMethod("setSoftApConfiguration", SoftApConfiguration::class.java)
} }
@get:RequiresApi(30)
val configuration get() = getSoftApConfiguration(Services.wifi) as SoftApConfiguration
/** /**
* Requires NETWORK_SETTINGS permission (or root) on API 30+, and OVERRIDE_WIFI_CONFIG on API 29-. * Requires NETWORK_SETTINGS permission (or root) on API 30+, and OVERRIDE_WIFI_CONFIG on API 29-.
*/ */
val configuration get() = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") { val configurationCompat get() = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
(getWifiApConfiguration(Services.wifi) as android.net.wifi.WifiConfiguration?)?.toCompat() (getWifiApConfiguration(Services.wifi) as android.net.wifi.WifiConfiguration?)?.toCompat()
?: SoftApConfigurationCompat() ?: SoftApConfigurationCompat()
} else (getSoftApConfiguration(Services.wifi) as SoftApConfiguration).toCompat() } else configuration.toCompat()
fun setConfiguration(value: SoftApConfigurationCompat) = (if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") { fun setConfigurationCompat(value: SoftApConfigurationCompat) = (if (Build.VERSION.SDK_INT >= 30) {
setSoftApConfiguration(Services.wifi, value.toPlatform())
} else @Suppress("DEPRECATION") {
setWifiApConfiguration(Services.wifi, value.toWifiConfiguration()) setWifiApConfiguration(Services.wifi, value.toWifiConfiguration())
} else setSoftApConfiguration(Services.wifi, value.toPlatform())) as Boolean }) as Boolean
@RequiresApi(28) @RequiresApi(28)
interface SoftApCallbackCompat { interface SoftApCallbackCompat {
@@ -236,6 +240,16 @@ object WifiApManager {
@RequiresApi(28) @RequiresApi(28)
fun unregisterSoftApCallback(key: Any) = unregisterSoftApCallback(Services.wifi, key) fun unregisterSoftApCallback(key: Any) = unregisterSoftApCallback(Services.wifi, key)
@get:RequiresApi(30)
private val startLocalOnlyHotspot by lazy @TargetApi(30) {
WifiManager::class.java.getDeclaredMethod("startLocalOnlyHotspot", SoftApConfiguration::class.java,
Executor::class.java, WifiManager.LocalOnlyHotspotCallback::class.java)
}
@RequiresApi(30)
fun startLocalOnlyHotspot(config: SoftApConfiguration, callback: WifiManager.LocalOnlyHotspotCallback?,
executor: Executor? = null) =
startLocalOnlyHotspot(Services.wifi, config, executor, callback)
private val cancelLocalOnlyHotspotRequest by lazy { private val cancelLocalOnlyHotspotRequest by lazy {
WifiManager::class.java.getDeclaredMethod("cancelLocalOnlyHotspotRequest") WifiManager::class.java.getDeclaredMethod("cancelLocalOnlyHotspotRequest")
} }

View File

@@ -0,0 +1,19 @@
package be.mygod.vpnhotspot.root
import android.net.wifi.SoftApConfiguration
import android.os.Parcelable
import androidx.annotation.RequiresApi
import kotlinx.parcelize.Parcelize
@RequiresApi(30)
sealed class LocalOnlyHotspotCallbacks : Parcelable {
@Parcelize
data class OnStarted(val config: SoftApConfiguration) : LocalOnlyHotspotCallbacks()
@Parcelize
class OnStopped : LocalOnlyHotspotCallbacks() {
override fun equals(other: Any?) = other is OnStopped
override fun hashCode() = 0x80acd3ca.toInt()
}
@Parcelize
data class OnFailed(val reason: Int) : LocalOnlyHotspotCallbacks()
}

View File

@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot.root package be.mygod.vpnhotspot.root
import android.net.wifi.WifiManager
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import be.mygod.librootkotlinx.ParcelableBoolean import be.mygod.librootkotlinx.ParcelableBoolean
@@ -62,6 +63,7 @@ object WifiApCommands {
private fun push(parcel: SoftApCallbackParcel) { private fun push(parcel: SoftApCallbackParcel) {
trySend(parcel).onClosed { trySend(parcel).onClosed {
finish.completeExceptionally(it ?: ClosedSendChannelException("Channel was closed normally")) finish.completeExceptionally(it ?: ClosedSendChannelException("Channel was closed normally"))
return
}.onFailure { throw it!! } }.onFailure { throw it!! }
} }
@@ -149,11 +151,60 @@ object WifiApCommands {
@Parcelize @Parcelize
class GetConfiguration : RootCommand<SoftApConfigurationCompat> { class GetConfiguration : RootCommand<SoftApConfigurationCompat> {
override suspend fun execute() = WifiApManager.configuration override suspend fun execute() = WifiApManager.configurationCompat
} }
@Parcelize @Parcelize
data class SetConfiguration(val configuration: SoftApConfigurationCompat) : RootCommand<ParcelableBoolean> { data class SetConfiguration(val configuration: SoftApConfigurationCompat) : RootCommand<ParcelableBoolean> {
override suspend fun execute() = ParcelableBoolean(WifiApManager.setConfiguration(configuration)) override suspend fun execute() = ParcelableBoolean(WifiApManager.setConfigurationCompat(configuration))
}
@Parcelize
@RequiresApi(30)
class StartLocalOnlyHotspot : RootCommandChannel<LocalOnlyHotspotCallbacks> {
override fun create(scope: CoroutineScope) = scope.produce(capacity = capacity) {
val finish = CompletableDeferred<Unit>()
var lohr: WifiManager.LocalOnlyHotspotReservation? = null
WifiApManager.startLocalOnlyHotspot(WifiApManager.configuration, object :
WifiManager.LocalOnlyHotspotCallback() {
private fun push(parcel: LocalOnlyHotspotCallbacks) {
trySend(parcel).onClosed {
finish.completeExceptionally(it ?: ClosedSendChannelException("Channel was closed normally"))
return
}.onFailure { throw it!! }
}
override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) {
if (reservation == null) onFailed(-3) else {
require(lohr == null)
lohr = reservation
push(LocalOnlyHotspotCallbacks.OnStarted(reservation.softApConfiguration))
}
}
override fun onStopped() {
push(LocalOnlyHotspotCallbacks.OnStopped())
finish.complete(Unit)
}
override fun onFailed(reason: Int) {
push(LocalOnlyHotspotCallbacks.OnFailed(reason))
finish.complete(Unit)
}
}) {
scope.launch {
try {
it.run()
} catch (e: Throwable) {
finish.completeExceptionally(e)
}
}
}
try {
finish.await()
} catch (e: Exception) {
WifiApManager.cancelLocalOnlyHotspotRequest()
throw e
} finally {
lohr?.close()
}
}
} }
} }

View File

@@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="?attr/colorControlNormal" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM15,5l6,6v10c0,1.1 -0.9,2 -2,2L7.99,23C6.89,23 6,22.1 6,21l0.01,-14c0,-1.1 0.89,-2 1.99,-2h7zM14,12h5.5L14,6.5L14,12z"/>
</vector>

View File

@@ -113,6 +113,8 @@
<string name="settings_service_repeater_safe_mode">中继安全模式</string> <string name="settings_service_repeater_safe_mode">中继安全模式</string>
<string name="settings_service_repeater_safe_mode_summary">不对系统配置进行修改,但是可能须要较长的网络名称。</string> <string name="settings_service_repeater_safe_mode_summary">不对系统配置进行修改,但是可能须要较长的网络名称。</string>
<string name="settings_service_repeater_safe_mode_warning">使用短名称可能需要关闭安全模式。</string> <string name="settings_service_repeater_safe_mode_warning">使用短名称可能需要关闭安全模式。</string>
<string name="settings_service_temp_hotspot_use_system">临时 WLAN 热点使用系统配置</string>
<string name="settings_service_temp_hotspot_use_system_summary">这将与其他使用本地热点的应用冲突</string>
<string name="settings_service_wifi_lock">保持 Wi\u2011Fi 开启</string> <string name="settings_service_wifi_lock">保持 Wi\u2011Fi 开启</string>
<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>

View File

@@ -131,6 +131,9 @@
not work with short network names.</string> not work with short network names.</string>
<string name="settings_service_repeater_safe_mode_warning">Short network names might require turning off safe <string name="settings_service_repeater_safe_mode_warning">Short network names might require turning off safe
mode.</string> mode.</string>
<string name="settings_service_temp_hotspot_use_system">Use system configuration for temporary hotspot</string>
<string name="settings_service_temp_hotspot_use_system_summary">Will conflict with other apps using local only
hotspot</string>
<string name="settings_service_wifi_lock">Keep Wi\u2011Fi alive</string> <string name="settings_service_wifi_lock">Keep Wi\u2011Fi alive</string>
<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>

View File

@@ -68,6 +68,11 @@
app:title="@string/settings_service_repeater_safe_mode" app:title="@string/settings_service_repeater_safe_mode"
app:summary="@string/settings_service_repeater_safe_mode_summary" app:summary="@string/settings_service_repeater_safe_mode_summary"
app:defaultValue="true"/> app:defaultValue="true"/>
<SwitchPreference
app:key="service.tempHotspot.useSystem"
app:icon="@drawable/ic_content_file_copy"
app:title="@string/settings_service_temp_hotspot_use_system"
app:summary="@string/settings_service_temp_hotspot_use_system_summary"/>
<com.takisoft.preferencex.SimpleMenuPreference <com.takisoft.preferencex.SimpleMenuPreference
app:key="service.ipMonitor" app:key="service.ipMonitor"
app:icon="@drawable/ic_hardware_device_hub" app:icon="@drawable/ic_hardware_device_hub"