Support toggling hotspots in app
This is a just-for-fun feature. It probably doesn't work.
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
|
||||
2
mobile/proguard-rules.pro
vendored
2
mobile/proguard-rules.pro
vendored
@@ -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.**
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
62
mobile/src/main/res/layout/listitem_manage_tether.xml
Normal file
62
mobile/src/main/res/layout/listitem_manage_tether.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user