diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/BluetoothTethering.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/BluetoothTethering.kt index c2dc3d5b..c8eea2ca 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/BluetoothTethering.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/BluetoothTethering.kt @@ -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) { diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt index 853953f4..215aa0a4 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt @@ -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) { data.text = errored.filter { TetherType.ofInterface(it) == tetherType } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringTileService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringTileService.kt index e8f8a68a..48bc1f9f 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringTileService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringTileService.kt @@ -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() } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt index b4e55325..7936c7ac 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt @@ -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().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,24 +182,47 @@ 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? = null) { val reference = WeakReference(callback) - val proxy = ProxyBuilder.forClass(classOnStartTetheringCallback) - .dexCache(app.deviceStorage.cacheDir) - .handler { proxy, method, args -> - if (args.isNotEmpty()) Timber.w("Unexpected args for ${method.name}: $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 (method.name) { + when (val name = method.name) { "onTetheringStarted" -> { + if (args.isNotEmpty()) Timber.w("Unexpected args for $name: $args") callback?.onTetheringStarted() null } "onTetheringFailed" -> { - callback?.onTetheringFailed() + if (args.size != 1) Timber.w("Unexpected args for $name: $args") + callback?.onTetheringFailed(args.getOrNull(0) as? Int?) null } else -> { @@ -157,8 +231,34 @@ object TetheringManager { } } } - .build() - startTethering.invoke(app.connectivity, type, showProvisioningUi, proxy, handler) + }.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) { + "onTetheringStarted" -> { + callback?.onTetheringStarted() + null + } + "onTetheringFailed" -> { + callback?.onTetheringFailed() + null + } + else -> { + Timber.w("Unexpected method, calling super: $method") + ProxyBuilder.callSuper(proxy, method, args) + } + } + } + }.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().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) {