Initial support for registerSoftApCallback
This commit is contained in:
@@ -15,7 +15,6 @@ import kotlinx.coroutines.channels.*
|
|||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
@@ -191,6 +190,9 @@ class RootServer @JvmOverloads constructor(private val warnLogger: (String) -> U
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
callbackSpin()
|
callbackSpin()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
process.destroy()
|
||||||
|
throw e
|
||||||
} finally {
|
} finally {
|
||||||
if (DEBUG) Log.d(TAG, "Waiting for exit")
|
if (DEBUG) Log.d(TAG, "Waiting for exit")
|
||||||
process.waitFor()
|
process.waitFor()
|
||||||
@@ -270,9 +272,13 @@ class RootServer @JvmOverloads constructor(private val warnLogger: (String) -> U
|
|||||||
if (active) {
|
if (active) {
|
||||||
active = false
|
active = false
|
||||||
if (DEBUG) Log.d(TAG, "Shutting down from client")
|
if (DEBUG) Log.d(TAG, "Shutting down from client")
|
||||||
sendLocked(Shutdown())
|
try {
|
||||||
output.close()
|
sendLocked(Shutdown())
|
||||||
process.outputStream.close()
|
output.close()
|
||||||
|
process.outputStream.close()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.i(TAG, "send Shutdown failed", e)
|
||||||
|
}
|
||||||
if (DEBUG) Log.d(TAG, "Client closed")
|
if (DEBUG) Log.d(TAG, "Client closed")
|
||||||
}
|
}
|
||||||
if (fromWorker) {
|
if (fromWorker) {
|
||||||
@@ -375,7 +381,7 @@ class RootServer @JvmOverloads constructor(private val warnLogger: (String) -> U
|
|||||||
CoroutineScope(Dispatchers.Main.immediate + job)
|
CoroutineScope(Dispatchers.Main.immediate + job)
|
||||||
}
|
}
|
||||||
val callbackWorker = newSingleThreadContext("callbackWorker")
|
val callbackWorker = newSingleThreadContext("callbackWorker")
|
||||||
val channels = LongSparseArray<WeakReference<ReceiveChannel<Parcelable?>>>()
|
val channels = LongSparseArray<ReceiveChannel<Parcelable?>>()
|
||||||
|
|
||||||
// thread safety: usage of output should be guarded by callbackWorker
|
// thread safety: usage of output should be guarded by callbackWorker
|
||||||
val output = DataOutputStream(System.out.buffered().apply {
|
val output = DataOutputStream(System.out.buffered().apply {
|
||||||
@@ -396,7 +402,7 @@ class RootServer @JvmOverloads constructor(private val warnLogger: (String) -> U
|
|||||||
val callback = counter
|
val callback = counter
|
||||||
if (DEBUG) Log.d(TAG, "Received #$callback: $command")
|
if (DEBUG) Log.d(TAG, "Received #$callback: $command")
|
||||||
when (command) {
|
when (command) {
|
||||||
is ChannelClosed -> channels[command.index]?.get()?.cancel()
|
is ChannelClosed -> channels[command.index]?.cancel()
|
||||||
is RootCommandOneWay -> defaultWorker.launch {
|
is RootCommandOneWay -> defaultWorker.launch {
|
||||||
try {
|
try {
|
||||||
command.execute()
|
command.execute()
|
||||||
@@ -415,10 +421,10 @@ class RootServer @JvmOverloads constructor(private val warnLogger: (String) -> U
|
|||||||
}
|
}
|
||||||
is RootCommandChannel<*> -> defaultWorker.launch {
|
is RootCommandChannel<*> -> defaultWorker.launch {
|
||||||
val result = try {
|
val result = try {
|
||||||
command.create(defaultWorker).also {
|
coroutineScope {
|
||||||
channels[callback] = WeakReference(it)
|
command.create(this).also { channels[callback] = it }.consumeEach { result ->
|
||||||
}.consumeEach { result ->
|
withContext(callbackWorker) { output.pushResult(callback, result) }
|
||||||
withContext(callbackWorker) { output.pushResult(callback, result) }
|
}
|
||||||
};
|
};
|
||||||
@Suppress("BlockingMethodInNonBlockingContext") {
|
@Suppress("BlockingMethodInNonBlockingContext") {
|
||||||
output.writeByte(CHANNEL_CONSUMED)
|
output.writeByte(CHANNEL_CONSUMED)
|
||||||
|
|||||||
@@ -66,7 +66,10 @@ class App : Application() {
|
|||||||
if (priority != Log.DEBUG || BuildConfig.DEBUG) Log.println(priority, tag, message)
|
if (priority != Log.DEBUG || BuildConfig.DEBUG) Log.println(priority, tag, message)
|
||||||
FirebaseCrashlytics.getInstance().log("${"XXVDIWEF".getOrElse(priority) { 'X' }}/$tag: $message")
|
FirebaseCrashlytics.getInstance().log("${"XXVDIWEF".getOrElse(priority) { 'X' }}/$tag: $message")
|
||||||
} else {
|
} else {
|
||||||
if (priority >= Log.WARN || priority == Log.DEBUG) Log.println(priority, tag, message)
|
if (priority >= Log.WARN || priority == Log.DEBUG) {
|
||||||
|
Log.println(priority, tag, message)
|
||||||
|
Log.d(tag, message, t)
|
||||||
|
}
|
||||||
if (priority >= Log.INFO && t !is NoShellException) {
|
if (priority >= Log.INFO && t !is NoShellException) {
|
||||||
FirebaseCrashlytics.getInstance().recordException(t)
|
FirebaseCrashlytics.getInstance().recordException(t)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Intent
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import androidx.preference.SwitchPreference
|
import androidx.preference.SwitchPreference
|
||||||
@@ -24,6 +25,7 @@ import be.mygod.vpnhotspot.util.showAllowingStateLoss
|
|||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
|
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -48,10 +50,11 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
|||||||
if (Build.VERSION.SDK_INT >= 27) {
|
if (Build.VERSION.SDK_INT >= 27) {
|
||||||
isChecked = TetherOffloadManager.enabled
|
isChecked = TetherOffloadManager.enabled
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
if (TetherOffloadManager.enabled != newValue) GlobalScope.launch(Dispatchers.Main.immediate) {
|
if (TetherOffloadManager.enabled != newValue) viewLifecycleOwner.lifecycleScope.launchWhenCreated {
|
||||||
isEnabled = false
|
isEnabled = false
|
||||||
try {
|
try {
|
||||||
TetherOffloadManager.setEnabled(newValue as Boolean)
|
TetherOffloadManager.setEnabled(newValue as Boolean)
|
||||||
|
} catch (_: CancellationException) {
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.w(e)
|
Timber.w(e)
|
||||||
SmartSnackbar.make(e).show()
|
SmartSnackbar.make(e).show()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package be.mygod.vpnhotspot.manage
|
package be.mygod.vpnhotspot.manage
|
||||||
|
|
||||||
|
import android.annotation.TargetApi
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
@@ -18,6 +19,7 @@ import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
|
|||||||
import be.mygod.vpnhotspot.net.TetherType
|
import be.mygod.vpnhotspot.net.TetherType
|
||||||
import be.mygod.vpnhotspot.net.TetheringManager
|
import be.mygod.vpnhotspot.net.TetheringManager
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
||||||
|
import be.mygod.vpnhotspot.root.WifiApCommands
|
||||||
import be.mygod.vpnhotspot.util.readableMessage
|
import be.mygod.vpnhotspot.util.readableMessage
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -67,7 +69,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
inner class Data : be.mygod.vpnhotspot.manage.Data() {
|
inner class Data : be.mygod.vpnhotspot.manage.Data() {
|
||||||
override val icon get() = tetherType.icon
|
override val icon get() = tetherType.icon
|
||||||
override val title get() = this@TetherManager.title
|
override val title get() = this@TetherManager.title
|
||||||
override var text: CharSequence = ""
|
override val text get() = error
|
||||||
override val active get() = isStarted
|
override val active get() = isStarted
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +77,10 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
abstract val title: CharSequence
|
abstract val title: CharSequence
|
||||||
abstract val tetherType: TetherType
|
abstract val tetherType: TetherType
|
||||||
open val isStarted get() = parent.enabledTypes.contains(tetherType)
|
open val isStarted get() = parent.enabledTypes.contains(tetherType)
|
||||||
|
protected open val error: CharSequence get() = baseError ?: ""
|
||||||
|
|
||||||
|
protected var baseError: String? = null
|
||||||
|
private set
|
||||||
|
|
||||||
protected abstract fun start()
|
protected abstract fun start()
|
||||||
protected abstract fun stop()
|
protected abstract fun stop()
|
||||||
@@ -98,27 +104,53 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
(viewHolder as ViewHolder).manager = this
|
(viewHolder as ViewHolder).manager = this
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getErrorMessage(iface: String): String {
|
|
||||||
return TetheringManager.tetherErrorMessage(try {
|
|
||||||
TetheringManager.getLastTetherError(iface)
|
|
||||||
} catch (e: InvocationTargetException) {
|
|
||||||
if (Build.VERSION.SDK_INT !in 24..25 || e.cause !is SecurityException) Timber.w(e) else Timber.d(e)
|
|
||||||
return e.readableMessage
|
|
||||||
})
|
|
||||||
}
|
|
||||||
protected open fun makeErrorMessage(errored: List<String>): CharSequence = errored
|
|
||||||
.filter { TetherType.ofInterface(it) == tetherType }
|
|
||||||
.joinToString("\n") { "$it: ${getErrorMessage(it)}" }
|
|
||||||
fun updateErrorMessage(errored: List<String>) {
|
fun updateErrorMessage(errored: List<String>) {
|
||||||
data.text = makeErrorMessage(errored)
|
val interested = errored.filter { TetherType.ofInterface(it) == tetherType }
|
||||||
data.notifyChange()
|
baseError = if (interested.isEmpty()) null else interested.joinToString("\n") { iface ->
|
||||||
|
"$iface: " + try {
|
||||||
|
TetheringManager.tetherErrorMessage(TetheringManager.getLastTetherError(iface))
|
||||||
|
} catch (e: InvocationTargetException) {
|
||||||
|
if (Build.VERSION.SDK_INT !in 24..25 || e.cause !is SecurityException) Timber.w(e) else Timber.d(e)
|
||||||
|
e.readableMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(24)
|
@RequiresApi(24)
|
||||||
class Wifi(parent: TetheringFragment) : TetherManager(parent) {
|
class Wifi(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver,
|
||||||
|
WifiApManager.SoftApCallbackCompat {
|
||||||
|
private var failureReason: Int? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (Build.VERSION.SDK_INT >= 28) parent.viewLifecycleOwner.lifecycle.addObserver(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(28)
|
||||||
|
override fun onStart(owner: LifecycleOwner) {
|
||||||
|
WifiApCommands.registerSoftApCallback(this)
|
||||||
|
}
|
||||||
|
@TargetApi(28)
|
||||||
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
|
WifiApCommands.unregisterSoftApCallback(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStateChanged(state: Int, failureReason: Int) {
|
||||||
|
if (state < 10 || state > 14) {
|
||||||
|
Timber.w(Exception("Unknown state $state"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val newReason = if (state == 14) failureReason else null
|
||||||
|
if (this.failureReason != newReason) {
|
||||||
|
this.failureReason = newReason
|
||||||
|
data.notifyChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override val title get() = parent.getString(R.string.tethering_manage_wifi)
|
override val title get() = parent.getString(R.string.tethering_manage_wifi)
|
||||||
override val tetherType get() = TetherType.WIFI
|
override val tetherType get() = TetherType.WIFI
|
||||||
override val type get() = VIEW_TYPE_WIFI
|
override val type get() = VIEW_TYPE_WIFI
|
||||||
|
override val error get() = listOfNotNull(failureReason?.let { WifiApManager.failureReason(it) },
|
||||||
|
baseError).joinToString("\n")
|
||||||
|
|
||||||
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this)
|
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this)
|
||||||
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI, this::onException)
|
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI, this::onException)
|
||||||
@@ -134,10 +166,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
}
|
}
|
||||||
@RequiresApi(24)
|
@RequiresApi(24)
|
||||||
class Bluetooth(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver {
|
class Bluetooth(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver {
|
||||||
private val tethering = BluetoothTethering(parent.requireContext()) {
|
private val tethering = BluetoothTethering(parent.requireContext()) { data.notifyChange() }
|
||||||
data.text = makeErrorMessage()
|
|
||||||
data.notifyChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
parent.viewLifecycleOwner.lifecycle.addObserver(this)
|
parent.viewLifecycleOwner.lifecycle.addObserver(this)
|
||||||
@@ -149,15 +178,9 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
override val tetherType get() = TetherType.BLUETOOTH
|
override val tetherType get() = TetherType.BLUETOOTH
|
||||||
override val type get() = VIEW_TYPE_BLUETOOTH
|
override val type get() = VIEW_TYPE_BLUETOOTH
|
||||||
override val isStarted get() = tethering.active == true
|
override val isStarted get() = tethering.active == true
|
||||||
|
override val error get() = listOfNotNull(
|
||||||
private var baseError: CharSequence? = null
|
|
||||||
private fun makeErrorMessage(): CharSequence = listOfNotNull(
|
|
||||||
if (tethering.active == null) tethering.activeFailureCause?.readableMessage else null,
|
if (tethering.active == null) tethering.activeFailureCause?.readableMessage else null,
|
||||||
baseError).joinToString("\n")
|
baseError).joinToString("\n")
|
||||||
override fun makeErrorMessage(errored: List<String>): CharSequence {
|
|
||||||
baseError = super.makeErrorMessage(errored).let { if (it.isEmpty()) null else it }
|
|
||||||
return makeErrorMessage()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun start() = BluetoothTethering.start(this)
|
override fun start() = BluetoothTethering.start(this)
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import be.mygod.vpnhotspot.root.RootManager
|
|||||||
import be.mygod.vpnhotspot.root.WifiApCommands
|
import be.mygod.vpnhotspot.root.WifiApCommands
|
||||||
import be.mygod.vpnhotspot.util.*
|
import be.mygod.vpnhotspot.util.*
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
@@ -188,6 +189,8 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
|||||||
if (e.targetException !is SecurityException) Timber.w(e)
|
if (e.targetException !is SecurityException) Timber.w(e)
|
||||||
try {
|
try {
|
||||||
RootManager.use { it.execute(WifiApCommands.GetConfiguration()) }
|
RootManager.use { it.execute(WifiApCommands.GetConfiguration()) }
|
||||||
|
} catch (_: CancellationException) {
|
||||||
|
null
|
||||||
} catch (eRoot: Exception) {
|
} catch (eRoot: Exception) {
|
||||||
eRoot.addSuppressed(e)
|
eRoot.addSuppressed(e)
|
||||||
Timber.w(eRoot)
|
Timber.w(eRoot)
|
||||||
@@ -216,6 +219,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
|||||||
} catch (e: InvocationTargetException) {
|
} catch (e: InvocationTargetException) {
|
||||||
try {
|
try {
|
||||||
RootManager.use { it.execute(WifiApCommands.SetConfiguration(ret!!.configuration)) }
|
RootManager.use { it.execute(WifiApCommands.SetConfiguration(ret!!.configuration)) }
|
||||||
|
} catch (_: CancellationException) {
|
||||||
} catch (eRoot: Exception) {
|
} catch (eRoot: Exception) {
|
||||||
eRoot.addSuppressed(e)
|
eRoot.addSuppressed(e)
|
||||||
Timber.w(eRoot)
|
Timber.w(eRoot)
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ import be.mygod.vpnhotspot.R
|
|||||||
import be.mygod.vpnhotspot.root.RootManager
|
import be.mygod.vpnhotspot.root.RootManager
|
||||||
import be.mygod.vpnhotspot.root.StartTethering
|
import be.mygod.vpnhotspot.root.StartTethering
|
||||||
import be.mygod.vpnhotspot.root.StopTethering
|
import be.mygod.vpnhotspot.root.StopTethering
|
||||||
import be.mygod.vpnhotspot.util.Services
|
import be.mygod.vpnhotspot.util.*
|
||||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
|
||||||
import be.mygod.vpnhotspot.util.callSuper
|
|
||||||
import be.mygod.vpnhotspot.util.ensureReceiverUnregistered
|
|
||||||
import com.android.dx.stock.ProxyBuilder
|
import com.android.dx.stock.ProxyBuilder
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@@ -225,8 +222,6 @@ object TetheringManager {
|
|||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val stopTethering by lazy { clazz.getDeclaredMethod("stopTethering", Int::class.java) }
|
private val stopTethering by lazy { clazz.getDeclaredMethod("stopTethering", Int::class.java) }
|
||||||
|
|
||||||
private fun Handler?.makeExecutor() = Executor { if (this == null) it.run() else post(it) }
|
|
||||||
|
|
||||||
@Deprecated("Legacy API")
|
@Deprecated("Legacy API")
|
||||||
@RequiresApi(24)
|
@RequiresApi(24)
|
||||||
fun startTetheringLegacy(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
|
fun startTetheringLegacy(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
|
||||||
|
|||||||
@@ -3,13 +3,25 @@ package be.mygod.vpnhotspot.net.wifi
|
|||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.MacAddress
|
||||||
import android.net.wifi.SoftApConfiguration
|
import android.net.wifi.SoftApConfiguration
|
||||||
import android.net.wifi.WifiManager
|
import android.net.wifi.WifiManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.collection.SparseArrayCompat
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
|
import be.mygod.vpnhotspot.R
|
||||||
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
|
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
|
||||||
import be.mygod.vpnhotspot.util.Services
|
import be.mygod.vpnhotspot.util.Services
|
||||||
|
import be.mygod.vpnhotspot.util.callSuper
|
||||||
|
import be.mygod.vpnhotspot.util.makeExecutor
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.lang.reflect.InvocationHandler
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import java.lang.reflect.Proxy
|
||||||
|
import java.util.concurrent.Executor
|
||||||
|
|
||||||
object WifiApManager {
|
object WifiApManager {
|
||||||
/**
|
/**
|
||||||
@@ -48,9 +60,153 @@ object WifiApManager {
|
|||||||
setWifiApConfiguration(Services.wifi, value.toWifiConfiguration())
|
setWifiApConfiguration(Services.wifi, value.toWifiConfiguration())
|
||||||
} else setSoftApConfiguration(Services.wifi, value.toPlatform())) as Boolean
|
} else setSoftApConfiguration(Services.wifi, value.toPlatform())) as Boolean
|
||||||
|
|
||||||
|
@RequiresApi(28)
|
||||||
|
interface SoftApCallbackCompat {
|
||||||
|
/**
|
||||||
|
* Called when soft AP state changes.
|
||||||
|
*
|
||||||
|
* @param state new new AP state. One of {@link #WIFI_AP_STATE_DISABLED},
|
||||||
|
* {@link #WIFI_AP_STATE_DISABLING}, {@link #WIFI_AP_STATE_ENABLED},
|
||||||
|
* {@link #WIFI_AP_STATE_ENABLING}, {@link #WIFI_AP_STATE_FAILED}
|
||||||
|
* @param failureReason reason when in failed state. One of
|
||||||
|
* {@link #SAP_START_FAILURE_GENERAL}, {@link #SAP_START_FAILURE_NO_CHANNEL}
|
||||||
|
*/
|
||||||
|
fun onStateChanged(state: Int, failureReason: Int) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when number of connected clients to soft AP changes.
|
||||||
|
*
|
||||||
|
* @param numClients number of connected clients
|
||||||
|
*/
|
||||||
|
@Deprecated("onConnectedClientsChanged")
|
||||||
|
fun onNumClientsChanged(numClients: Int) { }
|
||||||
|
|
||||||
|
@RequiresApi(30)
|
||||||
|
fun onConnectedClientsChanged(clients: List<MacAddress>) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
onNumClientsChanged(clients.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(30)
|
||||||
|
fun onInfoChanged(frequency: Int, bandwidth: Int) { }
|
||||||
|
|
||||||
|
@RequiresApi(30)
|
||||||
|
fun onCapabilityChanged(maxSupportedClients: Int, supportedFeatures: Long) { }
|
||||||
|
|
||||||
|
@RequiresApi(30)
|
||||||
|
fun onBlockedClientConnecting(client: MacAddress, blockedReason: Int) { }
|
||||||
|
}
|
||||||
|
private val startFailures29 = arrayOf("SAP_START_FAILURE_GENERAL", "SAP_START_FAILURE_NO_CHANNEL")
|
||||||
|
private val startFailures by lazy {
|
||||||
|
SparseArrayCompat<String>().apply {
|
||||||
|
for (field in WifiManager::class.java.declaredFields) try {
|
||||||
|
// all SAP_START_FAILURE_* are system-api since API 30
|
||||||
|
if (field.name.startsWith("SAP_START_FAILURE_")) put(field.get(null) as Int, field.name)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun failureReason(reason: Int): String {
|
||||||
|
if (Build.VERSION.SDK_INT >= 30) try {
|
||||||
|
startFailures.get(reason)?.let { return it }
|
||||||
|
} catch (e: ReflectiveOperationException) {
|
||||||
|
Timber.w(e)
|
||||||
|
}
|
||||||
|
return startFailures29.getOrNull(reason) ?: app.getString(R.string.failure_reason_unknown, reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val interfaceSoftApCallback by lazy { Class.forName("android.net.wifi.WifiManager\$SoftApCallback") }
|
||||||
|
private val registerSoftApCallback by lazy {
|
||||||
|
val parameters = if (Build.VERSION.SDK_INT >= 30) {
|
||||||
|
arrayOf(Executor::class.java, interfaceSoftApCallback)
|
||||||
|
} else arrayOf(interfaceSoftApCallback, Handler::class.java)
|
||||||
|
WifiManager::class.java.getDeclaredMethod("registerSoftApCallback", *parameters)
|
||||||
|
}
|
||||||
|
private val unregisterSoftApCallback by lazy {
|
||||||
|
WifiManager::class.java.getDeclaredMethod("unregisterSoftApCallback", interfaceSoftApCallback)
|
||||||
|
}
|
||||||
|
private val getMacAddress by lazy {
|
||||||
|
Class.forName("android.net.wifi.WifiClient").getDeclaredMethod("getMacAddress")
|
||||||
|
}
|
||||||
|
private val classSoftApInfo by lazy { Class.forName("android.net.wifi.SoftApInfo") }
|
||||||
|
private val getFrequency by lazy { classSoftApInfo.getDeclaredMethod("getFrequency") }
|
||||||
|
private val getBandwidth by lazy { classSoftApInfo.getDeclaredMethod("getBandwidth") }
|
||||||
|
private val classSoftApCapability by lazy { Class.forName("android.net.wifi.SoftApCapability") }
|
||||||
|
private val getMaxSupportedClients by lazy { classSoftApCapability.getDeclaredMethod("getMaxSupportedClients") }
|
||||||
|
private val areFeaturesSupported by lazy {
|
||||||
|
classSoftApCapability.getDeclaredMethod("areFeaturesSupported", Long::class.java)
|
||||||
|
}
|
||||||
|
@RequiresApi(28)
|
||||||
|
fun registerSoftApCallback(callback: SoftApCallbackCompat, executor: Executor): Any {
|
||||||
|
val proxy = Proxy.newProxyInstance(interfaceSoftApCallback.classLoader,
|
||||||
|
arrayOf(interfaceSoftApCallback), object : InvocationHandler {
|
||||||
|
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
|
||||||
|
return if (Build.VERSION.SDK_INT >= 30) invokeActual(proxy, method, args) else {
|
||||||
|
executor.execute { invokeActual(proxy, method, args) }
|
||||||
|
null // no return value as of API 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun invokeActual(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
|
||||||
|
val noArgs = args?.size ?: 0
|
||||||
|
return when (val name = method.name) {
|
||||||
|
"onStateChanged" -> {
|
||||||
|
if (noArgs != 2) Timber.w("Unexpected args for $name: $args")
|
||||||
|
callback.onStateChanged(args!![0] as Int, args[1] as Int)
|
||||||
|
}
|
||||||
|
"onNumClientsChanged" -> @Suppress("DEPRECATION") {
|
||||||
|
if (Build.VERSION.SDK_INT >= 30) Timber.w(Exception("Unexpected onNumClientsChanged"))
|
||||||
|
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
|
||||||
|
callback.onNumClientsChanged(args!![0] as Int)
|
||||||
|
}
|
||||||
|
"onConnectedClientsChanged" -> @TargetApi(30) {
|
||||||
|
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onConnectedClientsChanged"))
|
||||||
|
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
|
||||||
|
callback.onConnectedClientsChanged((args!![0] as Iterable<*>)
|
||||||
|
.map { getMacAddress(it) as MacAddress })
|
||||||
|
}
|
||||||
|
"onInfoChanged" -> @TargetApi(30) {
|
||||||
|
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onInfoChanged"))
|
||||||
|
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
|
||||||
|
val softApInfo = args!![0]
|
||||||
|
callback.onInfoChanged(getFrequency(softApInfo) as Int, getBandwidth(softApInfo) as Int)
|
||||||
|
}
|
||||||
|
"onCapabilityChanged" -> @TargetApi(30) {
|
||||||
|
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onCapabilityChanged"))
|
||||||
|
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
|
||||||
|
val softApCapability = args!![0]
|
||||||
|
var supportedFeatures = 0L
|
||||||
|
var probe = 1L
|
||||||
|
while (probe != 0L) {
|
||||||
|
if (areFeaturesSupported(softApCapability, probe) as Boolean) {
|
||||||
|
supportedFeatures = supportedFeatures or probe
|
||||||
|
}
|
||||||
|
probe += probe
|
||||||
|
}
|
||||||
|
callback.onCapabilityChanged(getMaxSupportedClients(softApCapability) as Int, supportedFeatures)
|
||||||
|
}
|
||||||
|
"onBlockedClientConnecting" -> @TargetApi(30) {
|
||||||
|
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onBlockedClientConnecting"))
|
||||||
|
if (noArgs != 2) Timber.w("Unexpected args for $name: $args")
|
||||||
|
callback.onBlockedClientConnecting(getMacAddress(args!![0]) as MacAddress, args[1] as Int)
|
||||||
|
}
|
||||||
|
else -> callSuper(interfaceSoftApCallback, proxy, method, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (Build.VERSION.SDK_INT >= 30) {
|
||||||
|
registerSoftApCallback(Services.wifi, executor, proxy)
|
||||||
|
} else registerSoftApCallback(Services.wifi, proxy, null)
|
||||||
|
return proxy
|
||||||
|
}
|
||||||
|
@RequiresApi(28)
|
||||||
|
fun unregisterSoftApCallback(key: Any) = unregisterSoftApCallback(Services.wifi, key)
|
||||||
|
|
||||||
private val cancelLocalOnlyHotspotRequest by lazy {
|
private val cancelLocalOnlyHotspotRequest by lazy {
|
||||||
WifiManager::class.java.getDeclaredMethod("cancelLocalOnlyHotspotRequest")
|
WifiManager::class.java.getDeclaredMethod("cancelLocalOnlyHotspotRequest")
|
||||||
}
|
}
|
||||||
|
@RequiresApi(26)
|
||||||
fun cancelLocalOnlyHotspotRequest() = cancelLocalOnlyHotspotRequest(Services.wifi)
|
fun cancelLocalOnlyHotspotRequest() = cancelLocalOnlyHotspotRequest(Services.wifi)
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package be.mygod.vpnhotspot.root
|
package be.mygod.vpnhotspot.root
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import android.util.Log
|
||||||
import be.mygod.librootkotlinx.RootCommandNoResult
|
import be.mygod.librootkotlinx.RootCommandNoResult
|
||||||
import be.mygod.librootkotlinx.RootServer
|
import be.mygod.librootkotlinx.RootServer
|
||||||
import be.mygod.librootkotlinx.RootSession
|
import be.mygod.librootkotlinx.RootSession
|
||||||
@@ -16,6 +17,16 @@ object RootManager : RootSession() {
|
|||||||
class RootInit : RootCommandNoResult {
|
class RootInit : RootCommandNoResult {
|
||||||
override suspend fun execute(): Parcelable? {
|
override suspend fun execute(): Parcelable? {
|
||||||
RootServer.DEBUG = BuildConfig.DEBUG
|
RootServer.DEBUG = BuildConfig.DEBUG
|
||||||
|
Timber.plant(object : Timber.DebugTree() {
|
||||||
|
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||||
|
if (t == null) {
|
||||||
|
Log.println(priority, tag, message)
|
||||||
|
} else {
|
||||||
|
Log.println(priority, tag, message)
|
||||||
|
Log.d(tag, message, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Services.init(RootJava.getSystemContext())
|
Services.init(RootJava.getSystemContext())
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,162 @@
|
|||||||
package be.mygod.vpnhotspot.root
|
package be.mygod.vpnhotspot.root
|
||||||
|
|
||||||
|
import android.net.MacAddress
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import be.mygod.librootkotlinx.ParcelableBoolean
|
import be.mygod.librootkotlinx.ParcelableBoolean
|
||||||
import be.mygod.librootkotlinx.RootCommand
|
import be.mygod.librootkotlinx.RootCommand
|
||||||
|
import be.mygod.librootkotlinx.RootCommandChannel
|
||||||
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
|
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
||||||
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
|
import kotlinx.coroutines.channels.consumeEach
|
||||||
|
import kotlinx.coroutines.channels.produce
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.Executor
|
||||||
|
|
||||||
object WifiApCommands {
|
object WifiApCommands {
|
||||||
|
@RequiresApi(28)
|
||||||
|
sealed class SoftApCallbackParcel : Parcelable {
|
||||||
|
abstract fun dispatch(callback: WifiApManager.SoftApCallbackCompat)
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class OnStateChanged(val state: Int, val failureReason: Int) : SoftApCallbackParcel() {
|
||||||
|
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
|
||||||
|
callback.onStateChanged(state, failureReason)
|
||||||
|
}
|
||||||
|
@Parcelize
|
||||||
|
data class OnNumClientsChanged(val numClients: Int) : SoftApCallbackParcel() {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
|
||||||
|
callback.onNumClientsChanged(numClients)
|
||||||
|
}
|
||||||
|
@Parcelize
|
||||||
|
@RequiresApi(30)
|
||||||
|
data class OnConnectedClientsChanged(val clients: List<MacAddress>) : SoftApCallbackParcel() {
|
||||||
|
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
|
||||||
|
callback.onConnectedClientsChanged(clients)
|
||||||
|
}
|
||||||
|
@Parcelize
|
||||||
|
@RequiresApi(30)
|
||||||
|
data class OnInfoChanged(val frequency: Int, val bandwidth: Int) : SoftApCallbackParcel() {
|
||||||
|
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
|
||||||
|
callback.onInfoChanged(frequency, bandwidth)
|
||||||
|
}
|
||||||
|
@Parcelize
|
||||||
|
@RequiresApi(30)
|
||||||
|
data class OnCapabilityChanged(val maxSupportedClients: Int,
|
||||||
|
val supportedFeatures: Long) : SoftApCallbackParcel() {
|
||||||
|
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
|
||||||
|
callback.onCapabilityChanged(maxSupportedClients, supportedFeatures)
|
||||||
|
}
|
||||||
|
@Parcelize
|
||||||
|
@RequiresApi(30)
|
||||||
|
data class OnBlockedClientConnecting(val client: MacAddress, val blockedReason: Int) : SoftApCallbackParcel() {
|
||||||
|
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
|
||||||
|
callback.onBlockedClientConnecting(client, blockedReason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
@RequiresApi(28)
|
||||||
|
class RegisterSoftApCallback : RootCommandChannel<SoftApCallbackParcel> {
|
||||||
|
override fun create(scope: CoroutineScope) = scope.produce(capacity = capacity) {
|
||||||
|
val finish = CompletableDeferred<Unit>()
|
||||||
|
val key = WifiApManager.registerSoftApCallback(object : WifiApManager.SoftApCallbackCompat {
|
||||||
|
private fun push(parcel: SoftApCallbackParcel) = check(try {
|
||||||
|
offer(parcel)
|
||||||
|
} catch (closed: Throwable) {
|
||||||
|
finish.completeExceptionally(closed)
|
||||||
|
true
|
||||||
|
})
|
||||||
|
|
||||||
|
override fun onStateChanged(state: Int, failureReason: Int) =
|
||||||
|
push(SoftApCallbackParcel.OnStateChanged(state, failureReason))
|
||||||
|
@Suppress("OverridingDeprecatedMember")
|
||||||
|
override fun onNumClientsChanged(numClients: Int) =
|
||||||
|
push(SoftApCallbackParcel.OnNumClientsChanged(numClients))
|
||||||
|
@RequiresApi(30)
|
||||||
|
override fun onConnectedClientsChanged(clients: List<MacAddress>) =
|
||||||
|
push(SoftApCallbackParcel.OnConnectedClientsChanged(clients))
|
||||||
|
@RequiresApi(30)
|
||||||
|
override fun onInfoChanged(frequency: Int, bandwidth: Int) =
|
||||||
|
push(SoftApCallbackParcel.OnInfoChanged(frequency, bandwidth))
|
||||||
|
@RequiresApi(30)
|
||||||
|
override fun onCapabilityChanged(maxSupportedClients: Int, supportedFeatures: Long) =
|
||||||
|
push(SoftApCallbackParcel.OnCapabilityChanged(maxSupportedClients, supportedFeatures))
|
||||||
|
@RequiresApi(30)
|
||||||
|
override fun onBlockedClientConnecting(client: MacAddress, blockedReason: Int) =
|
||||||
|
push(SoftApCallbackParcel.OnBlockedClientConnecting(client, blockedReason))
|
||||||
|
}, Executor {
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
it.run()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
finish.completeExceptionally(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
finish.await()
|
||||||
|
} finally {
|
||||||
|
WifiApManager.unregisterSoftApCallback(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class AutoFiringCallbacks(
|
||||||
|
var state: SoftApCallbackParcel.OnStateChanged? = null,
|
||||||
|
var numClients: SoftApCallbackParcel.OnNumClientsChanged? = null,
|
||||||
|
var connectedClients: SoftApCallbackParcel.OnConnectedClientsChanged? = null,
|
||||||
|
var info: SoftApCallbackParcel.OnInfoChanged? = null,
|
||||||
|
var capability: SoftApCallbackParcel.OnCapabilityChanged? = null) {
|
||||||
|
fun toSequence() = sequenceOf(state, numClients, connectedClients, info, capability)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val callbacks = mutableSetOf<WifiApManager.SoftApCallbackCompat>()
|
||||||
|
private val lastCallback = AutoFiringCallbacks()
|
||||||
|
private var rootCallbackJob: Job? = null
|
||||||
|
@RequiresApi(28)
|
||||||
|
private suspend fun handleChannel(channel: ReceiveChannel<SoftApCallbackParcel>) = channel.consumeEach { parcel ->
|
||||||
|
when (parcel) {
|
||||||
|
is SoftApCallbackParcel.OnStateChanged -> synchronized(callbacks) { lastCallback.state = parcel }
|
||||||
|
is SoftApCallbackParcel.OnNumClientsChanged -> synchronized(callbacks) { lastCallback.numClients = parcel }
|
||||||
|
is SoftApCallbackParcel.OnConnectedClientsChanged -> synchronized(callbacks) {
|
||||||
|
lastCallback.connectedClients = parcel
|
||||||
|
}
|
||||||
|
is SoftApCallbackParcel.OnInfoChanged -> synchronized(callbacks) { lastCallback.info = parcel }
|
||||||
|
is SoftApCallbackParcel.OnCapabilityChanged -> synchronized(callbacks) { lastCallback.capability = parcel }
|
||||||
|
}
|
||||||
|
for (callback in synchronized(callbacks) { callbacks }) parcel.dispatch(callback)
|
||||||
|
}
|
||||||
|
@RequiresApi(28)
|
||||||
|
fun registerSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) {
|
||||||
|
val wasEmpty = callbacks.isEmpty()
|
||||||
|
callbacks.add(callback)
|
||||||
|
if (wasEmpty) {
|
||||||
|
rootCallbackJob = GlobalScope.launch {
|
||||||
|
try {
|
||||||
|
RootManager.use { server -> handleChannel(server.create(RegisterSoftApCallback(), this)) }
|
||||||
|
} catch (_: CancellationException) {
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w(e)
|
||||||
|
SmartSnackbar.make(e).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastCallback
|
||||||
|
} else null
|
||||||
|
}?.toSequence()?.forEach { it?.dispatch(callback) }
|
||||||
|
@RequiresApi(28)
|
||||||
|
fun unregisterSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) {
|
||||||
|
if (callbacks.remove(callback) && callbacks.isEmpty()) {
|
||||||
|
rootCallbackJob!!.cancel()
|
||||||
|
rootCallbackJob = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class GetConfiguration : RootCommand<SoftApConfigurationCompat> {
|
class GetConfiguration : RootCommand<SoftApConfigurationCompat> {
|
||||||
override suspend fun execute() = WifiApManager.configuration
|
override suspend fun execute() = WifiApManager.configuration
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.annotation.TargetApi
|
|||||||
import android.content.*
|
import android.content.*
|
||||||
import android.net.InetAddresses
|
import android.net.InetAddresses
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
@@ -28,6 +29,7 @@ import java.lang.reflect.Method
|
|||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.NetworkInterface
|
import java.net.NetworkInterface
|
||||||
import java.net.SocketException
|
import java.net.SocketException
|
||||||
|
import java.util.concurrent.Executor
|
||||||
|
|
||||||
val Throwable.readableMessage: String get() = if (this is InvocationTargetException) {
|
val Throwable.readableMessage: String get() = if (this is InvocationTargetException) {
|
||||||
targetException.readableMessage
|
targetException.readableMessage
|
||||||
@@ -49,6 +51,8 @@ fun Context.ensureReceiverUnregistered(receiver: BroadcastReceiver) {
|
|||||||
} catch (_: IllegalArgumentException) { }
|
} catch (_: IllegalArgumentException) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Handler?.makeExecutor() = Executor { if (this == null) it.run() else post(it) }
|
||||||
|
|
||||||
fun DialogFragment.showAllowingStateLoss(manager: FragmentManager, tag: String? = null) {
|
fun DialogFragment.showAllowingStateLoss(manager: FragmentManager, tag: String? = null) {
|
||||||
if (!manager.isStateSaved) show(manager, tag)
|
if (!manager.isStateSaved) show(manager, tag)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user