diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml
index 6330f35c..c09932ed 100644
--- a/mobile/src/main/AndroidManifest.xml
+++ b/mobile/src/main/AndroidManifest.xml
@@ -235,6 +235,7 @@
+
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/App.kt b/mobile/src/main/java/be/mygod/vpnhotspot/App.kt
index 9e2eca05..6a5e3bb7 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/App.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/App.kt
@@ -48,6 +48,7 @@ class App : Application() {
// alternative to PreferenceManager.getDefaultSharedPreferencesName(this)
deviceStorage.moveSharedPreferencesFrom(this, PreferenceManager(this).sharedPreferencesName)
deviceStorage.moveDatabaseFrom(this, AppDatabase.DB_NAME)
+ BootReceiver.migrateIfNecessary(this, deviceStorage)
} else deviceStorage = this
Services.init { this }
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/BootReceiver.kt b/mobile/src/main/java/be/mygod/vpnhotspot/BootReceiver.kt
index 7bdd97aa..b88c6962 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/BootReceiver.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/BootReceiver.kt
@@ -5,31 +5,112 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
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.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() {
companion object {
+ const val KEY = "service.autoStart"
+
private val componentName by lazy { ComponentName(app, BootReceiver::class.java) }
- var enabled: Boolean
+ private var enabled: Boolean
get() = app.packageManager.getComponentEnabledSetting(componentName) ==
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
set(value) = app.packageManager.setComponentEnabledSetting(componentName,
if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED
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 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 add(value: Startable) = add(T::class.java.name, value)
+ inline fun 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 = mutableMapOf()) : Parcelable
+
override fun onReceive(context: Context, intent: Intent) {
if (started) return
- when (intent.action) {
- Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_LOCKED_BOOT_COMPLETED -> started = true
+ val isUpdate = when (intent.action) {
+ Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_LOCKED_BOOT_COMPLETED -> false
+ Intent.ACTION_MY_PACKAGE_REPLACED -> true
else -> return
}
- if (Services.p2p != null) {
- ContextCompat.startForegroundService(context, Intent(context, RepeaterService::class.java))
+ started = true
+ 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)
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt
index 95fcfd83..76023a9b 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt
@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot
+import android.content.Context
import android.content.Intent
import android.content.IntentFilter
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.widget.SmartSnackbar
import kotlinx.coroutines.*
+import kotlinx.parcelize.Parcelize
import timber.log.Timber
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 var reservation: WifiManager.LocalOnlyHotspotReservation? = null
/**
@@ -86,6 +95,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
return stopService()
}
binder.iface = iface
+ BootReceiver.add(Starter())
launch {
check(routingManager == null)
routingManager = RoutingManager.LocalOnly(this@LocalOnlyHotspotService, iface).apply { start() }
@@ -140,6 +150,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
}
private fun stopService() {
+ BootReceiver.delete()
binder.iface = null
unregisterReceiver()
ServiceNotification.stopForeground(this)
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt
index bf2950bb..0e7a877e 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt
@@ -3,6 +3,7 @@ package be.mygod.vpnhotspot
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.Service
+import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
@@ -14,6 +15,7 @@ import android.os.Looper
import android.provider.Settings
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
+import androidx.core.content.ContextCompat
import androidx.core.content.edit
import be.mygod.vpnhotspot.App.Companion.app
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.widget.SmartSnackbar
import kotlinx.coroutines.*
+import kotlinx.parcelize.Parcelize
import timber.log.Timber
import java.lang.reflect.InvocationTargetException
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 var channel: WifiP2pManager.Channel? = null
private val binder = Binder()
@@ -463,6 +473,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
routingManager = RoutingManager.LocalOnly(this@RepeaterService, group.`interface`!!).apply { start() }
status = Status.ACTIVE
showNotification(group)
+ BootReceiver.add(Starter())
}
private fun startFailure(msg: CharSequence, group: WifiP2pGroup? = null, showWifiEnable: Boolean = false) {
SmartSnackbar.make(msg).apply {
@@ -493,6 +504,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
})
}
private fun cleanLocked() {
+ BootReceiver.delete()
if (receiverRegistered) {
ensureReceiverUnregistered(receiver)
p2pPoller?.cancel()
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RoutingManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RoutingManager.kt
index a4a8514b..4da40af3 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/RoutingManager.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/RoutingManager.kt
@@ -66,7 +66,7 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
private var routing: Routing? = null
private var isWifi = forceWifi || TetherType.ofInterface(downstream).isWifi
- fun start() = synchronized(RoutingManager) {
+ fun start(fromMonitor: Boolean = false) = synchronized(RoutingManager) {
started = true
when (val other = active.putIfAbsent(downstream, this)) {
null -> {
@@ -78,14 +78,14 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
isWifi = isWifiNow
}
}
- initRoutingLocked()
+ initRoutingLocked(fromMonitor)
}
this -> true // already started
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 {
try {
configure()
@@ -97,10 +97,10 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
true
} catch (e: Exception) {
when (e) {
- is Routing.InterfaceNotFoundException -> Timber.d(e)
+ is Routing.InterfaceNotFoundException -> if (!fromMonitor) Timber.d(e)
!is CancellationException -> Timber.w(e)
}
- SmartSnackbar.make(e).show()
+ if (e !is Routing.InterfaceNotFoundException || !fromMonitor) SmartSnackbar.make(e).show()
routing = null
false
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/SettingsPreferenceFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/SettingsPreferenceFragment.kt
index cef0e46a..3c4a6a80 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/SettingsPreferenceFragment.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/SettingsPreferenceFragment.kt
@@ -66,14 +66,10 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
}
} else parent!!.removePreference(this)
}
- val boot = findPreference("service.repeater.startOnBoot")!!
- if (Services.p2p != null) {
- boot.setOnPreferenceChangeListener { _, value ->
- BootReceiver.enabled = value as Boolean
- true
- }
- boot.isChecked = BootReceiver.enabled
- } else boot.parent!!.removePreference(boot)
+ findPreference(BootReceiver.KEY)!!.setOnPreferenceChangeListener { _, value ->
+ BootReceiver.onUserSettingUpdated(value as Boolean)
+ true
+ }
if (Services.p2p == null || !RepeaterService.safeModeConfigurable) {
val safeMode = findPreference(RepeaterService.KEY_SAFE_MODE)!!
safeMode.parent!!.removePreference(safeMode)
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt
index 8f1ba20b..c981c9db 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt
@@ -1,8 +1,10 @@
package be.mygod.vpnhotspot
+import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.annotation.RequiresApi
+import androidx.core.content.ContextCompat
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.Routing
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.widget.SmartSnackbar
import kotlinx.coroutines.*
+import kotlinx.parcelize.Parcelize
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
@@ -17,6 +20,7 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
companion object {
const val EXTRA_ADD_INTERFACES = "interface.add"
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"
}
@@ -39,6 +43,15 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
}
}
+ @Parcelize
+ data class Starter(val monitored: ArrayList) : 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.
*/
@@ -55,7 +68,7 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
val toRemove = downstreams.toMutableMap() // make a copy
for (iface in interfaces) {
val downstream = toRemove.remove(iface) ?: continue
- if (downstream.monitor) downstream.start()
+ if (downstream.monitor && !downstream.start()) downstream.stop()
}
for ((iface, downstream) in toRemove) {
if (!downstream.monitor) check(downstreams.remove(iface, downstream))
@@ -81,6 +94,10 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
ServiceNotification.stopForeground(this)
stopSelf()
} else {
+ binder.monitoredIfaces.also {
+ if (it.isEmpty()) BootReceiver.delete()
+ else BootReceiver.add(Starter(ArrayList(it)))
+ }
if (!callbackRegistered) {
callbackRegistered = true
TetheringManager.registerTetheringEventCallbackCompat(this, this)
@@ -103,10 +120,12 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
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]
if (downstream == null) Downstream(this@TetheringService, iface, true).apply {
- start()
+ if (!start(true)) stop()
check(downstreams.put(iface, this) == null)
downstreams[iface] = this
} else downstream.monitor = true
@@ -120,6 +139,7 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
override fun onDestroy() {
launch {
+ BootReceiver.delete()
unregisterReceiver()
downstreams.values.forEach { it.stop() } // force clean to prevent leakage
cancel()
diff --git a/mobile/src/main/res/values-it/strings.xml b/mobile/src/main/res/values-it/strings.xml
index 16ba39e8..223c86c3 100644
--- a/mobile/src/main/res/values-it/strings.xml
+++ b/mobile/src/main/res/values-it/strings.xml
@@ -91,7 +91,6 @@
Servizio Android Netd
Disabilita tethering IPv6
Abilitando questa funzione si preveniranno perdite della VPN via IPv6.
- Avvia ripetitore all\'avvio
Tieni il Wi\u2011Fi attivo
Default di sistema
Attivo
diff --git a/mobile/src/main/res/values-zh-rCN/strings.xml b/mobile/src/main/res/values-zh-rCN/strings.xml
index 1230ea94..bef56fa4 100644
--- a/mobile/src/main/res/values-zh-rCN/strings.xml
+++ b/mobile/src/main/res/values-zh-rCN/strings.xml
@@ -111,7 +111,8 @@
Android Netd 服务
禁用 IPv6 共享
防止 VPN 通过 IPv6 泄漏。
- 开机自启动中继
+ 自动启动服务
+ 设备重启或应用升级后自动恢复之前运行的服务
中继安全模式
不对系统配置进行修改,但是可能须要较长的网络名称。
使用短名称可能需要关闭安全模式。
diff --git a/mobile/src/main/res/values-zh-rTW/strings.xml b/mobile/src/main/res/values-zh-rTW/strings.xml
index 93eed8f7..b2f9f704 100644
--- a/mobile/src/main/res/values-zh-rTW/strings.xml
+++ b/mobile/src/main/res/values-zh-rTW/strings.xml
@@ -109,7 +109,6 @@
Android Netd 服務
停用 IPv6 共用
防止 VPN 通過 IPv6 洩漏
- 開機時自動啟動中繼器
中繼安全模式
不對系統設定值進行任何修改,但是可能需要較長的 SSID。
使用短 SSID 可能需要關閉安全模式。
diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml
index 25492cfb..f96035f8 100644
--- a/mobile/src/main/res/values/strings.xml
+++ b/mobile/src/main/res/values/strings.xml
@@ -128,7 +128,9 @@
Android Netd Service
Disable IPv6 tethering
Enabling this option will prevent VPN leaks via IPv6.
- Start repeater on boot
+ Auto start services
+ Restore services if they were running before device reboot or app
+ update
Repeater safe mode
Makes no changes to your system configuration but might
not work with short network names.
diff --git a/mobile/src/main/res/xml/pref_settings.xml b/mobile/src/main/res/xml/pref_settings.xml
index 2f047ab9..e0d4e0fa 100644
--- a/mobile/src/main/res/xml/pref_settings.xml
+++ b/mobile/src/main/res/xml/pref_settings.xml
@@ -59,9 +59,10 @@
app:title="@string/settings_service_wifi_lock"
app:useSimpleSummaryProvider="true"/>
+ app:title="@string/settings_service_auto_start"
+ app:summary="@string/settings_service_auto_start_summary"/>