Draft for supporting using system configuration for temporary hotspot
Attempt at addressing #166.
This commit is contained in:
@@ -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 30) `Landroid/net/wifi/WifiManager;->setSoftApConfiguration(Landroid/net/wifi/SoftApConfiguration;)Z,sdk,system-api,test-api`
|
||||
* (prior to API 30) `Landroid/net/wifi/WifiManager;->setWifiApConfiguration(Landroid/net/wifi/WifiConfiguration;)Z,sdk,system-api,test-api`
|
||||
* (since API 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`
|
||||
* `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`
|
||||
|
||||
@@ -5,14 +5,20 @@ import android.content.IntentFilter
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
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.TetherType
|
||||
import be.mygod.vpnhotspot.net.TetheringManager
|
||||
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
|
||||
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
||||
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.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.StickyEvent1
|
||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||
@@ -23,6 +29,10 @@ import java.net.Inet4Address
|
||||
|
||||
@RequiresApi(26)
|
||||
class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
||||
companion object {
|
||||
const val KEY_USE_SYSTEM = "service.tempHotspot.useSystem"
|
||||
}
|
||||
|
||||
inner class Binder : android.os.Binder() {
|
||||
/**
|
||||
* null represents IDLE, "" represents CONNECTING, "something" represents CONNECTED.
|
||||
@@ -33,10 +43,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
||||
ifaceChanged(value)
|
||||
}
|
||||
val ifaceChanged = StickyEvent1 { iface }
|
||||
|
||||
val configuration get() = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
|
||||
reservation?.wifiConfiguration?.toCompat()
|
||||
} else reservation?.softApConfiguration?.toCompat()
|
||||
val configuration get() = reservation?.configuration
|
||||
|
||||
fun stop() {
|
||||
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 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.
|
||||
*/
|
||||
@@ -82,40 +146,35 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
||||
if (binder.iface != null) return START_STICKY
|
||||
binder.iface = ""
|
||||
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 {
|
||||
Services.wifi.startLocalOnlyHotspot(object : WifiManager.LocalOnlyHotspotCallback() {
|
||||
override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) {
|
||||
if (reservation == null) onFailed(-2) else {
|
||||
this@LocalOnlyHotspotService.reservation = 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
|
||||
}
|
||||
this@LocalOnlyHotspotService.reservation = Framework(reservation)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStopped() {
|
||||
Timber.d("LOHCallback.onStopped")
|
||||
reservation = null
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
override fun onFailed(reason: Int) = onFrameworkFailed(reason)
|
||||
}, null)
|
||||
} catch (e: IllegalStateException) {
|
||||
// throws IllegalStateException if the caller attempts to start the LocalOnlyHotspot while they
|
||||
@@ -128,7 +187,6 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
||||
SmartSnackbar.make(e).show()
|
||||
stopService()
|
||||
}
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onIpNeighbourAvailable(neighbours: Collection<IpNeighbour>) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package be.mygod.vpnhotspot
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -38,6 +39,8 @@ import java.io.PrintWriter
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
||||
private fun Preference.remove() = parent!!.removePreference(this)
|
||||
@TargetApi(26)
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
// handle complicated default value and possible system upgrades
|
||||
WifiDoubleLock.mode = WifiDoubleLock.mode
|
||||
@@ -65,7 +68,7 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
false
|
||||
}
|
||||
} else parent!!.removePreference(this)
|
||||
} else remove()
|
||||
}
|
||||
val boot = findPreference<SwitchPreference>("service.repeater.startOnBoot")!!
|
||||
if (Services.p2p != null) {
|
||||
@@ -74,11 +77,12 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
||||
true
|
||||
}
|
||||
boot.isChecked = BootReceiver.enabled
|
||||
} else boot.parent!!.removePreference(boot)
|
||||
} else boot.remove()
|
||||
if (Services.p2p == null || !RepeaterService.safeModeConfigurable) {
|
||||
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 {
|
||||
GlobalScope.launch { RoutingManager.clean() }
|
||||
true
|
||||
|
||||
@@ -191,7 +191,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
||||
apConfigurationRunning = true
|
||||
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
|
||||
try {
|
||||
WifiApManager.configuration
|
||||
WifiApManager.configurationCompat
|
||||
} catch (e: InvocationTargetException) {
|
||||
if (e.targetException !is SecurityException) Timber.w(e)
|
||||
try {
|
||||
@@ -237,7 +237,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
||||
SmartSnackbar.make(e).show()
|
||||
}
|
||||
val success = try {
|
||||
WifiApManager.setConfiguration(configuration)
|
||||
WifiApManager.setConfigurationCompat(configuration)
|
||||
} catch (e: InvocationTargetException) {
|
||||
try {
|
||||
RootManager.use { it.execute(WifiApCommands.SetConfiguration(configuration)) }
|
||||
|
||||
@@ -72,16 +72,20 @@ object WifiApManager {
|
||||
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-.
|
||||
*/
|
||||
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()
|
||||
?: SoftApConfigurationCompat()
|
||||
} else (getSoftApConfiguration(Services.wifi) as SoftApConfiguration).toCompat()
|
||||
fun setConfiguration(value: SoftApConfigurationCompat) = (if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
|
||||
} else configuration.toCompat()
|
||||
fun setConfigurationCompat(value: SoftApConfigurationCompat) = (if (Build.VERSION.SDK_INT >= 30) {
|
||||
setSoftApConfiguration(Services.wifi, value.toPlatform())
|
||||
} else @Suppress("DEPRECATION") {
|
||||
setWifiApConfiguration(Services.wifi, value.toWifiConfiguration())
|
||||
} else setSoftApConfiguration(Services.wifi, value.toPlatform())) as Boolean
|
||||
}) as Boolean
|
||||
|
||||
@RequiresApi(28)
|
||||
interface SoftApCallbackCompat {
|
||||
@@ -236,6 +240,16 @@ object WifiApManager {
|
||||
@RequiresApi(28)
|
||||
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 {
|
||||
WifiManager::class.java.getDeclaredMethod("cancelLocalOnlyHotspotRequest")
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package be.mygod.vpnhotspot.root
|
||||
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.RequiresApi
|
||||
import be.mygod.librootkotlinx.ParcelableBoolean
|
||||
@@ -62,6 +63,7 @@ object WifiApCommands {
|
||||
private fun push(parcel: SoftApCallbackParcel) {
|
||||
trySend(parcel).onClosed {
|
||||
finish.completeExceptionally(it ?: ClosedSendChannelException("Channel was closed normally"))
|
||||
return
|
||||
}.onFailure { throw it!! }
|
||||
}
|
||||
|
||||
@@ -149,11 +151,60 @@ object WifiApCommands {
|
||||
|
||||
@Parcelize
|
||||
class GetConfiguration : RootCommand<SoftApConfigurationCompat> {
|
||||
override suspend fun execute() = WifiApManager.configuration
|
||||
override suspend fun execute() = WifiApManager.configurationCompat
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5
mobile/src/main/res/drawable/ic_content_file_copy.xml
Normal file
5
mobile/src/main/res/drawable/ic_content_file_copy.xml
Normal 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>
|
||||
@@ -113,6 +113,8 @@
|
||||
<string name="settings_service_repeater_safe_mode">中继安全模式</string>
|
||||
<string name="settings_service_repeater_safe_mode_summary">不对系统配置进行修改,但是可能须要较长的网络名称。</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_none">系统默认</string>
|
||||
<string name="settings_service_wifi_lock_full">开</string>
|
||||
|
||||
@@ -131,6 +131,9 @@
|
||||
not work with short network names.</string>
|
||||
<string name="settings_service_repeater_safe_mode_warning">Short network names might require turning off safe
|
||||
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_none">System default</string>
|
||||
<string name="settings_service_wifi_lock_full">On</string>
|
||||
|
||||
@@ -68,6 +68,11 @@
|
||||
app:title="@string/settings_service_repeater_safe_mode"
|
||||
app:summary="@string/settings_service_repeater_safe_mode_summary"
|
||||
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
|
||||
app:key="service.ipMonitor"
|
||||
app:icon="@drawable/ic_hardware_device_hub"
|
||||
|
||||
Reference in New Issue
Block a user