From aee1a45ebaba349a71fd2aeba1b497298dc0572c Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 10 Oct 2021 17:08:16 -0400 Subject: [PATCH] Support auto start services Fixes #96. --- mobile/src/main/AndroidManifest.xml | 1 + .../src/main/java/be/mygod/vpnhotspot/App.kt | 1 + .../java/be/mygod/vpnhotspot/BootReceiver.kt | 95 +++++++++++++++++-- .../vpnhotspot/LocalOnlyHotspotService.kt | 11 +++ .../be/mygod/vpnhotspot/RepeaterService.kt | 12 +++ .../be/mygod/vpnhotspot/RoutingManager.kt | 10 +- .../vpnhotspot/SettingsPreferenceFragment.kt | 12 +-- .../be/mygod/vpnhotspot/TetheringService.kt | 26 ++++- mobile/src/main/res/values-it/strings.xml | 1 - mobile/src/main/res/values-zh-rCN/strings.xml | 3 +- mobile/src/main/res/values-zh-rTW/strings.xml | 1 - mobile/src/main/res/values/strings.xml | 4 +- mobile/src/main/res/xml/pref_settings.xml | 5 +- 13 files changed, 153 insertions(+), 29 deletions(-) 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"/>