Prevent initiailizing su in main thread
This should hopefully fix #113.
This commit is contained in:
@@ -23,13 +23,18 @@ import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps
|
||||
import be.mygod.vpnhotspot.net.wifi.configuration.channelToFrequency
|
||||
import be.mygod.vpnhotspot.util.*
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.newSingleThreadContext
|
||||
import timber.log.Timber
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
|
||||
/**
|
||||
* Service for handling Wi-Fi P2P. `supported` must be checked before this service is started otherwise it would crash.
|
||||
*/
|
||||
class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListener,
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
companion object {
|
||||
private const val TAG = "RepeaterService"
|
||||
private const val KEY_NETWORK_NAME = "service.repeater.networkName"
|
||||
@@ -121,7 +126,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
||||
when (intent.action) {
|
||||
WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION ->
|
||||
if (intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, 0) ==
|
||||
WifiP2pManager.WIFI_P2P_STATE_DISABLED) clean() // ignore P2P enabled
|
||||
WifiP2pManager.WIFI_P2P_STATE_DISABLED) launch { cleanLocked() } // ignore P2P enabled
|
||||
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> onP2pConnectionChanged(
|
||||
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO)!!,
|
||||
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP)!!)
|
||||
@@ -136,6 +141,10 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
||||
WifiP2pManagerHelper.WIFI_P2P_PERSISTENT_GROUPS_CHANGED_ACTION -> onPersistentGroupsChanged()
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Writes and critical reads to routingManager should be protected with this context.
|
||||
*/
|
||||
override val coroutineContext = newSingleThreadContext("TetheringService") + Job()
|
||||
private var routingManager: RoutingManager? = null
|
||||
private var persistNextGroup = false
|
||||
|
||||
@@ -250,7 +259,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
||||
p2pManager.requestGroupInfo(channel) {
|
||||
when {
|
||||
it == null -> doStart()
|
||||
it.isGroupOwner -> if (routingManager == null) doStart(it)
|
||||
it.isGroupOwner -> launch { if (routingManager == null) doStartLocked(it) }
|
||||
else -> {
|
||||
Timber.i("Removing old group ($it)")
|
||||
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
|
||||
@@ -324,23 +333,23 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
||||
/**
|
||||
* Used during step 2, also called when connection changed
|
||||
*/
|
||||
private fun onP2pConnectionChanged(info: WifiP2pInfo, group: WifiP2pGroup) {
|
||||
private fun onP2pConnectionChanged(info: WifiP2pInfo, group: WifiP2pGroup) = launch {
|
||||
DebugHelper.log(TAG, "P2P connection changed: $info\n$group")
|
||||
when {
|
||||
!info.groupFormed || !info.isGroupOwner || !group.isGroupOwner -> {
|
||||
if (routingManager != null) clean() // P2P shutdown, else other groups changing before start, ignore
|
||||
if (routingManager != null) cleanLocked() // P2P shutdown, else other groups changing before start, ignore
|
||||
}
|
||||
routingManager != null -> {
|
||||
binder.group = group
|
||||
showNotification(group)
|
||||
}
|
||||
else -> doStart(group)
|
||||
else -> doStartLocked(group)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* startService Step 3
|
||||
*/
|
||||
private fun doStart(group: WifiP2pGroup) {
|
||||
private fun doStartLocked(group: WifiP2pGroup) {
|
||||
binder.group = group
|
||||
if (persistNextGroup) {
|
||||
networkName = group.networkName
|
||||
@@ -355,7 +364,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
||||
private fun startFailure(msg: CharSequence, group: WifiP2pGroup? = null) {
|
||||
SmartSnackbar.make(msg).show()
|
||||
showNotification()
|
||||
if (group != null) removeGroup() else clean()
|
||||
if (group != null) removeGroup() else cleanLocked()
|
||||
}
|
||||
|
||||
private fun showNotification(group: WifiP2pGroup? = null) = ServiceNotification.startForeground(this,
|
||||
@@ -363,12 +372,14 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
||||
|
||||
private fun removeGroup() {
|
||||
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
|
||||
override fun onSuccess() = clean()
|
||||
override fun onSuccess() {
|
||||
launch { cleanLocked() }
|
||||
}
|
||||
override fun onFailure(reason: Int) {
|
||||
if (reason != WifiP2pManager.BUSY) {
|
||||
SmartSnackbar.make(formatReason(R.string.repeater_remove_group_failure, reason)).show()
|
||||
} // else assuming it's already gone
|
||||
clean()
|
||||
launch { cleanLocked() }
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -378,7 +389,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
||||
receiverRegistered = false
|
||||
}
|
||||
}
|
||||
private fun clean() {
|
||||
private fun cleanLocked() {
|
||||
unregisterReceiver()
|
||||
routingManager?.destroy()
|
||||
routingManager = null
|
||||
@@ -390,7 +401,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
||||
override fun onDestroy() {
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
if (status != Status.IDLE) binder.shutdown()
|
||||
clean() // force clean to prevent leakage
|
||||
launch { cleanLocked() } // force clean to prevent leakage
|
||||
if (Build.VERSION.SDK_INT < 29) @Suppress("DEPRECATION") {
|
||||
app.pref.unregisterOnSharedPreferenceChangeListener(this)
|
||||
unregisterReceiver(deviceListener)
|
||||
|
||||
@@ -21,6 +21,10 @@ import be.mygod.vpnhotspot.util.RootSession
|
||||
import be.mygod.vpnhotspot.util.launchUrl
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
@@ -40,14 +44,22 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
||||
if (Build.VERSION.SDK_INT >= 27) {
|
||||
isChecked = TetherOffloadManager.enabled
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
if (TetherOffloadManager.enabled != newValue) {
|
||||
isEnabled = false
|
||||
GlobalScope.launch {
|
||||
try {
|
||||
TetherOffloadManager.enabled = newValue as Boolean
|
||||
TetherOffloadManager.enabled == newValue
|
||||
} catch (e: Exception) {
|
||||
Timber.d(e)
|
||||
SmartSnackbar.make(e).show()
|
||||
false
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
isChecked = TetherOffloadManager.enabled
|
||||
isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
} else parent!!.removePreference(this)
|
||||
}
|
||||
@@ -60,20 +72,23 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
||||
boot.isChecked = BootReceiver.enabled
|
||||
} else boot.parent!!.removePreference(boot)
|
||||
findPreference<Preference>("service.clean")!!.setOnPreferenceClickListener {
|
||||
RoutingManager.clean()
|
||||
GlobalScope.launch { RoutingManager.clean() }
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(IpMonitor.KEY)!!.setOnPreferenceChangeListener { _, _ ->
|
||||
Snackbar.make(requireView(), R.string.settings_restart_required, Snackbar.LENGTH_LONG).apply {
|
||||
setAction(R.string.settings_exit_app) {
|
||||
GlobalScope.launch {
|
||||
RoutingManager.clean(false)
|
||||
RootSession.trimMemory()
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
}.show()
|
||||
true
|
||||
}
|
||||
findPreference<Preference>("misc.logcat")!!.setOnPreferenceClickListener {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val context = requireContext()
|
||||
val logDir = File(context.cacheDir, "log")
|
||||
logDir.mkdir()
|
||||
@@ -130,12 +145,13 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
}
|
||||
}
|
||||
startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND)
|
||||
context.startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND)
|
||||
.setType("text/x-log")
|
||||
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.putExtra(Intent.EXTRA_STREAM,
|
||||
FileProvider.getUriForFile(context, "be.mygod.vpnhotspot.log", logFile)),
|
||||
getString(R.string.abc_shareactionprovider_share_with)))
|
||||
context.getString(R.string.abc_shareactionprovider_share_with)))
|
||||
}
|
||||
true
|
||||
}
|
||||
findPreference<Preference>("misc.source")!!.setOnPreferenceClickListener {
|
||||
|
||||
@@ -10,11 +10,10 @@ import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces
|
||||
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
||||
import be.mygod.vpnhotspot.util.Event0
|
||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class TetheringService : IpNeighbourMonitoringService() {
|
||||
class TetheringService : IpNeighbourMonitoringService(), CoroutineScope {
|
||||
companion object {
|
||||
const val EXTRA_ADD_INTERFACES = "interface.add"
|
||||
const val EXTRA_ADD_INTERFACE_MONITOR = "interface.add.monitor"
|
||||
@@ -23,13 +22,11 @@ class TetheringService : IpNeighbourMonitoringService() {
|
||||
|
||||
inner class Binder : android.os.Binder() {
|
||||
val routingsChanged = Event0()
|
||||
val monitoredIfaces get() = synchronized(downstreams) {
|
||||
downstreams.values.filter { it.monitor }.map { it.downstream }
|
||||
}
|
||||
val monitoredIfaces get() = downstreams.values.filter { it.monitor }.map { it.downstream }
|
||||
|
||||
fun isActive(iface: String) = synchronized(downstreams) { downstreams.containsKey(iface) }
|
||||
fun isInactive(iface: String) = synchronized(downstreams) { downstreams[iface] }?.run { !started && monitor }
|
||||
fun monitored(iface: String) = synchronized(downstreams) { downstreams[iface] }?.monitor
|
||||
fun isActive(iface: String) = downstreams.containsKey(iface)
|
||||
fun isInactive(iface: String) = downstreams[iface]?.run { !started && monitor }
|
||||
fun monitored(iface: String) = downstreams[iface]?.monitor
|
||||
}
|
||||
|
||||
private inner class Downstream(caller: Any, downstream: String, var monitor: Boolean = false) :
|
||||
@@ -42,13 +39,17 @@ class TetheringService : IpNeighbourMonitoringService() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes and critical reads to downstreams should be protected with this context.
|
||||
*/
|
||||
override val coroutineContext = newSingleThreadContext("TetheringService") + Job()
|
||||
private val binder = Binder()
|
||||
private val downstreams = mutableMapOf<String, Downstream>()
|
||||
private val downstreams = ConcurrentHashMap<String, Downstream>()
|
||||
private var receiverRegistered = false
|
||||
private val receiver = broadcastReceiver { _, intent ->
|
||||
synchronized(downstreams) {
|
||||
launch {
|
||||
val toRemove = downstreams.toMutableMap() // make a copy
|
||||
for (iface in intent.tetheredIfaces ?: return@synchronized) {
|
||||
for (iface in intent.tetheredIfaces ?: return@launch) {
|
||||
val downstream = toRemove.remove(iface) ?: continue
|
||||
if (downstream.monitor) downstream.start()
|
||||
}
|
||||
@@ -58,12 +59,8 @@ class TetheringService : IpNeighbourMonitoringService() {
|
||||
onDownstreamsChangedLocked()
|
||||
}
|
||||
}
|
||||
override val activeIfaces get() = synchronized(downstreams) {
|
||||
downstreams.values.filter { it.started }.map { it.downstream }
|
||||
}
|
||||
override val inactiveIfaces get() = synchronized(downstreams) {
|
||||
downstreams.values.filter { !it.started }.map { it.downstream }
|
||||
}
|
||||
override val activeIfaces get() = downstreams.values.filter { it.started }.map { it.downstream }
|
||||
override val inactiveIfaces get() = downstreams.values.filter { !it.started }.map { it.downstream }
|
||||
|
||||
private fun onDownstreamsChangedLocked() {
|
||||
if (downstreams.isEmpty()) {
|
||||
@@ -78,21 +75,22 @@ class TetheringService : IpNeighbourMonitoringService() {
|
||||
}
|
||||
updateNotification()
|
||||
}
|
||||
GlobalScope.launch(Dispatchers.Main) { binder.routingsChanged() }
|
||||
launch(Dispatchers.Main) { binder.routingsChanged() }
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?) = binder
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent != null) synchronized(downstreams) {
|
||||
launch {
|
||||
if (intent != null) {
|
||||
for (iface in intent.getStringArrayExtra(EXTRA_ADD_INTERFACES) ?: emptyArray()) {
|
||||
if (downstreams[iface] == null) Downstream(this, iface).apply {
|
||||
if (downstreams[iface] == null) Downstream(this@TetheringService, iface).apply {
|
||||
if (start()) check(downstreams.put(iface, this) == null) else destroy()
|
||||
}
|
||||
}
|
||||
intent.getStringExtra(EXTRA_ADD_INTERFACE_MONITOR)?.also { iface ->
|
||||
val downstream = downstreams[iface]
|
||||
if (downstream == null) Downstream(this, iface, true).apply {
|
||||
if (downstream == null) Downstream(this@TetheringService, iface, true).apply {
|
||||
start()
|
||||
check(downstreams.put(iface, this) == null)
|
||||
downstreams[iface] = this
|
||||
@@ -102,13 +100,15 @@ class TetheringService : IpNeighbourMonitoringService() {
|
||||
updateNotification() // call this first just in case we are shutting down immediately
|
||||
onDownstreamsChangedLocked()
|
||||
} else if (downstreams.isEmpty()) stopSelf(startId)
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
synchronized(downstreams) {
|
||||
launch {
|
||||
downstreams.values.forEach { it.destroy() } // force clean to prevent leakage
|
||||
unregisterReceiver()
|
||||
cancel()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.content.SharedPreferences
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.util.RootSession
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
||||
@@ -24,7 +26,7 @@ object DhcpWorkaround : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
}
|
||||
|
||||
val shouldEnable get() = app.pref.getBoolean(KEY_ENABLED, false)
|
||||
fun enable(enabled: Boolean) {
|
||||
fun enable(enabled: Boolean) = GlobalScope.launch {
|
||||
val action = if (enabled) "add" else "del"
|
||||
try {
|
||||
RootSession.use { it.exec("ip rule $action iif lo uidrange 0-0 lookup local_network priority 11000") }
|
||||
@@ -33,7 +35,7 @@ object DhcpWorkaround : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
e.result.err.joinToString("\n") == "RTNETLINK answers: File exists"
|
||||
} else {
|
||||
e.result.err.joinToString("\n") == "RTNETLINK answers: No such file or directory"
|
||||
}) return
|
||||
}) return@launch
|
||||
Timber.w(IOException("Failed to tweak dhcp workaround rule", e))
|
||||
SmartSnackbar.make(e).show()
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -16,7 +16,6 @@ import be.mygod.vpnhotspot.util.RootSession
|
||||
@RequiresApi(27)
|
||||
object TetherOffloadManager {
|
||||
private const val TETHER_OFFLOAD_DISABLED = "tether_offload_disabled"
|
||||
@JvmStatic
|
||||
var enabled: Boolean
|
||||
get() = Settings.Global.getInt(app.contentResolver, TETHER_OFFLOAD_DISABLED, 0) == 0
|
||||
set(value) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package be.mygod.vpnhotspot.util
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.os.Looper
|
||||
import androidx.core.os.postDelayed
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import timber.log.Timber
|
||||
@@ -67,6 +68,12 @@ class RootSession : AutoCloseable {
|
||||
|
||||
class UnexpectedOutputException(msg: String, val result: Shell.Result) : RuntimeException(msg)
|
||||
|
||||
init {
|
||||
check(Looper.getMainLooper().thread != Thread.currentThread()) {
|
||||
"Unable to initialize shell in main thread" // https://github.com/topjohnwu/libsu/issues/33
|
||||
}
|
||||
}
|
||||
|
||||
private val shell = Shell.newInstance("su")
|
||||
private val stdout = ArrayList<String>()
|
||||
private val stderr = ArrayList<String>()
|
||||
|
||||
Reference in New Issue
Block a user