From 753e09b2d6918a6e207e3dbb0f0f671f82050a3b Mon Sep 17 00:00:00 2001 From: Mygod Date: Thu, 28 May 2020 01:43:28 -0400 Subject: [PATCH] Implement (un)registerTetheringEventCallback --- .../mygod/vpnhotspot/net/TetheringManager.kt | 259 ++++++++++++++++++ 1 file changed, 259 insertions(+) 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 b4a651d2..5547edbe 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt @@ -4,6 +4,8 @@ import android.content.Intent import android.content.pm.PackageManager import android.net.ConnectivityManager import android.net.LinkAddress +import android.net.MacAddress +import android.net.Network import android.os.Build import android.os.Handler import androidx.annotation.RequiresApi @@ -79,6 +81,13 @@ object TetheringManager { */ const val EXTRA_ERRORED_TETHER = "erroredArray" + /** Tethering offload status is stopped. */ + const val TETHER_HARDWARE_OFFLOAD_STOPPED = 0 + /** Tethering offload status is started. */ + const val TETHER_HARDWARE_OFFLOAD_STARTED = 1 + /** Fail to start tethering offload. */ + 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. @@ -289,6 +298,256 @@ object TetheringManager { stopTetheringLegacy.invoke(app.connectivity, type) } + data class TetheredClient(val underlying: Any) { + @get:RequiresApi(30) + val macAddress get() = getMacAddress.invoke(underlying) as MacAddress + @get:RequiresApi(30) + val addresses get() = (getAddresses.invoke(underlying) as Iterable<*>).map { inner -> + getAddress.invoke(inner) as LinkAddress to getHostname.invoke(inner) as String? + } + @get:RequiresApi(30) + val tetheringType get() = getTetheringType.invoke(underlying) as Int + } + + /** + * 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 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. + * + * *@param reg The new regular expressions. + * @param tetherableBluetoothRegexs an array of 0 or more regular expression Strings defining + * what interfaces are considered tetherable bluetooth interfaces. + * @param tetherableUsbRegexs an array of 0 or more regular expression Strings defining + * what interfaces are considered tetherable usb interfaces. + * @param tetherableWifiRegexs an array of 0 or more regular expression Strings defining + * what interfaces are considered tetherable wifi interfaces. + * + * @hide + */ + fun onTetherableInterfaceRegexpsChanged(tetherableBluetoothRegexs: List, + tetherableUsbRegexs: List, + tetherableWifiRegexs: List) {} + + /** + * 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) {} + + /** + * 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) {} + + /** + * 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. + * @param clients The new set of tethered clients; the collection is not ordered. + */ + fun onClientsChanged(clients: List) {} + + /** + * 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 classTetheredClient by lazy { Class.forName("android.net.TetheredClient") } + @get:RequiresApi(30) + private val getMacAddress by lazy { classTetheredClient.getDeclaredMethod("getMacAddress") } + @get:RequiresApi(30) + private val getAddresses by lazy { classTetheredClient.getDeclaredMethod("getAddresses") } + @get:RequiresApi(30) + private val getTetheringType by lazy { classTetheredClient.getDeclaredMethod("getTetheringType") } + + @get:RequiresApi(30) + private val classTetheringInterfaceRegexps by lazy { + Class.forName("android.net.TetheredClient\$TetheringInterfaceRegexps") + } + @get:RequiresApi(30) + private val getTetherableBluetoothRegexs by lazy { + classTetheringInterfaceRegexps.getDeclaredMethod("getTetherableBluetoothRegexs") + } + @get:RequiresApi(30) + private val getTetherableUsbRegexs by lazy { + classTetheringInterfaceRegexps.getDeclaredMethod("getTetherableUsbRegexs") + } + @get:RequiresApi(30) + private val getTetherableWifiRegexs by lazy { + classTetheringInterfaceRegexps.getDeclaredMethod("getTetherableWifiRegexs") + } + + @get:RequiresApi(30) + private val classAddressInfo by lazy { Class.forName("android.net.TetheredClient\$AddressInfo") } + @get:RequiresApi(30) + private val getAddress by lazy { classAddressInfo.getDeclaredMethod("getAddress") } + @get:RequiresApi(30) + private val getHostname by lazy { classAddressInfo.getDeclaredMethod("getHostname") } + + @get:RequiresApi(30) + private val interfaceTetheringEventCallback by lazy { + Class.forName("android.net.TetheredClient\$TetheringEventCallback") + } + @get:RequiresApi(30) + private val registerTetheringEventCallback by lazy { + clazz.getDeclaredMethod("registerTetheringEventCallback", Executor::class.java, interfaceTetheringEventCallback) + } + @get:RequiresApi(30) + private val unregisterTetheringEventCallback by lazy { + clazz.getDeclaredMethod("unregisterTetheringEventCallback", interfaceTetheringEventCallback) + } + + private val callbackMap = mutableMapOf() + /** + * 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) { + callbackMap.computeIfAbsent(callback) { + Proxy.newProxyInstance(interfaceTetheringEventCallback.classLoader, + arrayOf(interfaceTetheringEventCallback)) { proxy, method, args -> + @Suppress("NAME_SHADOWING") val callback = reference.get() + when (val name = method.name) { + "onTetheringSupported" -> { + if (args.size > 1) Timber.w("Unexpected args for $name: $args") + callback?.onTetheringSupported(args[0] as Boolean) + null + } + "onUpstreamChanged" -> { + if (args.size > 1) Timber.w("Unexpected args for $name: $args") + callback?.onUpstreamChanged(args[0] as Network?) + null + } + "onTetherableInterfaceRegexpsChanged" -> { + if (args.size > 1) Timber.w("Unexpected args for $name: $args") + val reg = args[0] + @Suppress("UNCHECKED_CAST") + callback?.onTetherableInterfaceRegexpsChanged( + getTetherableBluetoothRegexs.invoke(reg) as List, + getTetherableUsbRegexs.invoke(reg) as List, + getTetherableWifiRegexs.invoke(reg) as List) + null + } + "onTetherableInterfacesChanged" -> { + if (args.size > 1) Timber.w("Unexpected args for $name: $args") + @Suppress("UNCHECKED_CAST") + callback?.onTetherableInterfacesChanged(args[0] as List) + null + } + "onTetheredInterfacesChanged" -> { + if (args.size > 1) Timber.w("Unexpected args for $name: $args") + @Suppress("UNCHECKED_CAST") + callback?.onTetheredInterfacesChanged(args[0] as List) + null + } + "onError" -> { + if (args.size > 2) Timber.w("Unexpected args for $name: $args") + callback?.onError(args[0] as String, args[1] as Int) + null + } + "onClientsChanged" -> { + if (args.size > 1) Timber.w("Unexpected args for $name: $args") + callback?.onClientsChanged((args[0] as Iterable<*>).map { TetheredClient(it!!) }) + null + } + "onOffloadStatusChanged" -> { + if (args.size > 1) Timber.w("Unexpected args for $name: $args") + callback?.onOffloadStatusChanged(args[0] as Int) + null + } + else -> { + Timber.w("Unexpected method, calling super: $method") + ProxyBuilder.callSuper(proxy, method, args) + } + } + } + } + } + registerTetheringEventCallback.invoke(instance, executor, 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 + unregisterTetheringEventCallback.invoke(instance, proxy) + } + /** * Get a more detailed error code after a Tethering or Untethering * request asynchronously failed.