Support toggling hotspots in app

This is a just-for-fun feature. It probably doesn't work.
This commit is contained in:
Mygod
2018-04-21 13:02:52 -07:00
parent 0355dae65e
commit 958b1ec350
15 changed files with 440 additions and 50 deletions

View File

@@ -45,6 +45,11 @@ You'll have to use WPS for now to make the repeater switch to 2.4GHz.
### [IPv6 tethering?](https://github.com/Mygod/VPNHotspot/issues/6)
### Missing `android.permission.MANAGE_USB` permission?
Toggling USB tethering only works if you install this app as a system app (`/system/priv-app`).
Alternatively, use the toggle in your system settings instead.
### No root?
Without root, you can only:

View File

@@ -34,6 +34,7 @@ dependencies {
implementation "com.android.support:design:$supportLibraryVersion"
implementation "com.android.support:preference-v14:$supportLibraryVersion"
implementation 'com.android.support.constraint:constraint-layout:1.1.0'
implementation 'com.linkedin.dexmaker:dexmaker-mockito:2.16.0'
implementation "com.takisoft.fix:preference-v7:$takisoftFixVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlinVersion"
testImplementation 'junit:junit:4.12'

View File

@@ -20,3 +20,5 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontwarn org.mockito.**
-dontwarn org.objenesis.instantiator.**

View File

@@ -1,7 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="be.mygod.vpnhotspot">
<uses-feature
android:name="android.bluetooth"
android:required="false"/>
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false"/>
@@ -14,8 +18,14 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MANAGE_USB"
tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.TETHER_PRIVILEGE"/>
<uses-permission android:name="android.permission.WRITE_SETTINGS"
tools:ignore="ProtectedPermissions"/>
<application
android:name=".App"

View File

@@ -5,6 +5,7 @@ import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration
import android.net.ConnectivityManager
import android.os.Build
import android.os.Handler
import android.preference.PreferenceManager
@@ -37,6 +38,7 @@ class App : Application() {
lateinit var deviceContext: Context
val handler = Handler()
val pref: SharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(deviceContext) }
val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
fun toast(@StringRes resId: Int) = handler.post { Toast.makeText(this, resId, Toast.LENGTH_SHORT).show() }
}

View File

