@@ -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>
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user