Files
vpnhotspotmod/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt

641 lines
30 KiB
Kotlin

package be.mygod.vpnhotspot.net
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.Network
import android.os.Build
import android.os.DeadObjectException
import android.os.Handler
import androidx.annotation.RequiresApi
import androidx.core.os.ExecutorCompat
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.root.StartTethering
import be.mygod.vpnhotspot.root.StopTethering
import be.mygod.vpnhotspot.util.*
import com.android.dx.stock.ProxyBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
import java.lang.ref.WeakReference
import java.lang.reflect.InvocationHandler
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import java.lang.reflect.Proxy
import java.util.concurrent.CancellationException
import java.util.concurrent.Executor
/**
* Heavily based on:
* https://github.com/aegis1980/WifiHotSpot
* https://android.googlesource.com/platform/frameworks/base.git/+/android-7.0.0_r1/core/java/android/net/ConnectivityManager.java
*/
object TetheringManager {
/**
* Callback for use with [startTethering] to find out whether tethering succeeded.
*/
interface StartTetheringCallback {
/**
* Called when tethering has been successfully started.
*/
fun onTetheringStarted() { }
/**
* Called when starting tethering failed.
*
* @param error The error that caused the failure.
*/
fun onTetheringFailed(error: Int? = null) { }
/**
* ADDED: Called when a local Exception occurred.
*/
fun onException(e: Exception) {
when (e.getRootCause()) {
is SecurityException, is CancellationException -> { }
else -> Timber.w(e)
}
}
}
private object InPlaceExecutor : Executor {
override fun execute(command: Runnable) = try {
command.run()
} catch (e: Exception) {
Timber.w(e) // prevent Binder stub swallowing the exception
}
}
/**
* Use with {@link #getSystemService(String)} to retrieve a {@link android.net.TetheringManager}
* for managing tethering functions.
* @hide
* @see android.net.TetheringManager
*/
@RequiresApi(30)
const val TETHERING_SERVICE = "tethering"
@RequiresApi(30)
private const val TETHERING_CONNECTOR_CLASS = "android.net.ITetheringConnector"
@RequiresApi(30)
private const val IN_PROCESS_SUFFIX = ".InProcess"
/**
* This is a sticky broadcast since almost forever.
*
* https://android.googlesource.com/platform/frameworks/base.git/+/2a091d7aa0c174986387e5d56bf97a87fe075bdb%5E%21/services/java/com/android/server/connectivity/Tethering.java
*/
const val ACTION_TETHER_STATE_CHANGED = "android.net.conn.TETHER_STATE_CHANGED"
private const val EXTRA_ACTIVE_LOCAL_ONLY_LEGACY = "localOnlyArray"
/**
* gives a String[] listing all the interfaces currently in local-only
* mode (ie, has DHCPv4+IPv6-ULA support and no packet forwarding)
*/
@RequiresApi(30)
private const val EXTRA_ACTIVE_LOCAL_ONLY = "android.net.extra.ACTIVE_LOCAL_ONLY"
/**
* gives a String[] listing all the interfaces currently tethered
* (ie, has DHCPv4 support and packets potentially forwarded/NATed)
*/
private const val EXTRA_ACTIVE_TETHER = "tetherArray"
/**
* gives a String[] listing all the interfaces we tried to tether and
* failed. Use [getLastTetherError] to find the error code
* for any interfaces listed here.
*/
const val EXTRA_ERRORED_TETHER = "erroredArray"
/** Tethering offload status is stopped. */
@RequiresApi(30)
const val TETHER_HARDWARE_OFFLOAD_STOPPED = 0
/** Tethering offload status is started. */
@RequiresApi(30)
const val TETHER_HARDWARE_OFFLOAD_STARTED = 1
/** Fail to start tethering offload. */
@RequiresApi(30)
const val TETHER_HARDWARE_OFFLOAD_FAILED = 2
// tethering types supported by enableTetheringInternal: https://android.googlesource.com/platform/frameworks/base/+/5d36f01/packages/Tethering/src/com/android/networkstack/tethering/Tethering.java#549
/**
* Wifi tethering type.
* @see [startTethering].
*/
const val TETHERING_WIFI = 0
/**
* USB tethering type.
*
* Requires MANAGE_USB permission, unfortunately.
*
* Source: https://android.googlesource.com/platform/frameworks/base/+/7ca5d3a/services/usb/java/com/android/server/usb/UsbService.java#389
* @see startTethering
*/
const val TETHERING_USB = 1
/**
* Bluetooth tethering type.
*
* Requires BLUETOOTH permission.
* @see startTethering
*/
const val TETHERING_BLUETOOTH = 2
/**
* Ethernet tethering type.
*
* Requires MANAGE_USB permission, also.
* @see startTethering
*/
@RequiresApi(30)
const val TETHERING_ETHERNET = 5
@get:RequiresApi(30)
private val clazz by lazy { Class.forName("android.net.TetheringManager") }
@get:RequiresApi(30)
private val instance by lazy @TargetApi(30) {
@SuppressLint("WrongConstant") // hidden services are not included in constants as of R preview 4
val service = Services.context.getSystemService(TETHERING_SERVICE)!!
service
}
@get:RequiresApi(30)
val resolvedService get() = sequence {
for (action in arrayOf(TETHERING_CONNECTOR_CLASS + IN_PROCESS_SUFFIX, TETHERING_CONNECTOR_CLASS)) {
val result = app.packageManager.queryIntentServices(Intent(action), PackageManager.MATCH_SYSTEM_ONLY)
check(result.size <= 1) { "Multiple system services handle $action: ${result.joinToString()}" }
result.firstOrNull()?.let { yield(it) }
}
}.first()
private val classOnStartTetheringCallback by lazy {
Class.forName("android.net.ConnectivityManager\$OnStartTetheringCallback")
}
private val startTetheringLegacy by lazy {
ConnectivityManager::class.java.getDeclaredMethod("startTethering",
Int::class.java, Boolean::class.java, classOnStartTetheringCallback, Handler::class.java)
}
private val stopTetheringLegacy by lazy {
ConnectivityManager::class.java.getDeclaredMethod("stopTethering", Int::class.java)
}
private val getLastTetherError by lazy @SuppressLint("SoonBlockedPrivateApi") {
ConnectivityManager::class.java.getDeclaredMethod("getLastTetherError", String::class.java)
}
@get:RequiresApi(30)
private val classTetheringRequestBuilder by lazy {
Class.forName("android.net.TetheringManager\$TetheringRequest\$Builder")
}
@get:RequiresApi(30)
private val newTetheringRequestBuilder by lazy @TargetApi(30) {
classTetheringRequestBuilder.getConstructor(Int::class.java)
}
// @get:RequiresApi(30)
// private val setStaticIpv4Addresses by lazy {
// classTetheringRequestBuilder.getDeclaredMethod("setStaticIpv4Addresses",
// LinkAddress::class.java, LinkAddress::class.java)
// }
@get:RequiresApi(30)
private val setExemptFromEntitlementCheck by lazy @TargetApi(30) {
classTetheringRequestBuilder.getDeclaredMethod("setExemptFromEntitlementCheck", Boolean::class.java)
}
@get:RequiresApi(30)
private val setShouldShowEntitlementUi by lazy @TargetApi(30) {
classTetheringRequestBuilder.getDeclaredMethod("setShouldShowEntitlementUi", Boolean::class.java)
}
@get:RequiresApi(30)
private val build by lazy @TargetApi(30) { classTetheringRequestBuilder.getDeclaredMethod("build") }
@get:RequiresApi(30)
private val interfaceStartTetheringCallback by lazy {
Class.forName("android.net.TetheringManager\$StartTetheringCallback")
}
@get:RequiresApi(30)
private val startTethering by lazy @TargetApi(30) {
clazz.getDeclaredMethod("startTethering", Class.forName("android.net.TetheringManager\$TetheringRequest"),
Executor::class.java, interfaceStartTetheringCallback)
}
@get:RequiresApi(30)
private val stopTethering by lazy @TargetApi(30) { clazz.getDeclaredMethod("stopTethering", Int::class.java) }
@Deprecated("Legacy API")
fun startTetheringLegacy(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) {
val reference = WeakReference(callback)
val proxy = ProxyBuilder.forClass(classOnStartTetheringCallback).apply {
dexCache(cacheDir)
handler { proxy, method, args ->
@Suppress("NAME_SHADOWING") val callback = reference.get()
if (args.isEmpty()) when (method.name) {
"onTetheringStarted" -> return@handler callback?.onTetheringStarted()
"onTetheringFailed" -> return@handler callback?.onTetheringFailed()
}
ProxyBuilder.callSuper(proxy, method, args)
}
}.build()
startTetheringLegacy(Services.connectivity, type, showProvisioningUi, proxy, handler)
}
@RequiresApi(30)
fun startTethering(type: Int, exemptFromEntitlementCheck: Boolean, showProvisioningUi: Boolean, executor: Executor,
proxy: Any) {
startTethering(instance, newTetheringRequestBuilder.newInstance(type).let { builder ->
// setting exemption requires TETHER_PRIVILEGED permission
if (exemptFromEntitlementCheck) setExemptFromEntitlementCheck(builder, true)
setShouldShowEntitlementUi(builder, showProvisioningUi)
build(builder)
}, executor, proxy)
}
@RequiresApi(30)
fun proxy(callback: StartTetheringCallback): Any {
val reference = WeakReference(callback)
return Proxy.newProxyInstance(interfaceStartTetheringCallback.classLoader,
arrayOf(interfaceStartTetheringCallback), object : InvocationHandler {
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
@Suppress("NAME_SHADOWING") val callback = reference.get()
return when {
method.matches("onTetheringStarted") -> callback?.onTetheringStarted()
method.matches("onTetheringFailed", Integer.TYPE) -> {
callback?.onTetheringFailed(args?.get(0) as Int)
}
else -> callSuper(interfaceStartTetheringCallback, proxy, method, args)
}
}
})
}
/**
* Runs tether provisioning for the given type if needed and then starts tethering if
* the check succeeds. If no carrier provisioning is required for tethering, tethering is
* enabled immediately. If provisioning fails, tethering will not be enabled. It also
* schedules tether provisioning re-checks if appropriate.
*
* CHANGED BEHAVIOR: This method will not throw Exceptions, instead, callback.onException will be called.
*
* @param type The type of tethering to start. Must be one of
* {@link ConnectivityManager.TETHERING_WIFI},
* {@link ConnectivityManager.TETHERING_USB}, or
* {@link ConnectivityManager.TETHERING_BLUETOOTH}.
* @param showProvisioningUi a boolean indicating to show the provisioning app UI if there
* is one. This should be true the first time this function is called and also any time
* the user can see this UI. It gives users information from their carrier about the
* check failing and how they can sign up for tethering if possible.
* @param callback an {@link OnStartTetheringCallback} which will be called to notify the caller
* of the result of trying to tether.
* @param handler {@link Handler} to specify the thread upon which the callback will be invoked
* *@param localIPv4Address for API 30+ (not implemented for now due to blacklist). If present, it
* configures tethering with the preferred local IPv4 link address to use.
* *@see setStaticIpv4Addresses
*/
fun startTethering(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) {
if (Build.VERSION.SDK_INT >= 30) try {
val executor = if (handler == null) InPlaceExecutor else ExecutorCompat.create(handler)
startTethering(type, true, showProvisioningUi,
executor, proxy(object : StartTetheringCallback {
override fun onTetheringStarted() = callback.onTetheringStarted()
override fun onTetheringFailed(error: Int?) {
if (error != TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION) callback.onTetheringFailed(error)
else GlobalScope.launch(Dispatchers.Unconfined) {
val result = try {
RootManager.use { it.execute(StartTethering(type, showProvisioningUi)) }
} catch (eRoot: Exception) {
try { // last resort: start tethering without trying to bypass entitlement check
startTethering(type, false, showProvisioningUi, executor, proxy(callback))
if (eRoot !is CancellationException) Timber.w(eRoot)
} catch (e: Exception) {
e.addSuppressed(eRoot)
callback.onException(e)
}
return@launch
}
when {
result == null -> callback.onTetheringStarted()
result.value == TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION -> try {
startTethering(type, false, showProvisioningUi, executor, proxy(callback))
} catch (e: Exception) {
callback.onException(e)
}
else -> callback.onTetheringFailed(result.value)
}
}
}
override fun onException(e: Exception) = callback.onException(e)
}))
} catch (e: Exception) {
callback.onException(e)
} else @Suppress("DEPRECATION") try {
startTetheringLegacy(type, showProvisioningUi, callback, handler, cacheDir)
} catch (e: InvocationTargetException) {
if (e.targetException is SecurityException) GlobalScope.launch(Dispatchers.Unconfined) {
val result = try {
val rootCache = File(cacheDir, "root")
rootCache.mkdirs()
check(rootCache.exists()) { "Creating root cache dir failed" }
RootManager.use {
it.execute(be.mygod.vpnhotspot.root.StartTetheringLegacy(
rootCache, type, showProvisioningUi))
}.value
} catch (eRoot: Exception) {
eRoot.addSuppressed(e)
if (eRoot !is CancellationException) Timber.w(eRoot)
callback.onException(eRoot)
return@launch
}
if (result) callback.onTetheringStarted() else callback.onTetheringFailed()
} else callback.onException(e)
} catch (e: Exception) {
callback.onException(e)
}
}
/**
* Stops tethering for the given type. Also cancels any provisioning rechecks for that type if
* applicable.
*
* @param type The type of tethering to stop. Must be one of
* {@link ConnectivityManager.TETHERING_WIFI},
* {@link ConnectivityManager.TETHERING_USB}, or
* {@link ConnectivityManager.TETHERING_BLUETOOTH}.
*/
fun stopTethering(type: Int) {
if (Build.VERSION.SDK_INT >= 30) stopTethering(instance, type)
else stopTetheringLegacy(Services.connectivity, type)
}
fun stopTethering(type: Int, callback: (Exception) -> Unit) {
try {
stopTethering(type)
} catch (e: InvocationTargetException) {
if (e.targetException is SecurityException) GlobalScope.launch(Dispatchers.Unconfined) {
try {
RootManager.use { it.execute(StopTethering(type)) }
} catch (eRoot: Exception) {
eRoot.addSuppressed(e)
callback(eRoot)
}
} else callback(e)
}
}
/**
* Callback for use with [registerTetheringEventCallback] to find out tethering
* upstream status.
*/
interface TetheringEventCallback {
/**
* Called when tethering supported status changed.
*
* This will be called immediately after the callback is registered, and may be called
* multiple times later upon changes.
*
* Tethering may be disabled via system properties, device configuration, or device
* policy restrictions.
*
* @param supported The new supported status
*/
fun onTetheringSupported(supported: Boolean) {}
/**
* Called when tethering supported status changed.
*
* This will be called immediately after the callback is registered, and may be called
* multiple times later upon changes.
*
* Tethering may be disabled via system properties, device configuration, or device
* policy restrictions.
*
* @param supportedTypes a set of @TetheringType which is supported.
*/
@TargetApi(31)
fun onSupportedTetheringTypes(supportedTypes: Set<Int?>) {
val filtered = supportedTypes.filter { it !in 0..5 }
if (filtered.isNotEmpty()) Timber.w(Exception(
"Unexpected supported tethering types: ${filtered.joinToString()}"))
}
/**
* Called when tethering upstream changed.
*
* This will be called immediately after the callback is registered, and may be called
* multiple times later upon changes.
*
* @param network the [Network] of tethering upstream. Null means tethering doesn't
* have any upstream.
*/
fun onUpstreamChanged(network: Network?) {}
/**
* Called when there was a change in tethering interface regular expressions.
*
* This will be called immediately after the callback is registered, and may be called
* multiple times later upon changes.
*
* CHANGED: This method will NOT be immediately called after registration.
*
* *@param reg The new regular expressions.
* @hide
*/
fun onTetherableInterfaceRegexpsChanged(reg: Any?) {}
/**
* Called when there was a change in the list of tetherable interfaces. Tetherable
* interface means this interface is available and can be used for tethering.
*
* This will be called immediately after the callback is registered, and may be called
* multiple times later upon changes.
* @param interfaces The list of tetherable interface names.
*/
fun onTetherableInterfacesChanged(interfaces: List<String?>) {}
/**
* Called when there was a change in the list of tethered interfaces.
*
* This will be called immediately after the callback is registered, and may be called
* multiple times later upon changes.
* @param interfaces The list of 0 or more String of currently tethered interface names.
*/
fun onTetheredInterfacesChanged(interfaces: List<String?>) {}
/**
* Called when an error occurred configuring tethering.
*
* This will be called immediately after the callback is registered if the latest status
* on the interface is an error, and may be called multiple times later upon changes.
* @param ifName Name of the interface.
* @param error One of `TetheringManager#TETHER_ERROR_*`.
*/
fun onError(ifName: String, error: Int) {}
/**
* Called when the list of tethered clients changes.
*
* This callback provides best-effort information on connected clients based on state
* known to the system, however the list cannot be completely accurate (and should not be
* used for security purposes). For example, clients behind a bridge and using static IP
* assignments are not visible to the tethering device; or even when using DHCP, such
* clients may still be reported by this callback after disconnection as the system cannot
* determine if they are still connected.
*
* Only called if having permission one of NETWORK_SETTINGS, MAINLINE_NETWORK_STACK, NETWORK_STACK.
* @param clients The new set of tethered clients; the collection is not ordered.
*/
fun onClientsChanged(clients: Collection<*>) {
if (clients.isNotEmpty()) Timber.i("onClientsChanged: ${clients.joinToString()}")
}
/**
* Called when tethering offload status changes.
*
* This will be called immediately after the callback is registered.
* @param status The offload status.
*/
fun onOffloadStatusChanged(status: Int) {}
}
@get:RequiresApi(30)
private val interfaceTetheringEventCallback by lazy {
Class.forName("android.net.TetheringManager\$TetheringEventCallback")
}
@get:RequiresApi(30)
private val registerTetheringEventCallback by lazy @TargetApi(30) {
clazz.getDeclaredMethod("registerTetheringEventCallback", Executor::class.java, interfaceTetheringEventCallback)
}
@get:RequiresApi(30)
private val unregisterTetheringEventCallback by lazy @TargetApi(30) {
clazz.getDeclaredMethod("unregisterTetheringEventCallback", interfaceTetheringEventCallback)
}
private val callbackMap = mutableMapOf<TetheringEventCallback, Any>()
/**
* Start listening to tethering change events. Any new added callback will receive the last
* tethering status right away. If callback is registered,
* [TetheringEventCallback.onUpstreamChanged] will immediately be called. If tethering
* has no upstream or disabled, the argument of callback will be null. The same callback object
* cannot be registered twice.
*
* Requires TETHER_PRIVILEGED or ACCESS_NETWORK_STATE.
*
* @param executor the executor on which callback will be invoked.
* @param callback the callback to be called when tethering has change events.
*/
@RequiresApi(30)
fun registerTetheringEventCallback(executor: Executor?, callback: TetheringEventCallback) {
val reference = WeakReference(callback)
val proxy = synchronized(callbackMap) {
var computed = false
callbackMap.computeIfAbsent(callback) {
computed = true
Proxy.newProxyInstance(interfaceTetheringEventCallback.classLoader,
arrayOf(interfaceTetheringEventCallback), object : InvocationHandler {
private var regexpsSent = false
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
@Suppress("NAME_SHADOWING")
val callback = reference.get()
return when {
method.matches("onTetheringSupported", Boolean::class.java) -> {
callback?.onTetheringSupported(args!![0] as Boolean)
}
method.matches1<java.util.Set<*>>("onSupportedTetheringTypes") -> {
@Suppress("UNCHECKED_CAST")
callback?.onSupportedTetheringTypes(args!![0] as Set<Int?>)
}
method.matches1<Network>("onUpstreamChanged") -> {
callback?.onUpstreamChanged(args!![0] as Network?)
}
method.name == "onTetherableInterfaceRegexpsChanged" &&
method.parameters.singleOrNull()?.type?.name ==
"android.net.TetheringManager\$TetheringInterfaceRegexps" -> {
if (regexpsSent) callback?.onTetherableInterfaceRegexpsChanged(args!!.single())
regexpsSent = true
}
method.matches1<java.util.List<*>>("onTetherableInterfacesChanged") -> {
@Suppress("UNCHECKED_CAST")
callback?.onTetherableInterfacesChanged(args!![0] as List<String?>)
}
method.matches1<java.util.List<*>>("onTetheredInterfacesChanged") -> {
@Suppress("UNCHECKED_CAST")
callback?.onTetheredInterfacesChanged(args!![0] as List<String?>)
}
method.matches("onError", String::class.java, Integer.TYPE) -> {
callback?.onError(args!![0] as String, args[1] as Int)
}
method.matches1<java.util.Collection<*>>("onClientsChanged") -> {
callback?.onClientsChanged(args!![0] as Collection<*>)
}
method.matches("onOffloadStatusChanged", Integer.TYPE) -> {
callback?.onOffloadStatusChanged(args!![0] as Int)
}
else -> callSuper(interfaceTetheringEventCallback, proxy, method, args)
}
}
})
}.also { if (!computed) return }
}
registerTetheringEventCallback(instance, executor ?: InPlaceExecutor, proxy)
}
/**
* Remove tethering event callback previously registered with
* [registerTetheringEventCallback].
*
* Requires TETHER_PRIVILEGED or ACCESS_NETWORK_STATE.
*
* @param callback previously registered callback.
*/
@RequiresApi(30)
fun unregisterTetheringEventCallback(callback: TetheringEventCallback) {
val proxy = synchronized(callbackMap) { callbackMap.remove(callback) } ?: return
try {
unregisterTetheringEventCallback(instance, proxy)
} catch (e: InvocationTargetException) {
if (!e.targetException.let { it is IllegalStateException && it.cause is DeadObjectException }) throw e
}
}
/**
* [registerTetheringEventCallback] in a backwards compatible way.
*
* Only [TetheringEventCallback.onTetheredInterfacesChanged] is supported on API 29-.
*/
fun registerTetheringEventCallbackCompat(context: Context, callback: TetheringEventCallback) {
if (Build.VERSION.SDK_INT < 30) synchronized(callbackMap) {
callbackMap.computeIfAbsent(callback) {
broadcastReceiver { _, intent ->
callback.onTetheredInterfacesChanged(intent.tetheredIfaces ?: return@broadcastReceiver)
}.also { context.registerReceiver(it, IntentFilter(ACTION_TETHER_STATE_CHANGED)) }
}
} else registerTetheringEventCallback(InPlaceExecutor, callback)
}
fun unregisterTetheringEventCallbackCompat(context: Context, callback: TetheringEventCallback) {
if (Build.VERSION.SDK_INT < 30) {
val receiver = synchronized(callbackMap) { callbackMap.remove(callback) } ?: return
context.ensureReceiverUnregistered(receiver as BroadcastReceiver)
} else unregisterTetheringEventCallback(callback)
}
/**
* Get a more detailed error code after a Tethering or Untethering
* request asynchronously failed.
*
* @param iface The name of the interface of interest
* @return error The error code of the last error tethering or untethering the named
* interface
*/
@Deprecated("Use {@link TetheringEventCallback#onError(String, int)} instead.")
fun getLastTetherError(iface: String): Int = getLastTetherError(Services.connectivity, iface) as Int
val tetherErrorLookup = ConstantLookup("TETHER_ERROR_",
"TETHER_ERROR_NO_ERROR", "TETHER_ERROR_UNKNOWN_IFACE", "TETHER_ERROR_SERVICE_UNAVAIL",
"TETHER_ERROR_UNSUPPORTED", "TETHER_ERROR_UNAVAIL_IFACE", "TETHER_ERROR_MASTER_ERROR",
"TETHER_ERROR_TETHER_IFACE_ERROR", "TETHER_ERROR_UNTETHER_IFACE_ERROR", "TETHER_ERROR_ENABLE_NAT_ERROR",
"TETHER_ERROR_DISABLE_NAT_ERROR", "TETHER_ERROR_IFACE_CFG_ERROR", "TETHER_ERROR_PROVISION_FAILED",
"TETHER_ERROR_DHCPSERVER_ERROR", "TETHER_ERROR_ENTITLEMENT_UNKNOWN") @TargetApi(30) { clazz }
@RequiresApi(30)
const val TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14
val Intent.tetheredIfaces get() = getStringArrayListExtra(EXTRA_ACTIVE_TETHER)
val Intent.localOnlyTetheredIfaces get() = getStringArrayListExtra(
if (Build.VERSION.SDK_INT >= 30) EXTRA_ACTIVE_LOCAL_ONLY else EXTRA_ACTIVE_LOCAL_ONLY_LEGACY)
}