Support auto start services

Fixes #96.
This commit is contained in:
Mygod
2021-10-10 17:08:16 -04:00
parent 79422a05fd
commit aee1a45eba
13 changed files with 153 additions and 29 deletions

View File

@@ -235,6 +235,7 @@
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" /> <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter> </intent-filter>
</receiver> </receiver>

View File

@@ -48,6 +48,7 @@ class App : Application() {
// alternative to PreferenceManager.getDefaultSharedPreferencesName(this) // alternative to PreferenceManager.getDefaultSharedPreferencesName(this)
deviceStorage.moveSharedPreferencesFrom(this, PreferenceManager(this).sharedPreferencesName) deviceStorage.moveSharedPreferencesFrom(this, PreferenceManager(this).sharedPreferencesName)
deviceStorage.moveDatabaseFrom(this, AppDatabase.DB_NAME) deviceStorage.moveDatabaseFrom(this, AppDatabase.DB_NAME)
BootReceiver.migrateIfNecessary(this, deviceStorage)
} else deviceStorage = this } else deviceStorage = this
Services.init { this } Services.init { this }

View File

@@ -5,31 +5,112 @@ import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import androidx.core.content.ContextCompat import android.os.Parcelable
import androidx.annotation.RequiresApi
import be.mygod.librootkotlinx.toByteArray
import be.mygod.librootkotlinx.toParcelable
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.util.Services import kotlinx.parcelize.Parcelize
import timber.log.Timber
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.File
import java.io.FileNotFoundException
class BootReceiver : BroadcastReceiver() { class BootReceiver : BroadcastReceiver() {
companion object { companion object {
const val KEY = "service.autoStart"
private val componentName by lazy { ComponentName(app, BootReceiver::class.java) } private val componentName by lazy { ComponentName(app, BootReceiver::class.java) }
var enabled: Boolean private var enabled: Boolean
get() = app.packageManager.getComponentEnabledSetting(componentName) == get() = app.packageManager.getComponentEnabledSetting(componentName) ==
PackageManager.COMPONENT_ENABLED_STATE_ENABLED PackageManager.COMPONENT_ENABLED_STATE_ENABLED
set(value) = app.packageManager.setComponentEnabledSetting(componentName, set(value) = app.packageManager.setComponentEnabledSetting(componentName,
if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED
else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP) else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP)
fun onUserSettingUpdated(shouldStart: Boolean) {
enabled = shouldStart && try {
config
} catch (e: Exception) {
Timber.w(e)
null
}?.startables?.isEmpty() == false
}
private fun onConfigUpdated(isNotEmpty: Boolean) {
enabled = isNotEmpty && app.pref.getBoolean(KEY, false)
}
private var started = false private var started = false
private const val FILENAME = "bootconfig"
private val configFile by lazy { File(app.deviceStorage.noBackupFilesDir, FILENAME) }
private var config: Config?
get() = try {
DataInputStream(configFile.inputStream()).use { it.readBytes().toParcelable() }
} catch (_: FileNotFoundException) {
null
} }
set(value) = DataOutputStream(configFile.outputStream()).use { it.write(value.toByteArray()) }
fun add(key: String, value: Startable) = try {
synchronized(BootReceiver) {
val c = config ?: Config()
c.startables[key] = value
config = c
}
onConfigUpdated(true)
} catch (e: Exception) {
Timber.w(e)
}
fun delete(key: String) = try {
onConfigUpdated(synchronized(BootReceiver) {
val c = config ?: Config()
c.startables.remove(key)
config = c
c
}.startables.isNotEmpty())
} catch (e: Exception) {
Timber.w(e)
}
inline fun <reified T> add(value: Startable) = add(T::class.java.name, value)
inline fun <reified T> delete() = delete(T::class.java.name)
@RequiresApi(24)
fun migrateIfNecessary(old: Context, new: Context) {
val oldFile = File(old.noBackupFilesDir, FILENAME)
if (oldFile.canRead()) try {
val newFile = File(new.noBackupFilesDir, FILENAME)
if (!newFile.exists()) oldFile.copyTo(newFile)
if (!oldFile.delete()) oldFile.deleteOnExit()
} catch (e: Exception) {
Timber.w(e)
}
}
}
interface Startable : Parcelable {
fun start(context: Context)
}
@Parcelize
private data class Config(var startables: MutableMap<String, Startable> = mutableMapOf()) : Parcelable
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (started) return if (started) return
when (intent.action) { val isUpdate = when (intent.action) {
Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_LOCKED_BOOT_COMPLETED -> started = true Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_LOCKED_BOOT_COMPLETED -> false
Intent.ACTION_MY_PACKAGE_REPLACED -> true
else -> return else -> return
} }
if (Services.p2p != null) { started = true
ContextCompat.startForegroundService(context, Intent(context, RepeaterService::class.java)) val config = try {
synchronized(BootReceiver) { config }
} catch (e: Exception) {
Timber.w(e)
if (isUpdate) null else return
} }
if (config == null || config.startables.isEmpty()) {
enabled = false
} else for (startable in config.startables.values) startable.start(context)
} }
} }