@@ -28,7 +28,7 @@ import be.mygod.vpnhotspot.databinding.FragmentRepeaterBinding
import be.mygod.vpnhotspot.databinding.ListitemClientBinding
import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.ConnectivityManagerHelper
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.TetherType
import java.net.NetworkInterface
import java.net.SocketException
@@ -160,7 +160,7 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
private var p2pInterface: String? = null
private var tetheredInterfaces = emptySet<String>()
private val receiver = broadcastReceiver { _, intent ->
tetheredInterfaces = ConnectivityManagerHelper.getTetheredIfaces(intent.extras).toSet()
tetheredInterfaces = TetheringManager.getTetheredIfaces(intent.extras).toSet()
adapter.recreate()
}
@@ -185,7 +185,7 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
val context = requireContext()
context.bindService(Intent(context, RepeaterService::class.java), this, Context.BIND_AUTO_CREATE)
IpNeighbourMonitor.registerCallback(this)
context.registerReceiver(receiver, intentFilter(ConnectivityManagerHelper.ACTION_TETHER_STATE_CHANGED))
context.registerReceiver(receiver, intentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
}
override fun onStop() {

View File

@@ -1,10 +1,18 @@
package be.mygod.vpnhotspot
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothProfile
import android.content.*
import android.databinding.BaseObservable
import android.databinding.Bindable
import android.databinding.DataBindingUtil
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.provider.Settings
import android.support.annotation.RequiresApi
import android.support.v4.app.Fragment
import android.support.v4.content.ContextCompat
import android.support.v7.recyclerview.extensions.ListAdapter
@@ -15,10 +23,15 @@ import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.databinding.FragmentTetheringBinding
import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
import be.mygod.vpnhotspot.net.ConnectivityManagerHelper
import be.mygod.vpnhotspot.databinding.ListitemManageTetherBinding
import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.WifiApManager
import java.lang.reflect.InvocationTargetException
import java.net.NetworkInterface
import java.net.SocketException
import java.util.*
@@ -27,6 +40,19 @@ class TetheringFragment : Fragment(), ServiceConnection {
companion object {
private const val VIEW_TYPE_INTERFACE = 0
private const val VIEW_TYPE_MANAGE = 1
private const val VIEW_TYPE_WIFI = 2
private const val VIEW_TYPE_USB = 3
private const val VIEW_TYPE_BLUETOOTH = 4
private const val VIEW_TYPE_WIFI_LEGACY = 5
/**
* PAN Profile
* From BluetoothProfile.java.
*/
private const val PAN = 5
private val isTetheringOn by lazy @SuppressLint("PrivateApi") {
Class.forName("android.bluetooth.BluetoothPan").getDeclaredMethod("isTetheringOn")
}
}
inner class Data(val iface: TetheredInterface) : BaseObservable() {
@@ -62,6 +88,105 @@ class TetheringFragment : Fragment(), ServiceConnection {
.setClassName("com.android.settings", "com.android.settings.TetherSettings"))
}
}
private inner class ManageItemHolder(binding: ListitemManageTetherBinding, private val type: Int)
: RecyclerView.ViewHolder(binding.root), View.OnClickListener, TetheringManager.OnStartTetheringCallback {
val tetherType = when (type) {
VIEW_TYPE_WIFI, VIEW_TYPE_WIFI_LEGACY -> TetherType.WIFI
VIEW_TYPE_USB -> TetherType.USB
VIEW_TYPE_BLUETOOTH -> TetherType.BLUETOOTH
else -> TetherType.NONE
}
init {
itemView.setOnClickListener(this)
binding.icon = tetherType.icon
binding.title = getString(when (type) {
VIEW_TYPE_USB -> R.string.tethering_manage_usb
VIEW_TYPE_WIFI -> R.string.tethering_manage_wifi
VIEW_TYPE_WIFI_LEGACY -> R.string.tethering_manage_wifi_legacy
VIEW_TYPE_BLUETOOTH -> R.string.tethering_manage_bluetooth
else -> throw IllegalStateException()
})
binding.tetherListener = tetherListener
binding.type = tetherType
}
override fun onClick(v: View?) {
val context = requireContext()
if (Build.VERSION.SDK_INT >= 23 && !Settings.System.canWrite(context)) {
startActivity(Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS,
Uri.parse("package:${context.packageName}")))
return
}
val started = tetherListener.isStarted(tetherType)
try {
when (type) {
VIEW_TYPE_WIFI -> @RequiresApi(24) {
if (started) TetheringManager.stop(TetheringManager.TETHERING_WIFI)
else TetheringManager.start(TetheringManager.TETHERING_WIFI, true, this)
}
VIEW_TYPE_USB -> @RequiresApi(24) {
if (started) TetheringManager.stop(TetheringManager.TETHERING_USB)
else TetheringManager.start(TetheringManager.TETHERING_USB, true, this)
}
VIEW_TYPE_BLUETOOTH -> @RequiresApi(24) {
if (started) {
TetheringManager.stop(TetheringManager.TETHERING_BLUETOOTH)
Thread.sleep(1) // give others a room to breathe
onTetheringStarted() // force flush state
} else TetheringManager.start(TetheringManager.TETHERING_BLUETOOTH, true, this)
}
VIEW_TYPE_WIFI_LEGACY -> @Suppress("DEPRECATION") {
if (started) WifiApManager.stop() else WifiApManager.start()
}
}
} catch (e: InvocationTargetException) {
e.printStackTrace()
var cause: Throwable? = e
while (cause != null) {
cause = cause.cause
if (cause != null && cause !is InvocationTargetException) {
Toast.makeText(context, cause.message, Toast.LENGTH_LONG).show()
break
}
}
}
}
override fun onTetheringStarted() = tetherListener.notifyPropertyChanged(BR.enabledTypes)
override fun onTetheringFailed() {
app.handler.post {
Toast.makeText(requireContext(), "Android system has failed to start tethering.", Toast.LENGTH_SHORT).show()
}
}
}
class TetherListener : BaseObservable(), BluetoothProfile.ServiceListener {
var enabledTypes = emptySet<TetherType>()
@Bindable get
set(value) {
field = value
notifyPropertyChanged(BR.enabledTypes)
}
var pan: BluetoothProfile? = null
override fun onServiceDisconnected(profile: Int) {
pan = null
}
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
pan = proxy
}
/**
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/78d5efd/src/com/android/settings/TetherSettings.java
*/
fun isStarted(type: TetherType, enabledTypes: Set<TetherType> = this.enabledTypes): Boolean {
return if (type == TetherType.BLUETOOTH) {
val pan = pan
BluetoothAdapter.getDefaultAdapter()?.state == BluetoothAdapter.STATE_ON && pan != null &&
isTetheringOn.invoke(pan) as Boolean
} else enabledTypes.contains(type)
}
}
class TetheredInterface(val name: String, lookup: Map<String, NetworkInterface>) : Comparable<TetheredInterface> {
val addresses = lookup[name]?.formatAddresses() ?: ""
@@ -91,17 +216,30 @@ class TetheringFragment : Fragment(), ServiceConnection {
e.printStackTrace()
emptyMap<String, NetworkInterface>()
}
this@TetheringFragment.tetherListener.enabledTypes = data.map { TetherType.ofInterface(it) }.toSet()
submitList(data.map { TetheredInterface(it, lookup) }.sorted())
}
override fun getItemCount() = super.getItemCount() + 1
override fun getItemViewType(position: Int) =
if (position == super.getItemCount()) VIEW_TYPE_MANAGE else VIEW_TYPE_INTERFACE
override fun getItemCount() = super.getItemCount() + when (Build.VERSION.SDK_INT) {
in 0 until 24 -> 2
in 24..25 -> 5
else -> 4
}
override fun getItemViewType(position: Int) = when (position - super.getItemCount()) {
0 -> VIEW_TYPE_MANAGE
1 -> if (Build.VERSION.SDK_INT >= 24) VIEW_TYPE_USB else VIEW_TYPE_WIFI_LEGACY
2 -> VIEW_TYPE_WIFI
3 -> VIEW_TYPE_BLUETOOTH
4 -> VIEW_TYPE_WIFI_LEGACY
else -> VIEW_TYPE_INTERFACE
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
VIEW_TYPE_INTERFACE -> InterfaceViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false))
VIEW_TYPE_MANAGE -> ManageViewHolder(inflater.inflate(R.layout.listitem_manage, parent, false))
VIEW_TYPE_WIFI, VIEW_TYPE_USB, VIEW_TYPE_BLUETOOTH, VIEW_TYPE_WIFI_LEGACY ->
ManageItemHolder(ListitemManageTetherBinding.inflate(inflater, parent, false), viewType)
else -> throw IllegalArgumentException("Invalid view type")
}
}
@@ -112,11 +250,12 @@ class TetheringFragment : Fragment(), ServiceConnection {
}
}
private val tetherListener = TetherListener()
private lateinit var binding: FragmentTetheringBinding
private var binder: TetheringService.TetheringBinder? = null
val adapter = TetheringAdapter()
private val receiver = broadcastReceiver { _, intent ->
adapter.update(ConnectivityManagerHelper.getTetheredIfaces(intent.extras))
adapter.update(TetheringManager.getTetheredIfaces(intent.extras))
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@@ -124,13 +263,14 @@ class TetheringFragment : Fragment(), ServiceConnection {
binding.interfaces.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
binding.interfaces.itemAnimator = DefaultItemAnimator()
binding.interfaces.adapter = adapter
BluetoothAdapter.getDefaultAdapter()?.getProfileProxy(requireContext(), tetherListener, PAN)
return binding.root
}
override fun onStart() {
super.onStart()
val context = requireContext()
context.registerReceiver(receiver, intentFilter(ConnectivityManagerHelper.ACTION_TETHER_STATE_CHANGED))
context.registerReceiver(receiver, intentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
context.bindService(Intent(context, TetheringService::class.java), this, Context.BIND_AUTO_CREATE)
}
@@ -141,6 +281,11 @@ class TetheringFragment : Fragment(), ServiceConnection {
super.onStop()
}
override fun onDestroy() {
tetherListener.pan = null
super.onDestroy()
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder = service as TetheringService.TetheringBinder
this.binder = binder

View File

@@ -31,8 +31,8 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
private val receiver = broadcastReceiver { _, intent ->
synchronized(routings) {
when (intent.action) {
ConnectivityManagerHelper.ACTION_TETHER_STATE_CHANGED -> {
val remove = routings.keys - ConnectivityManagerHelper.getTetheredIfaces(intent.extras)
TetheringManager.ACTION_TETHER_STATE_CHANGED -> {
val remove = routings.keys - TetheringManager.getTetheredIfaces(intent.extras)
if (remove.isEmpty()) return@broadcastReceiver
val failed = remove.any { routings.remove(it)?.stop() == false }
if (failed) Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
@@ -67,7 +67,7 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
}
if (failed) Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
} else if (!receiverRegistered) {
registerReceiver(receiver, intentFilter(ConnectivityManagerHelper.ACTION_TETHER_STATE_CHANGED))
registerReceiver(receiver, intentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
LocalBroadcastManager.getInstance(this)
.registerReceiver(receiver, intentFilter(App.ACTION_CLEAN_ROUTINGS))
IpNeighbourMonitor.registerCallback(this)

View File

@@ -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()
}

View 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()
}

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="be.mygod.vpnhotspot.TetheringFragment"/>
<variable
name="icon"
type="Integer"/>
<variable
name="title"
type="String"/>
<variable
name="tetherListener"
type="TetheringFragment.TetherListener"/>
<variable
name="type"
type="be.mygod.vpnhotspot.net.TetherType"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:focusable="true"
android:padding="16dp"
android:paddingStart="56dp"
tools:ignore="RtlSymmetry">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:src="@{safeUnbox(icon)}"
android:tint="?android:attr/textColorPrimary"
tools:src="@drawable/ic_device_network_wifi"/>
<Space
android:layout_width="16dp"
android:layout_height="0dp"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:text="@{title}"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
tools:text="@string/tethering_manage_wifi"/>
<Switch
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:checked="@{tetherListener.isStarted(type, tetherListener.enabledTypes)}"
android:clickable="false"
android:ellipsize="end"
android:focusable="false"
android:focusableInTouchMode="false"
android:gravity="center_vertical"/>
</LinearLayout>
</layout>

View File

@@ -21,18 +21,30 @@
<string name="repeater_reset_credentials_failure">重置凭据失败(原因:%s</string>
<string name="repeater_inactive">未打开</string>
<string name="repeater_p2p_unavailable">Wi-Fi 直连不可用</string>
<string name="repeater_p2p_unavailable">Wi\u2011Fi 直连不可用</string>
<string name="repeater_create_group_failure">创建 P2P 群组失败(原因:%s</string>
<string name="repeater_remove_group_failure">关闭已有 P2P 群组失败(原因:%s</string>
<string name="repeater_remove_old_group_failure">关闭 P2P 群组失败(原因:%s</string>
<string name="repeater_failure_reason_error">内部异常</string>
<string name="repeater_failure_reason_p2p_unsupported">设备不支持 Wi-Fi 直连</string>
<string name="repeater_failure_reason_p2p_unsupported">设备不支持 Wi\u2011Fi 直连</string>
<string name="repeater_failure_reason_busy">系统忙</string>
<string name="repeater_failure_reason_no_service_requests">未添加服务请求</string>
<string name="repeater_failure_reason_unknown">未知 #%d</string>
<string name="tethering_manage">管理…</string>
<!--
Values copied from:
* https://android.googlesource.com/platform/packages/apps/Settings/+/7686ef8/res/xml/tether_prefs.xml
* https://android.googlesource.com/platform/packages/apps/Settings/+/b63de87/res/values-zh-rCN/strings.xml
* @string/usb_tethering_button_text
* @string/wifi_hotspot_checkbox_text
* @string/bluetooth_tether_checkbox_text
-->
<string name="tethering_manage_usb">USB 网络共享</string>
<string name="tethering_manage_wifi">WLAN 热点</string>
<string name="tethering_manage_wifi_legacy">WLAN 热点 (旧 API)</string>
<string name="tethering_manage_bluetooth">蓝牙网络共享</string>
<string name="connected_devices">已连接设备</string>
<string name="connected_state_incomplete">%s (正在连接)</string>

View File

@@ -23,18 +23,30 @@
<string name="repeater_reset_credentials_failure">Failed to reset credentials (reason: %s)</string>
<string name="repeater_inactive">Service inactive</string>
<string name="repeater_p2p_unavailable">Wi-Fi direct unavailable</string>
<string name="repeater_p2p_unavailable">Wi\u2011Fi direct unavailable</string>
<string name="repeater_create_group_failure">Failed to create P2P group (reason: %s)</string>
<string name="repeater_remove_group_failure">Failed to remove P2P group (reason: %s)</string>
<string name="repeater_remove_old_group_failure">Failed to remove old P2P group (reason: %s)</string>
<string name="repeater_failure_reason_error">internal error</string>
<string name="repeater_failure_reason_p2p_unsupported">Wi-Fi direct unsupported</string>
<string name="repeater_failure_reason_p2p_unsupported">Wi\u2011Fi direct unsupported</string>
<string name="repeater_failure_reason_busy">framework is busy</string>
<string name="repeater_failure_reason_no_service_requests">no service requests added</string>
<string name="repeater_failure_reason_unknown">unknown #%d</string>
<string name="tethering_manage">Manage…</string>
<!--
Values copied from:
* https://android.googlesource.com/platform/packages/apps/Settings/+/7686ef8/res/xml/tether_prefs.xml
* https://android.googlesource.com/platform/packages/apps/Settings/+/e5ed810/res/values/strings.xml
* @string/usb_tethering_button_text
* @string/wifi_hotspot_checkbox_text
* @string/bluetooth_tether_checkbox_text
-->
<string name="tethering_manage_usb">USB tethering</string>
<string name="tethering_manage_wifi">Wi\u2011Fi hotspot</string>
<string name="tethering_manage_wifi_legacy">Wi\u2011Fi hotspot (legacy)</string>
<string name="tethering_manage_bluetooth">Bluetooth tethering</string>
<string name="connected_devices">Connected devices</string>
<string name="connected_state_incomplete">%s (connecting)</string>