Support toggling hotspots in app
This is a just-for-fun feature. It probably doesn't work.
This commit is contained in:
@@ -1,26 +0,0 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.annotation.RequiresApi
|
||||
|
||||
/**
|
||||
* Hidden constants from ConnectivityManager and some helpers.
|
||||
*/
|
||||
object ConnectivityManagerHelper {
|
||||
/**
|
||||
* 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_TETHER_LEGACY = "activeArray"
|
||||
@RequiresApi(26)
|
||||
private const val EXTRA_ACTIVE_LOCAL_ONLY = "localOnlyArray"
|
||||
@RequiresApi(26)
|
||||
private const val EXTRA_ACTIVE_TETHER = "tetherArray"
|
||||
|
||||
fun getTetheredIfaces(extras: Bundle) = if (Build.VERSION.SDK_INT >= 26)
|
||||
extras.getStringArrayList(EXTRA_ACTIVE_TETHER).toSet() + extras.getStringArrayList(EXTRA_ACTIVE_LOCAL_ONLY)
|
||||
else extras.getStringArrayList(EXTRA_ACTIVE_TETHER_LEGACY).toSet()
|
||||
}
|
||||
131
mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt
Normal file
131
mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt
Normal file
@@ -0,0 +1,131 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.util.Log
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import com.android.dx.stock.ProxyBuilder
|
||||
|
||||
/**
|
||||
* 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 OnStartTetheringCallback {
|
||||
/**
|
||||
* Called when tethering has been successfully started.
|
||||
*/
|
||||
fun onTetheringStarted() { }
|
||||
|
||||
/**
|
||||
* Called when starting tethering failed.
|
||||
*/
|
||||
fun onTetheringFailed() { }
|
||||
}
|
||||
|
||||
private const val TAG = "TetheringManager"
|
||||
|
||||
/**
|
||||
* 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_TETHER_LEGACY = "activeArray"
|
||||
@RequiresApi(26)
|
||||
private const val EXTRA_ACTIVE_LOCAL_ONLY = "localOnlyArray"
|
||||
@RequiresApi(26)
|
||||
private const val EXTRA_ACTIVE_TETHER = "tetherArray"
|
||||
|
||||
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
|
||||
*/
|
||||
const val TETHERING_USB = 1
|
||||
/**
|
||||
* Requires BLUETOOTH permission.
|
||||
*/
|
||||
const val TETHERING_BLUETOOTH = 2
|
||||
|
||||
private val classOnStartTetheringCallback by lazy @SuppressLint("PrivateApi") {
|
||||
Class.forName("android.net.ConnectivityManager\$OnStartTetheringCallback")
|
||||
}
|
||||
private val startTethering by lazy {
|
||||
ConnectivityManager::class.java.getDeclaredMethod("startTethering",
|
||||
Int::class.java, Boolean::class.java, classOnStartTetheringCallback, Handler::class.java)
|
||||
}
|
||||
private val stopTethering by lazy {
|
||||
ConnectivityManager::class.java.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
|
||||
* enabled immediately. If provisioning fails, tethering will not be enabled. It also
|
||||
* schedules tether provisioning re-checks if appropriate.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
@RequiresApi(24)
|
||||
fun start(type: Int, showProvisioningUi: Boolean, callback: OnStartTetheringCallback, handler: Handler? = null) {
|
||||
val proxy = ProxyBuilder.forClass(classOnStartTetheringCallback)
|
||||
.dexCache(app.cacheDir)
|
||||
.handler { proxy, method, args ->
|
||||
if (args.isNotEmpty()) Log.w(TAG, "Unexpected args for ${method.name}: $args")
|
||||
when (method.name) {
|
||||
"onTetheringStarted" -> {
|
||||
callback.onTetheringStarted()
|
||||
null
|
||||
}
|
||||
"onTetheringFailed" -> {
|
||||
callback.onTetheringFailed()
|
||||
null
|
||||
}
|
||||
else -> {
|
||||
Log.w(TAG, "Unexpected method, calling super: $method")
|
||||
ProxyBuilder.callSuper(proxy, method, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
.build()
|
||||
startTethering.invoke(app.connectivity, type, showProvisioningUi, proxy, handler)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
*/
|
||||
@RequiresApi(24)
|
||||
fun stop(type: Int) {
|
||||
stopTethering.invoke(app.connectivity, type)
|
||||
}
|
||||
|
||||
fun getTetheredIfaces(extras: Bundle) = if (Build.VERSION.SDK_INT >= 26)
|
||||
extras.getStringArrayList(EXTRA_ACTIVE_TETHER).toSet() + extras.getStringArrayList(EXTRA_ACTIVE_LOCAL_ONLY)
|
||||
else extras.getStringArrayList(EXTRA_ACTIVE_TETHER_LEGACY).toSet()
|
||||
}
|
||||
@@ -17,7 +17,6 @@ object VpnMonitor : ConnectivityManager.NetworkCallback() {
|
||||
|
||||
private const val TAG = "VpnMonitor"
|
||||
|
||||
private val manager = app.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
private val request = NetworkRequest.Builder()
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
|
||||
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||
@@ -31,7 +30,7 @@ object VpnMonitor : ConnectivityManager.NetworkCallback() {
|
||||
private val available = HashMap<Network, String>()
|
||||
private var currentNetwork: Network? = null
|
||||
override fun onAvailable(network: Network) {
|
||||
val properties = manager.getLinkProperties(network)
|
||||
val properties = app.connectivity.getLinkProperties(network)
|
||||
val ifname = properties?.interfaceName ?: return
|
||||
synchronized(this) {
|
||||
if (available.put(network, ifname) != null) return
|
||||
@@ -55,7 +54,7 @@ object VpnMonitor : ConnectivityManager.NetworkCallback() {
|
||||
while (available.isNotEmpty()) {
|
||||
val next = available.entries.first()
|
||||
currentNetwork = next.key
|
||||
val properties = manager.getLinkProperties(next.key)
|
||||
val properties = app.connectivity.getLinkProperties(next.key)
|
||||
if (properties != null) {
|
||||
debugLog(TAG, "Switching to ${next.value} as VPN interface")
|
||||
callbacks.forEach { it.onAvailable(next.value, properties.dnsServers) }
|
||||
@@ -70,16 +69,17 @@ object VpnMonitor : ConnectivityManager.NetworkCallback() {
|
||||
if (synchronized(this) {
|
||||
if (!callbacks.add(callback)) return
|
||||
if (!registered) {
|
||||
manager.registerNetworkCallback(request, this)
|
||||
app.connectivity.registerNetworkCallback(request, this)
|
||||
registered = true
|
||||
manager.allNetworks.all {
|
||||
val cap = manager.getNetworkCapabilities(it)
|
||||
app.connectivity.allNetworks.all {
|
||||
val cap = app.connectivity.getNetworkCapabilities(it)
|
||||
!cap.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
|
||||
cap.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||
}
|
||||
} else if (available.isEmpty()) true else {
|
||||
available.forEach {
|
||||
callback.onAvailable(it.value, manager.getLinkProperties(it.key)?.dnsServers ?: emptyList())
|
||||
callback.onAvailable(it.value,
|
||||
app.connectivity.getLinkProperties(it.key)?.dnsServers ?: emptyList())
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -87,7 +87,7 @@ object VpnMonitor : ConnectivityManager.NetworkCallback() {
|
||||
}
|
||||
fun unregisterCallback(callback: Callback) = synchronized(this) {
|
||||
if (!callbacks.remove(callback) || callbacks.isNotEmpty() || !registered) return
|
||||
manager.unregisterNetworkCallback(this)
|
||||
app.connectivity.unregisterNetworkCallback(this)
|
||||
registered = false
|
||||
available.clear()
|
||||
currentNetwork = null
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import android.content.Context
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.net.wifi.WifiManager
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
|
||||
@Deprecated("No longer usable since API 26.")
|
||||
object WifiApManager {
|
||||
private val wifi = app.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||
private val setWifiApEnabled = WifiManager::class.java.getDeclaredMethod("setWifiApEnabled",
|
||||
WifiConfiguration::class.java, Boolean::class.java)
|
||||
/**
|
||||
* Start AccessPoint mode with the specified
|
||||
* configuration. If the radio is already running in
|
||||
* AP mode, update the new configuration
|
||||
* Note that starting in access point mode disables station
|
||||
* mode operation
|
||||
* @param wifiConfig SSID, security and channel details as
|
||||
* part of WifiConfiguration
|
||||
* @return {@code true} if the operation succeeds, {@code false} otherwise
|
||||
*/
|
||||
private fun WifiManager.setWifiApEnabled(wifiConfig: WifiConfiguration?, enabled: Boolean) =
|
||||
setWifiApEnabled.invoke(this, wifiConfig, enabled) as Boolean
|
||||
|
||||
fun start(wifiConfig: WifiConfiguration? = null) {
|
||||
wifi.isWifiEnabled = false
|
||||
wifi.setWifiApEnabled(wifiConfig, true)
|
||||
}
|
||||
fun stop() {
|
||||
wifi.setWifiApEnabled(null, false)
|
||||
wifi.isWifiEnabled = true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user