View File

@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot package be.mygod.vpnhotspot
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
@@ -15,6 +16,7 @@ import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.StickyEvent1 import be.mygod.vpnhotspot.util.StickyEvent1
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
import java.net.Inet4Address import java.net.Inet4Address
@@ -45,6 +47,13 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
} }
} }
@Parcelize
class Starter : BootReceiver.Startable {
override fun start(context: Context) {
context.startForegroundService(Intent(context, LocalOnlyHotspotService::class.java))
}
}
private val binder = Binder() private val binder = Binder()
private var reservation: WifiManager.LocalOnlyHotspotReservation? = null private var reservation: WifiManager.LocalOnlyHotspotReservation? = null
/** /**
@@ -86,6 +95,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
return stopService() return stopService()
} }
binder.iface = iface binder.iface = iface
BootReceiver.add<LocalOnlyHotspotService>(Starter())
launch { launch {
check(routingManager == null) check(routingManager == null)
routingManager = RoutingManager.LocalOnly(this@LocalOnlyHotspotService, iface).apply { start() } routingManager = RoutingManager.LocalOnly(this@LocalOnlyHotspotService, iface).apply { start() }
@@ -140,6 +150,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
} }
private fun stopService() { private fun stopService() {
BootReceiver.delete<LocalOnlyHotspotService>()
binder.iface = null binder.iface = null
unregisterReceiver() unregisterReceiver()
ServiceNotification.stopForeground(this) ServiceNotification.stopForeground(this)

View File

@@ -3,6 +3,7 @@ package be.mygod.vpnhotspot
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.annotation.TargetApi import android.annotation.TargetApi
import android.app.Service import android.app.Service
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
@@ -14,6 +15,7 @@ import android.os.Looper
import android.provider.Settings import android.provider.Settings
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.content.edit import androidx.core.content.edit
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.MacAddressCompat import be.mygod.vpnhotspot.net.MacAddressCompat
@@ -33,6 +35,7 @@ import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.util.* import be.mygod.vpnhotspot.util.*
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@@ -203,6 +206,13 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
} }
} }
@Parcelize
class Starter : BootReceiver.Startable {
override fun start(context: Context) {
ContextCompat.startForegroundService(context, Intent(context, RepeaterService::class.java))
}
}
private val p2pManager get() = Services.p2p!! private val p2pManager get() = Services.p2p!!
private var channel: WifiP2pManager.Channel? = null private var channel: WifiP2pManager.Channel? = null
private val binder = Binder() private val binder = Binder()
@@ -463,6 +473,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
routingManager = RoutingManager.LocalOnly(this@RepeaterService, group.`interface`!!).apply { start() } routingManager = RoutingManager.LocalOnly(this@RepeaterService, group.`interface`!!).apply { start() }
status = Status.ACTIVE status = Status.ACTIVE
showNotification(group) showNotification(group)
BootReceiver.add<RepeaterService>(Starter())
} }
private fun startFailure(msg: CharSequence, group: WifiP2pGroup? = null, showWifiEnable: Boolean = false) { private fun startFailure(msg: CharSequence, group: WifiP2pGroup? = null, showWifiEnable: Boolean = false) {
SmartSnackbar.make(msg).apply { SmartSnackbar.make(msg).apply {
@@ -493,6 +504,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
}) })
} }
private fun cleanLocked() { private fun cleanLocked() {
BootReceiver.delete<RepeaterService>()
if (receiverRegistered) { if (receiverRegistered) {
ensureReceiverUnregistered(receiver) ensureReceiverUnregistered(receiver)
p2pPoller?.cancel() p2pPoller?.cancel()

View File

@@ -66,7 +66,7 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
private var routing: Routing? = null private var routing: Routing? = null
private var isWifi = forceWifi || TetherType.ofInterface(downstream).isWifi private var isWifi = forceWifi || TetherType.ofInterface(downstream).isWifi
fun start() = synchronized(RoutingManager) { fun start(fromMonitor: Boolean = false) = synchronized(RoutingManager) {
started = true started = true
when (val other = active.putIfAbsent(downstream, this)) { when (val other = active.putIfAbsent(downstream, this)) {
null -> { null -> {
@@ -78,14 +78,14 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
isWifi = isWifiNow isWifi = isWifiNow
} }
} }
initRoutingLocked() initRoutingLocked(fromMonitor)
} }
this -> true // already started this -> true // already started
else -> error("Double routing detected for $downstream from $caller != ${other.caller}") else -> error("Double routing detected for $downstream from $caller != ${other.caller}")
} }
} }
private fun initRoutingLocked() = try { private fun initRoutingLocked(fromMonitor: Boolean = false) = try {
routing = Routing(caller, downstream).apply { routing = Routing(caller, downstream).apply {
try { try {
configure() configure()
@@ -97,10 +97,10 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
true true
} catch (e: Exception) { } catch (e: Exception) {
when (e) { when (e) {
is Routing.InterfaceNotFoundException -> Timber.d(e) is Routing.InterfaceNotFoundException -> if (!fromMonitor) Timber.d(e)
!is CancellationException -> Timber.w(e) !is CancellationException -> Timber.w(e)
} }
SmartSnackbar.make(e).show() if (e !is Routing.InterfaceNotFoundException || !fromMonitor) SmartSnackbar.make(e).show()
routing = null routing = null
false false
} }

View File

@@ -66,14 +66,10 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
} }
} else parent!!.removePreference(this) } else parent!!.removePreference(this)
} }
val boot = findPreference<SwitchPreference>("service.repeater.startOnBoot")!! findPreference<SwitchPreference>(BootReceiver.KEY)!!.setOnPreferenceChangeListener { _, value ->
if (Services.p2p != null) { BootReceiver.onUserSettingUpdated(value as Boolean)
boot.setOnPreferenceChangeListener { _, value ->
BootReceiver.enabled = value as Boolean
true true
} }
boot.isChecked = BootReceiver.enabled
} else boot.parent!!.removePreference(boot)
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.parent!!.removePreference(safeMode)

View File

@@ -1,8 +1,10 @@
package be.mygod.vpnhotspot package be.mygod.vpnhotspot
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.Routing import be.mygod.vpnhotspot.net.Routing
import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetheringManager
@@ -10,6 +12,7 @@ import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.util.Event0 import be.mygod.vpnhotspot.util.Event0
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@@ -17,6 +20,7 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
companion object { companion object {
const val EXTRA_ADD_INTERFACES = "interface.add" const val EXTRA_ADD_INTERFACES = "interface.add"
const val EXTRA_ADD_INTERFACE_MONITOR = "interface.add.monitor" const val EXTRA_ADD_INTERFACE_MONITOR = "interface.add.monitor"
const val EXTRA_ADD_INTERFACES_MONITOR = "interface.adds.monitor"
const val EXTRA_REMOVE_INTERFACE = "interface.remove" const val EXTRA_REMOVE_INTERFACE = "interface.remove"
} }
@@ -39,6 +43,15 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
} }
} }
@Parcelize
data class Starter(val monitored: ArrayList<String>) : BootReceiver.Startable {
override fun start(context: Context) {
ContextCompat.startForegroundService(context, Intent(context, TetheringService::class.java).apply {
putStringArrayListExtra(EXTRA_ADD_INTERFACES_MONITOR, monitored)
})
}
}
/** /**
* Writes and critical reads to downstreams should be protected with this context. * Writes and critical reads to downstreams should be protected with this context.
*/ */
@@ -55,7 +68,7 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
val toRemove = downstreams.toMutableMap() // make a copy val toRemove = downstreams.toMutableMap() // make a copy
for (iface in interfaces) { for (iface in interfaces) {
val downstream = toRemove.remove(iface) ?: continue val downstream = toRemove.remove(iface) ?: continue
if (downstream.monitor) downstream.start() if (downstream.monitor && !downstream.start()) downstream.stop()
} }
for ((iface, downstream) in toRemove) { for ((iface, downstream) in toRemove) {
if (!downstream.monitor) check(downstreams.remove(iface, downstream)) if (!downstream.monitor) check(downstreams.remove(iface, downstream))
@@ -81,6 +94,10 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
ServiceNotification.stopForeground(this) ServiceNotification.stopForeground(this)
stopSelf() stopSelf()
} else { } else {
binder.monitoredIfaces.also {
if (it.isEmpty()) BootReceiver.delete<TetheringService>()
else BootReceiver.add<TetheringService>(Starter(ArrayList(it)))
}
if (!callbackRegistered) { if (!callbackRegistered) {
callbackRegistered = true callbackRegistered = true
TetheringManager.registerTetheringEventCallbackCompat(this, this) TetheringManager.registerTetheringEventCallbackCompat(this, this)
@@ -103,10 +120,12 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
if (start()) check(downstreams.put(iface, this) == null) else stop() if (start()) check(downstreams.put(iface, this) == null) else stop()
} }
} }
intent.getStringExtra(EXTRA_ADD_INTERFACE_MONITOR)?.also { iface -> val monitorList = intent.getStringArrayListExtra(EXTRA_ADD_INTERFACES_MONITOR) ?:
intent.getStringExtra(EXTRA_ADD_INTERFACE_MONITOR)?.let { listOf(it) }
if (!monitorList.isNullOrEmpty()) for (iface in monitorList) {
val downstream = downstreams[iface] val downstream = downstreams[iface]
if (downstream == null) Downstream(this@TetheringService, iface, true).apply { if (downstream == null) Downstream(this@TetheringService, iface, true).apply {
start() if (!start(true)) stop()
check(downstreams.put(iface, this) == null) check(downstreams.put(iface, this) == null)
downstreams[iface] = this downstreams[iface] = this
} else downstream.monitor = true } else downstream.monitor = true
@@ -120,6 +139,7 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
override fun onDestroy() { override fun onDestroy() {
launch { launch {
BootReceiver.delete<TetheringService>()
unregisterReceiver() unregisterReceiver()
downstreams.values.forEach { it.stop() } // force clean to prevent leakage downstreams.values.forEach { it.stop() } // force clean to prevent leakage
cancel() cancel()

View File

@@ -91,7 +91,6 @@
<string name="settings_service_masquerade_netd">Servizio Android Netd</string> <string name="settings_service_masquerade_netd">Servizio Android Netd</string>
<string name="settings_service_disable_ipv6">Disabilita tethering IPv6</string> <string name="settings_service_disable_ipv6">Disabilita tethering IPv6</string>
<string name="settings_service_disable_ipv6_summary">Abilitando questa funzione si preveniranno perdite della VPN via IPv6.</string> <string name="settings_service_disable_ipv6_summary">Abilitando questa funzione si preveniranno perdite della VPN via IPv6.</string>
<string name="settings_service_repeater_start_on_boot">Avvia ripetitore all\'avvio</string>
<string name="settings_service_wifi_lock">Tieni il Wi\u2011Fi attivo</string> <string name="settings_service_wifi_lock">Tieni il Wi\u2011Fi attivo</string>
<string name="settings_service_wifi_lock_none">Default di sistema</string> <string name="settings_service_wifi_lock_none">Default di sistema</string>
<string name="settings_service_wifi_lock_full">Attivo</string> <string name="settings_service_wifi_lock_full">Attivo</string>

View File

@@ -111,7 +111,8 @@
<string name="settings_service_masquerade_netd">Android Netd 服务</string> <string name="settings_service_masquerade_netd">Android Netd 服务</string>
<string name="settings_service_disable_ipv6">禁用 IPv6 共享</string> <string name="settings_service_disable_ipv6">禁用 IPv6 共享</string>
<string name="settings_service_disable_ipv6_summary">防止 VPN 通过 IPv6 泄漏。</string> <string name="settings_service_disable_ipv6_summary">防止 VPN 通过 IPv6 泄漏。</string>
<string name="settings_service_repeater_start_on_boot">开机自启动中继</string> <string name="settings_service_auto_start">自动启动服务</string>
<string name="settings_service_auto_start_summary">设备重启或应用升级后自动恢复之前运行的服务</string>
<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>

View File

@@ -109,7 +109,6 @@
<string name="settings_service_masquerade_netd">Android Netd 服務</string> <string name="settings_service_masquerade_netd">Android Netd 服務</string>
<string name="settings_service_disable_ipv6">停用 IPv6 共用</string> <string name="settings_service_disable_ipv6">停用 IPv6 共用</string>
<string name="settings_service_disable_ipv6_summary">防止 VPN 通過 IPv6 洩漏</string> <string name="settings_service_disable_ipv6_summary">防止 VPN 通過 IPv6 洩漏</string>
<string name="settings_service_repeater_start_on_boot">開機時自動啟動中繼器</string>
<string name="settings_service_repeater_safe_mode">中繼安全模式</string> <string name="settings_service_repeater_safe_mode">中繼安全模式</string>
<string name="settings_service_repeater_safe_mode_summary">不對系統設定值進行任何修改,但是可能需要較長的 SSID。</string> <string name="settings_service_repeater_safe_mode_summary">不對系統設定值進行任何修改,但是可能需要較長的 SSID。</string>
<string name="settings_service_repeater_safe_mode_warning">使用短 SSID 可能需要關閉安全模式。</string> <string name="settings_service_repeater_safe_mode_warning">使用短 SSID 可能需要關閉安全模式。</string>

View File

@@ -128,7 +128,9 @@
<string name="settings_service_masquerade_netd">Android Netd Service</string> <string name="settings_service_masquerade_netd">Android Netd Service</string>
<string name="settings_service_disable_ipv6">Disable IPv6 tethering</string> <string name="settings_service_disable_ipv6">Disable IPv6 tethering</string>
<string name="settings_service_disable_ipv6_summary">Enabling this option will prevent VPN leaks via IPv6.</string> <string name="settings_service_disable_ipv6_summary">Enabling this option will prevent VPN leaks via IPv6.</string>
<string name="settings_service_repeater_start_on_boot">Start repeater on boot</string> <string name="settings_service_auto_start">Auto start services</string>
<string name="settings_service_auto_start_summary">Restore services if they were running before device reboot or app
update</string>
<string name="settings_service_repeater_safe_mode">Repeater safe mode</string> <string name="settings_service_repeater_safe_mode">Repeater safe mode</string>
<string name="settings_service_repeater_safe_mode_summary">Makes no changes to your system configuration but might <string name="settings_service_repeater_safe_mode_summary">Makes no changes to your system configuration but might
not work with short network names.</string> not work with short network names.</string>

View File

@@ -59,9 +59,10 @@
app:title="@string/settings_service_wifi_lock" app:title="@string/settings_service_wifi_lock"
app:useSimpleSummaryProvider="true"/> app:useSimpleSummaryProvider="true"/>
<SwitchPreference <SwitchPreference
app:key="service.repeater.startOnBoot" app:key="service.autoStart"
app:icon="@drawable/ic_action_autorenew" app:icon="@drawable/ic_action_autorenew"
app:title="@string/settings_service_repeater_start_on_boot"/> app:title="@string/settings_service_auto_start"
app:summary="@string/settings_service_auto_start_summary"/>
<SwitchPreference <SwitchPreference
app:key="service.repeater.safeMode" app:key="service.repeater.safeMode"
app:icon="@drawable/ic_alert_warning" app:icon="@drawable/ic_alert_warning"