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) ### [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? ### No root?
Without root, you can only: Without root, you can only:

View File

@@ -34,6 +34,7 @@ dependencies {
implementation "com.android.support:design:$supportLibraryVersion" implementation "com.android.support:design:$supportLibraryVersion"
implementation "com.android.support:preference-v14:$supportLibraryVersion" implementation "com.android.support:preference-v14:$supportLibraryVersion"
implementation 'com.android.support.constraint:constraint-layout:1.1.0' 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 "com.takisoft.fix:preference-v7:$takisoftFixVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlinVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlinVersion"
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import android.app.Application
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
import android.net.ConnectivityManager
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.preference.PreferenceManager import android.preference.PreferenceManager
@@ -37,6 +38,7 @@ class App : Application() {
lateinit var deviceContext: Context lateinit var deviceContext: Context
val handler = Handler() val handler = Handler()
val pref: SharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(deviceContext) } 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() } 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.databinding.ListitemClientBinding
import be.mygod.vpnhotspot.net.IpNeighbour import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.IpNeighbourMonitor 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 be.mygod.vpnhotspot.net.TetherType
import java.net.NetworkInterface import java.net.NetworkInterface
import java.net.SocketException import java.net.SocketException
@@ -160,7 +160,7 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
private var p2pInterface: String? = null private var p2pInterface: String? = null
private var tetheredInterfaces = emptySet<String>() private var tetheredInterfaces = emptySet<String>()
private val receiver = broadcastReceiver { _, intent -> private val receiver = broadcastReceiver { _, intent ->
tetheredInterfaces = ConnectivityManagerHelper.getTetheredIfaces(intent.extras).toSet() tetheredInterfaces = TetheringManager.getTetheredIfaces(intent.extras).toSet()
adapter.recreate() adapter.recreate()
} }
@@ -185,7 +185,7 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
val context = requireContext() val context = requireContext()
context.bindService(Intent(context, RepeaterService::class.java), this, Context.BIND_AUTO_CREATE) context.bindService(Intent(context, RepeaterService::class.java), this, Context.BIND_AUTO_CREATE)
IpNeighbourMonitor.registerCallback(this) IpNeighbourMonitor.registerCallback(this)
context.registerReceiver(receiver, intentFilter(ConnectivityManagerHelper.ACTION_TETHER_STATE_CHANGED)) context.registerReceiver(receiver, intentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
} }
override fun onStop() { override fun onStop() {

View File

@@ -1,10 +1,18 @@
package be.mygod.vpnhotspot package be.mygod.vpnhotspot
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothProfile
import android.content.* import android.content.*
import android.databinding.BaseObservable import android.databinding.BaseObservable
import android.databinding.Bindable
import android.databinding.DataBindingUtil import android.databinding.DataBindingUtil
import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.provider.Settings
import android.support.annotation.RequiresApi
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import android.support.v7.recyclerview.extensions.ListAdapter import android.support.v7.recyclerview.extensions.ListAdapter
@@ -15,10 +23,15 @@ import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup 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.FragmentTetheringBinding
import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding 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.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.NetworkInterface
import java.net.SocketException import java.net.SocketException
import java.util.* import java.util.*
@@ -27,6 +40,19 @@ class TetheringFragment : Fragment(), ServiceConnection {
companion object { companion object {
private const val VIEW_TYPE_INTERFACE = 0 private const val VIEW_TYPE_INTERFACE = 0
private const val VIEW_TYPE_MANAGE = 1 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() { inner class Data(val iface: TetheredInterface) : BaseObservable() {
@@ -62,6 +88,105 @@ class TetheringFragment : Fragment(), ServiceConnection {
.setClassName("com.android.settings", "com.android.settings.TetherSettings")) .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> { class TetheredInterface(val name: String, lookup: Map<String, NetworkInterface>) : Comparable<TetheredInterface> {
val addresses = lookup[name]?.formatAddresses() ?: "" val addresses = lookup[name]?.formatAddresses() ?: ""
@@ -91,17 +216,30 @@ class TetheringFragment : Fragment(), ServiceConnection {
e.printStackTrace() e.printStackTrace()
emptyMap<String, NetworkInterface>() emptyMap<String, NetworkInterface>()
} }
this@TetheringFragment.tetherListener.enabledTypes = data.map { TetherType.ofInterface(it) }.toSet()
submitList(data.map { TetheredInterface(it, lookup) }.sorted()) submitList(data.map { TetheredInterface(it, lookup) }.sorted())
} }
override fun getItemCount() = super.getItemCount() + 1 override fun getItemCount() = super.getItemCount() + when (Build.VERSION.SDK_INT) {
override fun getItemViewType(position: Int) = in 0 until 24 -> 2
if (position == super.getItemCount()) VIEW_TYPE_MANAGE else VIEW_TYPE_INTERFACE 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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
return when (viewType) { return when (viewType) {
VIEW_TYPE_INTERFACE -> InterfaceViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false)) VIEW_TYPE_INTERFACE -> InterfaceViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false))
VIEW_TYPE_MANAGE -> ManageViewHolder(inflater.inflate(R.layout.listitem_manage, 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") else -> throw IllegalArgumentException("Invalid view type")
} }
} }
@@ -112,11 +250,12 @@ class TetheringFragment : Fragment(), ServiceConnection {
} }
} }
private val tetherListener = TetherListener()
private lateinit var binding: FragmentTetheringBinding private lateinit var binding: FragmentTetheringBinding
private var binder: TetheringService.TetheringBinder? = null private var binder: TetheringService.TetheringBinder? = null
val adapter = TetheringAdapter() val adapter = TetheringAdapter()
private val receiver = broadcastReceiver { _, intent -> 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? { 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.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
binding.interfaces.itemAnimator = DefaultItemAnimator() binding.interfaces.itemAnimator = DefaultItemAnimator()
binding.interfaces.adapter = adapter binding.interfaces.adapter = adapter
BluetoothAdapter.getDefaultAdapter()?.getProfileProxy(requireContext(), tetherListener, PAN)
return binding.root return binding.root
} }
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
val context = requireContext() 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) context.bindService(Intent(context, TetheringService::class.java), this, Context.BIND_AUTO_CREATE)
} }
@@ -141,6 +281,11 @@ class TetheringFragment : Fragment(), ServiceConnection {
super.onStop() super.onStop()
} }
override fun onDestroy() {
tetherListener.pan = null
super.onDestroy()
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) { override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder = service as TetheringService.TetheringBinder val binder = service as TetheringService.TetheringBinder
this.binder = binder this.binder = binder

View File

@@ -31,8 +31,8 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
private val receiver = broadcastReceiver { _, intent -> private val receiver = broadcastReceiver { _, intent ->
synchronized(routings) { synchronized(routings) {
when (intent.action) { when (intent.action) {
ConnectivityManagerHelper.ACTION_TETHER_STATE_CHANGED -> { TetheringManager.ACTION_TETHER_STATE_CHANGED -> {
val remove = routings.keys - ConnectivityManagerHelper.getTetheredIfaces(intent.extras) val remove = routings.keys - TetheringManager.getTetheredIfaces(intent.extras)
if (remove.isEmpty()) return@broadcastReceiver if (remove.isEmpty()) return@broadcastReceiver
val failed = remove.any { routings.remove(it)?.stop() == false } val failed = remove.any { routings.remove(it)?.stop() == false }
if (failed) Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show() 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() if (failed) Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
} else if (!receiverRegistered) { } else if (!receiverRegistered) {
registerReceiver(receiver, intentFilter(ConnectivityManagerHelper.ACTION_TETHER_STATE_CHANGED)) registerReceiver(receiver, intentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
LocalBroadcastManager.getInstance(this) LocalBroadcastManager.getInstance(this)
.registerReceiver(receiver, intentFilter(App.ACTION_CLEAN_ROUTINGS)) .registerReceiver(receiver, intentFilter(App.ACTION_CLEAN_ROUTINGS))
IpNeighbourMonitor.registerCallback(this) 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 const val TAG = "VpnMonitor"
private val manager = app.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private val request = NetworkRequest.Builder() private val request = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_VPN) .addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
@@ -31,7 +30,7 @@ object VpnMonitor : ConnectivityManager.NetworkCallback() {
private val available = HashMap<Network, String>() private val available = HashMap<Network, String>()
private var currentNetwork: Network? = null private var currentNetwork: Network? = null
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
val properties = manager.getLinkProperties(network) val properties = app.connectivity.getLinkProperties(network)
val ifname = properties?.interfaceName ?: return val ifname = properties?.interfaceName ?: return
synchronized(this) { synchronized(this) {
if (available.put(network, ifname) != null) return if (available.put(network, ifname) != null) return
@@ -55,7 +54,7 @@ object VpnMonitor : ConnectivityManager.NetworkCallback() {
while (available.isNotEmpty()) { while (available.isNotEmpty()) {
val next = available.entries.first() val next = available.entries.first()
currentNetwork = next.key currentNetwork = next.key
val properties = manager.getLinkProperties(next.key) val properties = app.connectivity.getLinkProperties(next.key)
if (properties != null) { if (properties != null) {
debugLog(TAG, "Switching to ${next.value} as VPN interface") debugLog(TAG, "Switching to ${next.value} as VPN interface")
callbacks.forEach { it.onAvailable(next.value, properties.dnsServers) } callbacks.forEach { it.onAvailable(next.value, properties.dnsServers) }
@@ -70,16 +69,17 @@ object VpnMonitor : ConnectivityManager.NetworkCallback() {
if (synchronized(this) { if (synchronized(this) {
if (!callbacks.add(callback)) return if (!callbacks.add(callback)) return
if (!registered) { if (!registered) {
manager.registerNetworkCallback(request, this) app.connectivity.registerNetworkCallback(request, this)
registered = true registered = true
manager.allNetworks.all { app.connectivity.allNetworks.all {
val cap = manager.getNetworkCapabilities(it) val cap = app.connectivity.getNetworkCapabilities(it)
!cap.hasTransport(NetworkCapabilities.TRANSPORT_VPN) || !cap.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
cap.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) cap.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
} }
} else if (available.isEmpty()) true else { } else if (available.isEmpty()) true else {
available.forEach { available.forEach {
callback.onAvailable(it.value, manager.getLinkProperties(it.key)?.dnsServers ?: emptyList()) callback.onAvailable(it.value,
app.connectivity.getLinkProperties(it.key)?.dnsServers ?: emptyList())
} }
false false
} }
@@ -87,7 +87,7 @@ object VpnMonitor : ConnectivityManager.NetworkCallback() {
} }
fun unregisterCallback(callback: Callback) = synchronized(this) { fun unregisterCallback(callback: Callback) = synchronized(this) {
if (!callbacks.remove(callback) || callbacks.isNotEmpty() || !registered) return if (!callbacks.remove(callback) || callbacks.isNotEmpty() || !registered) return
manager.unregisterNetworkCallback(this) app.connectivity.unregisterNetworkCallback(this)
registered = false registered = false
available.clear() available.clear()
currentNetwork = null 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_reset_credentials_failure">重置凭据失败(原因:%s</string>
<string name="repeater_inactive">未打开</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_create_group_failure">创建 P2P 群组失败(原因:%s</string>
<string name="repeater_remove_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_remove_old_group_failure">关闭 P2P 群组失败(原因:%s</string>
<string name="repeater_failure_reason_error">内部异常</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_busy">系统忙</string>
<string name="repeater_failure_reason_no_service_requests">未添加服务请求</string> <string name="repeater_failure_reason_no_service_requests">未添加服务请求</string>
<string name="repeater_failure_reason_unknown">未知 #%d</string> <string name="repeater_failure_reason_unknown">未知 #%d</string>
<string name="tethering_manage">管理…</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_devices">已连接设备</string>
<string name="connected_state_incomplete">%s (正在连接)</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_reset_credentials_failure">Failed to reset credentials (reason: %s)</string>
<string name="repeater_inactive">Service inactive</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_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_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_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_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_busy">framework is busy</string>
<string name="repeater_failure_reason_no_service_requests">no service requests added</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="repeater_failure_reason_unknown">unknown #%d</string>
<string name="tethering_manage">Manage…</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_devices">Connected devices</string>
<string name="connected_state_incomplete">%s (connecting)</string> <string name="connected_state_incomplete">%s (connecting)</string>