Implement new startTethering API

This commit is contained in:
Mygod
2020-05-27 14:30:11 -04:00
parent 991a3466ca
commit b4d2b79ae9
4 changed files with 194 additions and 73 deletions

View File

@@ -35,7 +35,7 @@ class BluetoothTethering(context: Context, val stateListener: (Int) -> Unit) :
app.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
private val Intent.bluetoothState get() = getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
private var pendingCallback: TetheringManager.OnStartTetheringCallback? = null
private var pendingCallback: TetheringManager.StartTetheringCallback? = null
/**
* https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/TetherSettings.java#215
@@ -72,7 +72,7 @@ class BluetoothTethering(context: Context, val stateListener: (Int) -> Unit) :
* https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/TetherSettings.java#384
*/
@RequiresApi(24)
fun start(callback: TetheringManager.OnStartTetheringCallback) {
fun start(callback: TetheringManager.StartTetheringCallback) {
if (pendingCallback != null) return
val adapter = BluetoothAdapter.getDefaultAdapter()
if (adapter?.state == BluetoothAdapter.STATE_OFF) {

View File

@@ -7,7 +7,6 @@ import android.view.View
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.core.net.toUri
import androidx.core.os.BuildCompat
import androidx.core.view.updatePaddingRelative
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
@@ -20,12 +19,13 @@ import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.util.readableMessage
import be.mygod.vpnhotspot.widget.SmartSnackbar
import timber.log.Timber
import java.io.IOException
import java.lang.reflect.InvocationTargetException
sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
TetheringManager.OnStartTetheringCallback {
TetheringManager.StartTetheringCallback {
class ViewHolder(private val binding: ListitemInterfaceBinding) : RecyclerView.ViewHolder(binding.root),
View.OnClickListener {
init {
@@ -91,8 +91,9 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
protected abstract fun stop()
override fun onTetheringStarted() = data.notifyChange()
override fun onTetheringFailed() {
Timber.d(javaClass.simpleName, "onTetheringFailed")
override fun onTetheringFailed(error: Int?) {
Timber.d(javaClass.simpleName, "onTetheringFailed: $error")
error?.let { SmartSnackbar.make("$tetherType: ${TetheringManager.tetherErrorMessage(it)}") }
data.notifyChange()
}
@@ -101,34 +102,12 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
}
private fun getErrorMessage(iface: String): String {
val error = try {
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
}
if (BuildCompat.isAtLeastR()) try {
TetheringManager.tetherErrors.get(error)?.let { return it }
} catch (e: ReflectiveOperationException) {
Timber.w(e)
}
return when (error) {
TetheringManager.TETHER_ERROR_NO_ERROR -> "TETHER_ERROR_NO_ERROR"
TetheringManager.TETHER_ERROR_UNKNOWN_IFACE -> "TETHER_ERROR_UNKNOWN_IFACE"
TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL -> "TETHER_ERROR_SERVICE_UNAVAIL"
TetheringManager.TETHER_ERROR_UNSUPPORTED -> "TETHER_ERROR_UNSUPPORTED"
TetheringManager.TETHER_ERROR_UNAVAIL_IFACE -> "TETHER_ERROR_UNAVAIL_IFACE"
TetheringManager.TETHER_ERROR_MASTER_ERROR -> "TETHER_ERROR_MASTER_ERROR"
TetheringManager.TETHER_ERROR_TETHER_IFACE_ERROR -> "TETHER_ERROR_TETHER_IFACE_ERROR"
TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR -> "TETHER_ERROR_UNTETHER_IFACE_ERROR"
TetheringManager.TETHER_ERROR_ENABLE_NAT_ERROR -> "TETHER_ERROR_ENABLE_NAT_ERROR"
TetheringManager.TETHER_ERROR_DISABLE_NAT_ERROR -> "TETHER_ERROR_DISABLE_NAT_ERROR"
TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR -> "TETHER_ERROR_IFACE_CFG_ERROR"
TetheringManager.TETHER_ERROR_PROVISION_FAILED -> "TETHER_ERROR_PROVISION_FAILED"
TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR -> "TETHER_ERROR_DHCPSERVER_ERROR"
TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN -> "TETHER_ERROR_ENTITLEMENT_UNKNOWN"
else -> app.getString(R.string.failure_reason_unknown, error)
}
})
}
fun updateErrorMessage(errored: List<String>) {
data.text = errored.filter { TetherType.ofInterface(it) == tetherType }

View File

@@ -21,7 +21,7 @@ import java.io.IOException
import java.lang.reflect.InvocationTargetException
@RequiresApi(24)
sealed class TetheringTileService : TetherListeningTileService(), TetheringManager.OnStartTetheringCallback {
sealed class TetheringTileService : TetherListeningTileService(), TetheringManager.StartTetheringCallback {
protected val tileOff by lazy { Icon.createWithResource(application, icon) }
protected val tileOn by lazy { Icon.createWithResource(application, R.drawable.ic_quick_settings_tile_on) }
@@ -108,8 +108,9 @@ sealed class TetheringTileService : TetherListeningTileService(), TetheringManag
}
override fun onTetheringStarted() = updateTile()
override fun onTetheringFailed() {
Timber.d("onTetheringFailed")
override fun onTetheringFailed(error: Int?) {
Timber.d("onTetheringFailed: $error")
error?.let { Toast.makeText(this, TetheringManager.tetherErrorMessage(it), Toast.LENGTH_LONG).show() }
updateTile()
}

View File

@@ -1,16 +1,21 @@
package be.mygod.vpnhotspot.net
import android.content.Intent
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.LinkAddress
import android.os.Build
import android.os.Handler
import androidx.annotation.RequiresApi
import androidx.collection.SparseArrayCompat
import androidx.core.os.BuildCompat
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R
import com.android.dx.stock.ProxyBuilder
import timber.log.Timber
import java.lang.ref.WeakReference
import java.lang.reflect.InvocationTargetException
import java.util.concurrent.Executor
/**
* Heavily based on:
@@ -21,7 +26,7 @@ object TetheringManager {
/**
* Callback for use with [startTethering] to find out whether tethering succeeded.
*/
interface OnStartTetheringCallback {
interface StartTetheringCallback {
/**
* Called when tethering has been successfully started.
*/
@@ -29,12 +34,22 @@ object TetheringManager {
/**
* Called when starting tethering failed.
*
* @param error The error that caused the failure.
*/
fun onTetheringFailed() { }
fun onTetheringFailed(error: Int? = null) { }
fun onException() { }
}
/**
* Use with {@link #getSystemService(String)} to retrieve a {@link android.net.TetheringManager}
* for managing tethering functions.
* @hide
* @see android.net.TetheringManager
*/
const val TETHERING_SERVICE = "tethering"
/**
* This is a sticky broadcast since almost forever.
*
@@ -62,59 +77,95 @@ object TetheringManager {
* for any interfaces listed here.
*/
const val EXTRA_ERRORED_TETHER = "erroredArray"
const val TETHER_ERROR_NO_ERROR = 0
const val TETHER_ERROR_UNKNOWN_IFACE = 1
const val TETHER_ERROR_SERVICE_UNAVAIL = 2
const val TETHER_ERROR_UNSUPPORTED = 3
const val TETHER_ERROR_UNAVAIL_IFACE = 4
const val TETHER_ERROR_MASTER_ERROR = 5
const val TETHER_ERROR_TETHER_IFACE_ERROR = 6
const val TETHER_ERROR_UNTETHER_IFACE_ERROR = 7
const val TETHER_ERROR_ENABLE_NAT_ERROR = 8
const val TETHER_ERROR_DISABLE_NAT_ERROR = 9
const val TETHER_ERROR_IFACE_CFG_ERROR = 10
const val TETHER_ERROR_PROVISION_FAILED = 11
const val TETHER_ERROR_DHCPSERVER_ERROR = 12
const val TETHER_ERROR_ENTITLEMENT_UNKNOWN = 13
// tether errors defined in ConnectivityManager up to Android 10
private const val TETHER_ERROR_NO_ERROR = 0
private const val TETHER_ERROR_UNKNOWN_IFACE = 1
private const val TETHER_ERROR_SERVICE_UNAVAIL = 2
private const val TETHER_ERROR_UNSUPPORTED = 3
private const val TETHER_ERROR_UNAVAIL_IFACE = 4
private const val TETHER_ERROR_MASTER_ERROR = 5
private const val TETHER_ERROR_TETHER_IFACE_ERROR = 6
private const val TETHER_ERROR_UNTETHER_IFACE_ERROR = 7
private const val TETHER_ERROR_ENABLE_NAT_ERROR = 8
private const val TETHER_ERROR_DISABLE_NAT_ERROR = 9
private const val TETHER_ERROR_IFACE_CFG_ERROR = 10
private const val TETHER_ERROR_PROVISION_FAILED = 11
private const val TETHER_ERROR_DHCPSERVER_ERROR = 12
private const val TETHER_ERROR_ENTITLEMENT_UNKNOWN = 13
@RequiresApi(24)
const val TETHERING_WIFI = 0
/**
* Requires MANAGE_USB permission, unfortunately.
*
* Source: https://android.googlesource.com/platform/frameworks/base/+/7ca5d3a/services/usb/java/com/android/server/usb/UsbService.java#389
*/
@RequiresApi(24)
const val TETHERING_USB = 1
/**
* Requires BLUETOOTH permission, or BLUETOOTH_PRIVILEGED on API 30+.
*/
@RequiresApi(24)
const val TETHERING_BLUETOOTH = 2
@get:RequiresApi(30)
val tetherErrors by lazy {
SparseArrayCompat<String>().apply {
for (field in Class.forName("android.net.TetheringManager").declaredFields) try {
// all TETHER_ERROR_* are system-api since API 30
if (field.name.startsWith("TETHER_ERROR_")) put(field.get(null) as Int, field.name)
} catch (e: Exception) {
Timber.w(e)
}
}
}
private val clazz by lazy { Class.forName("android.net.TetheringManager") }
@get:RequiresApi(30)
private val instance by lazy { app.getSystemService(TETHERING_SERVICE) }
@get:RequiresApi(24)
private val classOnStartTetheringCallback by lazy {
Class.forName("android.net.ConnectivityManager\$OnStartTetheringCallback")
}
private val startTethering by lazy {
@get:RequiresApi(24)
private val startTetheringLegacy by lazy {
ConnectivityManager::class.java.getDeclaredMethod("startTethering",
Int::class.java, Boolean::class.java, classOnStartTetheringCallback, Handler::class.java)
}
private val stopTethering by lazy {
@get:RequiresApi(24)
private val stopTetheringLegacy by lazy {
ConnectivityManager::class.java.getDeclaredMethod("stopTethering", Int::class.java)
}
@get:RequiresApi(24)
private val getLastTetherError by lazy {
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 { 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 {
classTetheringRequestBuilder.getDeclaredMethod("setExemptFromEntitlementCheck", Boolean::class.java)
}
@get:RequiresApi(30)
private val setShouldShowEntitlementUi by lazy {
classTetheringRequestBuilder.getDeclaredMethod("setShouldShowEntitlementUi", Boolean::class.java)
}
@get:RequiresApi(30)
private val build by lazy { classTetheringRequestBuilder.getDeclaredMethod("build") }
@get:RequiresApi(30)
private val classStartTetheringCallback by lazy {
Class.forName("android.net.TetheringManager\$StartTetheringCallback")
}
@get:RequiresApi(30)
private val startTethering by lazy {
clazz.getDeclaredMethod("startTethering", Class.forName("android.net.TetheringManager\$TetheringRequest"),
Executor::class.java, classStartTetheringCallback)
}
@get:RequiresApi(30)
private val stopTethering by lazy { clazz.getDeclaredMethod("stopTethering", Int::class.java) }
/**
* 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
@@ -131,15 +182,64 @@ object TetheringManager {
* 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 handler {@link Handler} to specify the thread upon which the callback will be invoked
* @param address A pair (localIPv4Address, clientAddress) for API 30+. If present, it
* configures tethering with static IPv4 assignment.
*
* A DHCP server will be started, but will only be able to offer the client address.
* The two addresses must be in the same prefix.
*
* localIPv4Address: The preferred local IPv4 link address to use.
* clientAddress: The static client address.
* @see setStaticIpv4Addresses
*/
@RequiresApi(24)
fun startTethering(type: Int, showProvisioningUi: Boolean, callback: OnStartTetheringCallback,
handler: Handler? = null) {
fun startTethering(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
handler: Handler? = null, address: Pair<LinkAddress, LinkAddress>? = null) {
val reference = WeakReference(callback)
val proxy = ProxyBuilder.forClass(classOnStartTetheringCallback)
.dexCache(app.deviceStorage.cacheDir)
.handler { proxy, method, args ->
if (BuildCompat.isAtLeastR()) try {
val request = newTetheringRequestBuilder.newInstance(type).let { builder ->
// setting exemption requires TETHER_PRIVILEGED permission
if (app.checkSelfPermission("android.permission.TETHER_PRIVILEGED") ==
PackageManager.PERMISSION_GRANTED) setExemptFromEntitlementCheck.invoke(builder, true)
setShouldShowEntitlementUi.invoke(builder, showProvisioningUi)
if (address != null) {
val (localIPv4Address, clientAddress) = address
setStaticIpv4Addresses(builder, localIPv4Address, clientAddress)
}
build.invoke(this)
}
val executor = Executor { if (handler == null) it.run() else handler.post(it) }
val proxy = ProxyBuilder.forClass(classStartTetheringCallback).apply {
dexCache(app.deviceStorage.cacheDir)
handler { proxy, method, args ->
@Suppress("NAME_SHADOWING") val callback = reference.get()
when (val name = method.name) {
"onTetheringStarted" -> {
if (args.isNotEmpty()) Timber.w("Unexpected args for $name: $args")
callback?.onTetheringStarted()
null
}
"onTetheringFailed" -> {
if (args.size != 1) Timber.w("Unexpected args for $name: $args")
callback?.onTetheringFailed(args.getOrNull(0) as? Int?)
null
}
else -> {
Timber.w("Unexpected method, calling super: $method")
ProxyBuilder.callSuper(proxy, method, args)
}
}
}
}.build()
startTethering.invoke(instance, request, executor, proxy)
return
} catch (e: InvocationTargetException) {
Timber.w(e, "Unable to invoke TetheringManager.startTethering, falling back to ConnectivityManager")
}
val proxy = ProxyBuilder.forClass(classOnStartTetheringCallback).apply {
dexCache(app.deviceStorage.cacheDir)
handler { proxy, method, args ->
if (args.isNotEmpty()) Timber.w("Unexpected args for ${method.name}: $args")
@Suppress("NAME_SHADOWING") val callback = reference.get()
when (method.name) {
@@ -157,8 +257,8 @@ object TetheringManager {
}
}
}
.build()
startTethering.invoke(app.connectivity, type, showProvisioningUi, proxy, handler)
}.build()
startTetheringLegacy.invoke(app.connectivity, type, showProvisioningUi, proxy, handler)
}
/**
@@ -172,7 +272,12 @@ object TetheringManager {
*/
@RequiresApi(24)
fun stopTethering(type: Int) {
stopTethering.invoke(app.connectivity, type)
if (BuildCompat.isAtLeastR()) try {
stopTethering.invoke(instance, type)
} catch (e: InvocationTargetException) {
Timber.w(e, "Unable to invoke TetheringManager.stopTethering, falling back to ConnectivityManager")
}
stopTetheringLegacy.invoke(app.connectivity, type)
}
/**
@@ -185,6 +290,42 @@ object TetheringManager {
*/
fun getLastTetherError(iface: String): Int = getLastTetherError.invoke(app.connectivity, iface) as Int
@get:RequiresApi(30)
private val tetherErrors by lazy {
SparseArrayCompat<String>().apply {
for (field in clazz.declaredFields) try {
// all TETHER_ERROR_* are system-api since API 30
if (field.name.startsWith("TETHER_ERROR_")) put(field.get(null) as Int, field.name)
} catch (e: Exception) {
Timber.w(e)
}
}
}
fun tetherErrorMessage(error: Int): String {
if (BuildCompat.isAtLeastR()) try {
tetherErrors.get(error)?.let { return it }
} catch (e: ReflectiveOperationException) {
Timber.w(e)
}
return when (error) {
TETHER_ERROR_NO_ERROR -> "TETHER_ERROR_NO_ERROR"
TETHER_ERROR_UNKNOWN_IFACE -> "TETHER_ERROR_UNKNOWN_IFACE"
TETHER_ERROR_SERVICE_UNAVAIL -> "TETHER_ERROR_SERVICE_UNAVAIL"
TETHER_ERROR_UNSUPPORTED -> "TETHER_ERROR_UNSUPPORTED"
TETHER_ERROR_UNAVAIL_IFACE -> "TETHER_ERROR_UNAVAIL_IFACE"
TETHER_ERROR_MASTER_ERROR -> "TETHER_ERROR_MASTER_ERROR"
TETHER_ERROR_TETHER_IFACE_ERROR -> "TETHER_ERROR_TETHER_IFACE_ERROR"
TETHER_ERROR_UNTETHER_IFACE_ERROR -> "TETHER_ERROR_UNTETHER_IFACE_ERROR"
TETHER_ERROR_ENABLE_NAT_ERROR -> "TETHER_ERROR_ENABLE_NAT_ERROR"
TETHER_ERROR_DISABLE_NAT_ERROR -> "TETHER_ERROR_DISABLE_NAT_ERROR"
TETHER_ERROR_IFACE_CFG_ERROR -> "TETHER_ERROR_IFACE_CFG_ERROR"
TETHER_ERROR_PROVISION_FAILED -> "TETHER_ERROR_PROVISION_FAILED"
TETHER_ERROR_DHCPSERVER_ERROR -> "TETHER_ERROR_DHCPSERVER_ERROR"
TETHER_ERROR_ENTITLEMENT_UNKNOWN -> "TETHER_ERROR_ENTITLEMENT_UNKNOWN"
else -> app.getString(R.string.failure_reason_unknown, error)
}
}
val Intent.tetheredIfaces get() = getStringArrayListExtra(
if (Build.VERSION.SDK_INT >= 26) EXTRA_ACTIVE_TETHER else EXTRA_ACTIVE_TETHER_LEGACY)
val Intent.localOnlyTetheredIfaces get() = if (Build.VERSION.SDK_INT >= 26) {