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>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>

View File

@@ -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 }

View File

@@ -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 <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) {
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)
}
}

View File

@@ -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<LocalOnlyHotspotService>(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<LocalOnlyHotspotService>()
binder.iface = null
unregisterReceiver()
ServiceNotification.stopForeground(this)

View File

@@ -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<RepeaterService>(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<RepeaterService>()
if (receiverRegistered) {
ensureReceiverUnregistered(receiver)
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 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
}

View File

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

View File

@@ -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<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.
*/
@@ -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<TetheringService>()
else BootReceiver.add<TetheringService>(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<TetheringService>()
unregisterReceiver()
downstreams.values.forEach { it.stop() } // force clean to prevent leakage
cancel()

View File

@@ -91,7 +91,6 @@
<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_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_none">Default di sistema</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_disable_ipv6">禁用 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_summary">不对系统配置进行修改,但是可能须要较长的网络名称。</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_disable_ipv6">停用 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_summary">不對系統設定值進行任何修改,但是可能需要較長的 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_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_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_summary">Makes no changes to your system configuration but might
not work with short network names.</string>

View File

@@ -59,9 +59,10 @@
app:title="@string/settings_service_wifi_lock"
app:useSimpleSummaryProvider="true"/>
<SwitchPreference
app:key="service.repeater.startOnBoot"
app:key="service.autoStart"
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
app:key="service.repeater.safeMode"
app:icon="@drawable/ic_alert_warning"