diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml
index 498d070e..176c47c2 100644
--- a/mobile/src/main/AndroidManifest.xml
+++ b/mobile/src/main/AndroidManifest.xml
@@ -30,6 +30,7 @@
+
+
+
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/App.kt b/mobile/src/main/java/be/mygod/vpnhotspot/App.kt
index 12d387e5..34fde065 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/App.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/App.kt
@@ -6,6 +6,7 @@ import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration
import android.net.ConnectivityManager
+import android.net.wifi.WifiManager
import android.os.Build
import android.os.Handler
import android.preference.PreferenceManager
@@ -43,6 +44,7 @@ class App : Application() {
val handler = Handler()
val pref: SharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(deviceContext) }
val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
+ val wifi by lazy { app.getSystemService(Context.WIFI_SERVICE) as WifiManager }
val operatingChannel: Int get() {
val result = pref.getString(KEY_OPERATING_CHANNEL, null)?.toIntOrNull() ?: 0
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/IpNeighbourMonitoringService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/IpNeighbourMonitoringService.kt
new file mode 100644
index 00000000..9d227c06
--- /dev/null
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/IpNeighbourMonitoringService.kt
@@ -0,0 +1,24 @@
+package be.mygod.vpnhotspot
+
+import android.app.Service
+import be.mygod.vpnhotspot.net.IpNeighbour
+import be.mygod.vpnhotspot.net.IpNeighbourMonitor
+
+abstract class IpNeighbourMonitoringService : Service(), IpNeighbourMonitor.Callback {
+ private var neighbours = emptyList()
+
+ protected abstract val activeIfaces: List
+
+ override fun onIpNeighbourAvailable(neighbours: Map) {
+ this.neighbours = neighbours.values.toList()
+ }
+ override fun postIpNeighbourAvailable() {
+ val sizeLookup = neighbours.groupBy { it.dev }.mapValues { (_, neighbours) ->
+ neighbours
+ .filter { it.state != IpNeighbour.State.FAILED }
+ .distinctBy { it.lladdr }
+ .size
+ }
+ ServiceNotification.startForeground(this, activeIfaces.associate { Pair(it, sizeLookup[it] ?: 0) })
+ }
+}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt
new file mode 100644
index 00000000..4713c07d
--- /dev/null
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt
@@ -0,0 +1,109 @@
+package be.mygod.vpnhotspot
+
+import android.content.Intent
+import android.net.wifi.WifiManager
+import android.os.Binder
+import android.support.annotation.RequiresApi
+import android.widget.Toast
+import be.mygod.vpnhotspot.App.Companion.app
+import be.mygod.vpnhotspot.net.IpNeighbourMonitor
+import be.mygod.vpnhotspot.net.TetheringManager
+
+@RequiresApi(26)
+class LocalOnlyHotspotService : IpNeighbourMonitoringService() {
+ companion object {
+ private const val TAG = "LocalOnlyHotspotService"
+ }
+
+ inner class HotspotBinder : Binder() {
+ var fragment: TetheringFragment? = null
+ var iface: String? = null
+ val configuration get() = reservation?.wifiConfiguration
+
+ fun stop() = reservation?.close()
+ }
+
+ private val binder = HotspotBinder()
+ private var reservation: WifiManager.LocalOnlyHotspotReservation? = null
+ private var routingManager: LocalOnlyInterfaceManager? = null
+ private var receiverRegistered = false
+ private val receiver = broadcastReceiver { _, intent ->
+ val ifaces = TetheringManager.getLocalOnlyTetheredIfaces(intent.extras)
+ debugLog(TAG, "onTetherStateChangedLocked: $ifaces")
+ check(ifaces.size <= 1)
+ val iface = ifaces.singleOrNull()
+ binder.iface = iface
+ if (iface == null) {
+ routingManager?.stop()
+ routingManager = null
+ unregisterReceiver()
+ ServiceNotification.stopForeground(this)
+ stopSelf()
+ } else {
+ val routingManager = routingManager
+ if (routingManager == null) {
+ this.routingManager = LocalOnlyInterfaceManager(iface)
+ IpNeighbourMonitor.registerCallback(this)
+ } else check(iface == routingManager.downstream)
+ }
+ app.handler.post { binder.fragment?.adapter?.updateLocalOnlyViewHolder() }
+ }
+ override val activeIfaces get() = listOfNotNull(binder.iface)
+
+ override fun onBind(intent: Intent?) = binder
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ // throws IllegalStateException if the caller attempts to start the LocalOnlyHotspot while they
+ // have an outstanding request.
+ // https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/53e0284/service/java/com/android/server/wifi/WifiServiceImpl.java#1192
+ try {
+ app.wifi.startLocalOnlyHotspot(object : WifiManager.LocalOnlyHotspotCallback() {
+ override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) {
+ if (reservation == null) onFailed(-2) else {
+ this@LocalOnlyHotspotService.reservation = reservation
+ if (!receiverRegistered) {
+ registerReceiver(receiver, intentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
+ receiverRegistered = true
+ }
+ }
+ }
+
+ override fun onStopped() {
+ debugLog(TAG, "LOHCallback.onStopped")
+ reservation = null
+ }
+
+ override fun onFailed(reason: Int) {
+ Toast.makeText(this@LocalOnlyHotspotService, getString(R.string.tethering_temp_hotspot_failure,
+ when (reason) {
+ WifiManager.LocalOnlyHotspotCallback.ERROR_NO_CHANNEL ->
+ getString(R.string.tethering_temp_hotspot_failure_no_channel)
+ WifiManager.LocalOnlyHotspotCallback.ERROR_GENERIC ->
+ getString(R.string.tethering_temp_hotspot_failure_generic)
+ WifiManager.LocalOnlyHotspotCallback.ERROR_INCOMPATIBLE_MODE ->
+ getString(R.string.tethering_temp_hotspot_failure_incompatible_mode)
+ WifiManager.LocalOnlyHotspotCallback.ERROR_TETHERING_DISALLOWED ->
+ getString(R.string.tethering_temp_hotspot_failure_tethering_disallowed)
+ else -> getString(R.string.failure_reason_unknown, reason)
+ }), Toast.LENGTH_SHORT).show()
+ }
+ }, app.handler)
+ } catch (e: IllegalStateException) {
+ e.printStackTrace()
+ }
+ return START_STICKY
+ }
+
+ override fun onDestroy() {
+ unregisterReceiver()
+ super.onDestroy()
+ }
+
+ private fun unregisterReceiver() {
+ if (receiverRegistered) {
+ unregisterReceiver(receiver)
+ IpNeighbourMonitor.unregisterCallback(this)
+ receiverRegistered = false
+ }
+ }
+}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyInterfaceManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyInterfaceManager.kt
new file mode 100644
index 00000000..d3dee1a8
--- /dev/null
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyInterfaceManager.kt
@@ -0,0 +1,65 @@
+package be.mygod.vpnhotspot
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.support.v4.content.LocalBroadcastManager
+import android.widget.Toast
+import be.mygod.vpnhotspot.App.Companion.app
+import be.mygod.vpnhotspot.net.Routing
+import be.mygod.vpnhotspot.net.VpnMonitor
+import java.net.InetAddress
+import java.net.SocketException
+
+class LocalOnlyInterfaceManager(val downstream: String, private val owner: InetAddress? = null) :
+ BroadcastReceiver(), VpnMonitor.Callback {
+ private var routing: Routing? = null
+ private var dns = emptyList()
+
+ init {
+ LocalBroadcastManager.getInstance(app).registerReceiver(this, intentFilter(App.ACTION_CLEAN_ROUTINGS))
+ VpnMonitor.registerCallback(this) { initRouting() }
+ }
+
+ override fun onAvailable(ifname: String, dns: List) {
+ val routing = routing
+ initRouting(ifname, if (routing == null) owner else {
+ routing.stop()
+ check(routing.upstream == null)
+ routing.hostAddress
+ }, dns)
+ }
+ override fun onLost(ifname: String) {
+ val routing = routing ?: return
+ if (!routing.stop()) app.toast(R.string.noisy_su_failure)
+ initRouting(null, routing.hostAddress, emptyList())
+ }
+ override fun onReceive(context: Context?, intent: Intent?) {
+ val routing = routing ?: return
+ routing.started = false
+ initRouting(routing.upstream, routing.hostAddress, dns)
+ }
+
+ private fun initRouting(upstream: String? = null, owner: InetAddress? = this.owner,
+ dns: List = this.dns) {
+ try {
+ val routing = Routing(upstream, downstream, owner)
+ this.routing = routing
+ this.dns = dns
+ val strict = app.pref.getBoolean("service.repeater.strict", false)
+ if (strict && upstream == null) return // in this case, nothing to be done
+ if (routing.ipForward() // local only interfaces may not enable ip_forward
+ .rule().forward(strict).masquerade(strict).dnsRedirect(dns).start()) return
+ app.toast(R.string.noisy_su_failure)
+ } catch (e: SocketException) {
+ Toast.makeText(app, e.message, Toast.LENGTH_SHORT).show()
+ routing = null
+ }
+ }
+
+ fun stop() {
+ VpnMonitor.unregisterCallback(this)
+ LocalBroadcastManager.getInstance(app).unregisterReceiver(this)
+ if (routing?.stop() == false) app.toast(R.string.noisy_su_failure)
+ }
+}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt
index 60d66959..78a446d8 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt
@@ -61,7 +61,7 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
}
}
- val ssid @Bindable get() = binder?.ssid ?: getText(R.string.repeater_inactive)
+ val ssid @Bindable get() = binder?.ssid ?: getText(R.string.service_inactive)
val addresses @Bindable get(): String {
return try {
NetworkInterface.getByName(p2pInterface ?: return "")?.formatAddresses() ?: ""
@@ -160,7 +160,8 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
private var p2pInterface: String? = null
private var tetheredInterfaces = emptySet()
private val receiver = broadcastReceiver { _, intent ->
- tetheredInterfaces = TetheringManager.getTetheredIfaces(intent.extras).toSet()
+ tetheredInterfaces = TetheringManager.getTetheredIfaces(intent.extras).toSet() +
+ TetheringManager.getLocalOnlyTetheredIfaces(intent.extras)
adapter.recreate()
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt
index 42707233..1e9fb4cf 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt
@@ -16,8 +16,6 @@ import android.support.v4.content.LocalBroadcastManager
import android.util.Log
import android.widget.Toast
import be.mygod.vpnhotspot.App.Companion.app
-import be.mygod.vpnhotspot.net.Routing
-import be.mygod.vpnhotspot.net.VpnMonitor
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.netId
@@ -26,10 +24,8 @@ import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps
import java.lang.reflect.InvocationTargetException
import java.net.InetAddress
-import java.net.SocketException
-class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Callback,
- SharedPreferences.OnSharedPreferenceChangeListener {
+class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPreferences.OnSharedPreferenceChangeListener {
companion object {
const val ACTION_STATUS_CHANGED = "be.mygod.vpnhotspot.RepeaterService.STATUS_CHANGED"
private const val TAG = "RepeaterService"
@@ -110,17 +106,9 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO),
intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO),
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP))
- App.ACTION_CLEAN_ROUTINGS -> if (status == Status.ACTIVE) {
- val routing = routing
- routing!!.started = false
- resetup(routing, upstream, dns)
- }
}
}
-
- private var upstream: String? = null
- private var dns: List = emptyList()
- private var routing: Routing? = null
+ private var routingManager: LocalOnlyInterfaceManager? = null
var status = Status.IDLE
private set(value) {
@@ -134,7 +122,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
WifiP2pManager.P2P_UNSUPPORTED -> getString(R.string.repeater_failure_reason_p2p_unsupported)
WifiP2pManager.BUSY -> getString(R.string.repeater_failure_reason_busy)
WifiP2pManager.NO_SERVICE_REQUESTS -> getString(R.string.repeater_failure_reason_no_service_requests)
- else -> getString(R.string.repeater_failure_reason_unknown, reason)
+ else -> getString(R.string.failure_reason_unknown, reason)
})
override fun onCreate() {
@@ -176,36 +164,19 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
}
/**
- * startService 1st stop
+ * startService Step 1
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (status != Status.IDLE) return START_NOT_STICKY
status = Status.STARTING
- VpnMonitor.registerCallback(this) { setup() }
- return START_NOT_STICKY
- }
- private fun startFailure(msg: CharSequence?, group: WifiP2pGroup? = null) {
- Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
- showNotification()
- if (group != null) removeGroup() else clean()
- }
-
- /**
- * startService 2nd stop
- */
- private fun setup(ifname: String? = null, dns: List = emptyList()) {
val matcher = WifiP2pManagerHelper.patternNetworkInfo.matcher(
loggerSu("dumpsys ${Context.WIFI_P2P_SERVICE}") ?: "")
when {
!matcher.find() -> startFailure(getString(R.string.root_unavailable))
matcher.group(2) == "true" -> {
unregisterReceiver()
- upstream = ifname
- this.dns = dns
registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION))
- LocalBroadcastManager.getInstance(this)
- .registerReceiver(receiver, intentFilter(App.ACTION_CLEAN_ROUTINGS))
receiverRegistered = true
p2pManager.requestGroupInfo(channel, {
when {
@@ -227,72 +198,40 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
}
else -> startFailure(getString(R.string.repeater_p2p_unavailable))
}
+ return START_NOT_STICKY
}
-
- private fun resetup(routing: Routing, ifname: String? = null, dns: List = emptyList()) =
- initRouting(ifname, routing.downstream, routing.hostAddress, dns)
-
- override fun onAvailable(ifname: String, dns: List) = when (status) {
- Status.STARTING -> setup(ifname, dns)
- Status.ACTIVE -> {
- val routing = routing!!
- if (routing.started) {
- routing.stop()
- check(routing.upstream == null)
- }
- resetup(routing, ifname, dns)
- while (false) { }
- }
- else -> throw IllegalStateException("RepeaterService is in unexpected state when receiving onAvailable")
- }
- override fun onLost(ifname: String) {
- if (routing?.stop() == false)
- Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
- upstream = null
- if (status == Status.ACTIVE) resetup(routing!!)
- }
-
+ /**
+ * startService Step 2 (if a group isn't already available)
+ */
private fun doStart() = p2pManager.createGroup(channel, object : WifiP2pManager.ActionListener {
override fun onFailure(reason: Int) = startFailure(formatReason(R.string.repeater_create_group_failure, reason))
- override fun onSuccess() { } // wait for WIFI_P2P_CONNECTION_CHANGED_ACTION to fire
+ override fun onSuccess() { } // wait for WIFI_P2P_CONNECTION_CHANGED_ACTION to fire to go to step 3
})
- private fun doStart(group: WifiP2pGroup) {
- this.group = group
- status = Status.ACTIVE
- showNotification(group)
- }
-
/**
- * startService 3rd stop (if a group isn't already available), also called when connection changed
+ * Used during step 2, also called when connection changed
*/
private fun onP2pConnectionChanged(info: WifiP2pInfo, net: NetworkInfo?, group: WifiP2pGroup) {
debugLog(TAG, "P2P connection changed: $info\n$net\n$group")
if (!info.groupFormed || !info.isGroupOwner || !group.isGroupOwner) {
- if (routing != null) clean() // P2P shutdown
- return
- }
- if (routing == null) try {
- if (initRouting(upstream, group.`interface` ?: return, info.groupOwnerAddress ?: return, dns))
- doStart(group)
- } catch (e: SocketException) {
- startFailure(e.message, group)
- return
- } else showNotification(group)
- this.group = group
+ if (routingManager != null) clean() // P2P shutdown
+ } else if (routingManager != null) {
+ this.group = group
+ showNotification(group)
+ } else doStart(group, info.groupOwnerAddress)
}
- private fun initRouting(upstream: String?, downstream: String,
- owner: InetAddress, dns: List): Boolean {
- val routing = Routing(upstream, downstream, owner)
- this.routing = routing
- this.dns = dns
- val strict = app.pref.getBoolean("service.repeater.strict", false)
- return if (strict && upstream == null || // in this case, nothing to be done
- routing.ipForward() // Wi-Fi direct doesn't enable ip_forward
- .rule().forward(strict).masquerade(strict).dnsRedirect(dns).start()) true else {
- routing.stop()
- Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
- false
- }
+ /**
+ * startService Step 3
+ */
+ private fun doStart(group: WifiP2pGroup, ownerAddress: InetAddress? = null) {
+ this.group = group
+ routingManager = LocalOnlyInterfaceManager(group.`interface`!!, ownerAddress)
+ status = Status.ACTIVE
+ showNotification(group)
+ }
+ private fun startFailure(msg: CharSequence?, group: WifiP2pGroup? = null) {
+ Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
+ showNotification()
+ if (group != null) removeGroup() else clean()
}
private fun showNotification(group: WifiP2pGroup? = null) = ServiceNotification.startForeground(this,
@@ -314,16 +253,13 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
private fun unregisterReceiver() {
if (receiverRegistered) {
unregisterReceiver(receiver)
- LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
receiverRegistered = false
}
}
private fun clean() {
- VpnMonitor.unregisterCallback(this)
unregisterReceiver()
- if (routing?.stop() == false)
- Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
- routing = null
+ routingManager?.stop()
+ routingManager = null
status = Status.IDLE
ServiceNotification.stopForeground(this)
stopSelf()
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringFragment.kt
index 63f03073..f3bf910a 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringFragment.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringFragment.kt
@@ -1,9 +1,12 @@
package be.mygod.vpnhotspot
+import android.Manifest
import android.annotation.SuppressLint
+import android.annotation.TargetApi
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothProfile
import android.content.*
+import android.content.pm.PackageManager
import android.databinding.BaseObservable
import android.databinding.Bindable
import android.databinding.DataBindingUtil
@@ -39,12 +42,15 @@ import java.util.*
class TetheringFragment : Fragment(), ServiceConnection {
companion object {
private const val VIEW_TYPE_INTERFACE = 0
+ private const val VIEW_TYPE_LOCAL_ONLY_HOTSPOT = 6
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
+ private const val START_LOCAL_ONLY_HOTSPOT = 1
+
/**
* PAN Profile
* From BluetoothProfile.java.
@@ -55,26 +61,63 @@ class TetheringFragment : Fragment(), ServiceConnection {
}
}
- inner class Data(val iface: TetheredInterface) : BaseObservable() {
- val icon: Int get() = TetherType.ofInterface(iface.name).icon
- val active = binder?.isActive(iface.name) == true
+ interface Data {
+ val icon: Int
+ val title: CharSequence
+ val text: CharSequence
+ val active: Boolean
+ }
+ inner class TetheredData(val iface: TetheredInterface) : Data {
+ override val icon: Int get() = TetherType.ofInterface(iface.name).icon
+ override val title get() = iface.name
+ override val text get() = iface.addresses
+ override val active = tetheringBinder?.isActive(iface.name) == true
+ }
+ inner class LocalHotspotData(private val lookup: Map) : Data {
+ override val icon: Int get() {
+ val iface = hotspotBinder?.iface ?: return TetherType.WIFI.icon
+ return TetherType.ofInterface(iface).icon
+ }
+ override val title get() = getString(R.string.tethering_temp_hotspot)
+ override val text by lazy {
+ val binder = hotspotBinder
+ val configuration = binder?.configuration ?: return@lazy getText(R.string.service_inactive)
+ val iface = binder.iface ?: return@lazy getText(R.string.service_inactive)
+ "${configuration.SSID} - ${configuration.preSharedKey}\n${TetheredInterface(iface, lookup).addresses}"
+ }
+ override val active = hotspotBinder?.iface != null
}
- private class InterfaceViewHolder(val binding: ListitemInterfaceBinding) : RecyclerView.ViewHolder(binding.root),
- View.OnClickListener {
+ private open class InterfaceViewHolder(val binding: ListitemInterfaceBinding) :
+ RecyclerView.ViewHolder(binding.root), View.OnClickListener {
init {
itemView.setOnClickListener(this)
}
override fun onClick(view: View) {
val context = itemView.context
- val data = binding.data!!
+ val data = binding.data as TetheredData
if (data.active) context.startService(Intent(context, TetheringService::class.java)
.putExtra(TetheringService.EXTRA_REMOVE_INTERFACE, data.iface.name))
else ContextCompat.startForegroundService(context, Intent(context, TetheringService::class.java)
.putExtra(TetheringService.EXTRA_ADD_INTERFACE, data.iface.name))
}
}
+ @RequiresApi(26)
+ private inner class LocalOnlyHotspotViewHolder(binding: ListitemInterfaceBinding) : InterfaceViewHolder(binding) {
+ override fun onClick(view: View) {
+ val binder = hotspotBinder
+ if (binder?.iface != null) binder.stop() else {
+ val context = requireContext()
+ if (context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) ==
+ PackageManager.PERMISSION_GRANTED) {
+ context.startForegroundService(Intent(context, LocalOnlyHotspotService::class.java))
+ } else {
+ requestPermissions(arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION), START_LOCAL_ONLY_HOTSPOT)
+ }
+ }
+ }
+ }
private class ManageViewHolder(view: View) : RecyclerView.ViewHolder(view), View.OnClickListener {
init {
view.setOnClickListener(this)
@@ -160,7 +203,7 @@ class TetheringFragment : Fragment(), ServiceConnection {
}
}
- class TetherListener : BaseObservable(), BluetoothProfile.ServiceListener {
+ inner class TetherListener : BaseObservable(), BluetoothProfile.ServiceListener {
var enabledTypes = emptySet()
@Bindable get
set(value) {
@@ -208,29 +251,40 @@ class TetheringFragment : Fragment(), ServiceConnection {
}
inner class TetheringAdapter :
ListAdapter(TetheredInterface.DiffCallback) {
- fun update(data: Set) {
- val lookup = try {
+ private var lookup: Map = emptyMap()
+
+ fun update(activeIfaces: List, localOnlyIfaces: List) {
+ lookup = try {
NetworkInterface.getNetworkInterfaces().asSequence().associateBy { it.name }
} catch (e: SocketException) {
e.printStackTrace()
- emptyMap()
+ emptyMap()
}
- this@TetheringFragment.tetherListener.enabledTypes = data.map { TetherType.ofInterface(it) }.toSet()
- submitList(data.map { TetheredInterface(it, lookup) }.sorted())
+ this@TetheringFragment.tetherListener.enabledTypes =
+ (activeIfaces + localOnlyIfaces).map { TetherType.ofInterface(it) }.toSet()
+ submitList(activeIfaces.map { TetheredInterface(it, lookup) }.sorted())
+ if (Build.VERSION.SDK_INT >= 26) updateLocalOnlyViewHolder()
}
- 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 getItemCount() = super.getItemCount() + if (Build.VERSION.SDK_INT < 24) 2 else 5
+ override fun getItemViewType(position: Int) = if (Build.VERSION.SDK_INT < 26) {
+ 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
+ }
+ } else {
+ when (position - super.getItemCount()) {
+ 0 -> VIEW_TYPE_LOCAL_ONLY_HOTSPOT
+ 1 -> VIEW_TYPE_MANAGE
+ 2 -> VIEW_TYPE_USB
+ 3 -> VIEW_TYPE_WIFI
+ 4 -> VIEW_TYPE_BLUETOOTH
+ else -> VIEW_TYPE_INTERFACE
+ }
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
@@ -239,22 +293,33 @@ class TetheringFragment : Fragment(), ServiceConnection {
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)
+ VIEW_TYPE_LOCAL_ONLY_HOTSPOT -> @TargetApi(26) {
+ LocalOnlyHotspotViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false))
+ }
else -> throw IllegalArgumentException("Invalid view type")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
- is InterfaceViewHolder -> holder.binding.data = Data(getItem(position))
+ is LocalOnlyHotspotViewHolder -> holder.binding.data = LocalHotspotData(lookup)
+ is InterfaceViewHolder -> holder.binding.data = TetheredData(getItem(position))
}
}
+ @RequiresApi(26)
+ fun updateLocalOnlyViewHolder() {
+ notifyItemChanged(super.getItemCount())
+ notifyItemChanged(super.getItemCount() + 3)
+ }
}
private val tetherListener = TetherListener()
private lateinit var binding: FragmentTetheringBinding
- private var binder: TetheringService.TetheringBinder? = null
+ private var hotspotBinder: LocalOnlyHotspotService.HotspotBinder? = null
+ private var tetheringBinder: TetheringService.TetheringBinder? = null
val adapter = TetheringAdapter()
private val receiver = broadcastReceiver { _, intent ->
- adapter.update(TetheringManager.getTetheredIfaces(intent.extras))
+ adapter.update(TetheringManager.getTetheredIfaces(intent.extras),
+ TetheringManager.getLocalOnlyTetheredIfaces(intent.extras))
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@@ -269,14 +334,23 @@ class TetheringFragment : Fragment(), ServiceConnection {
override fun onStart() {
super.onStart()
val context = requireContext()
- context.registerReceiver(receiver, intentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
context.bindService(Intent(context, TetheringService::class.java), this, Context.BIND_AUTO_CREATE)
+ if (Build.VERSION.SDK_INT >= 26) {
+ context.bindService(Intent(context, LocalOnlyHotspotService::class.java), this, Context.BIND_AUTO_CREATE)
+ }
+ }
+
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ if (requestCode == START_LOCAL_ONLY_HOTSPOT) @TargetApi(26) {
+ if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
+ val context = requireContext()
+ context.startForegroundService(Intent(context, LocalOnlyHotspotService::class.java))
+ }
+ } else super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onStop() {
- val context = requireContext()
- context.unbindService(this)
- context.unregisterReceiver(receiver)
+ requireContext().unbindService(this)
super.onStop()
}
@@ -285,14 +359,34 @@ class TetheringFragment : Fragment(), ServiceConnection {
super.onDestroy()
}
- override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
- val binder = service as TetheringService.TetheringBinder
- this.binder = binder
- binder.fragment = this
+ override fun onServiceConnected(name: ComponentName?, service: IBinder?) = when (service) {
+ is TetheringService.TetheringBinder -> {
+ tetheringBinder = service
+ service.fragment = this
+ requireContext().registerReceiver(receiver, intentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
+ while (false) { }
+ }
+ is LocalOnlyHotspotService.HotspotBinder -> @TargetApi(26) {
+ hotspotBinder = service
+ service.fragment = this
+ adapter.updateLocalOnlyViewHolder()
+ }
+ else -> throw IllegalArgumentException("service")
}
override fun onServiceDisconnected(name: ComponentName?) {
- binder?.fragment = null
- binder = null
+ val context = requireContext()
+ when (name) {
+ ComponentName(context, TetheringService::class.java) -> {
+ tetheringBinder?.fragment = null
+ tetheringBinder = null
+ context.unregisterReceiver(receiver)
+ }
+ ComponentName(context, LocalOnlyHotspotService::class.java) -> {
+ hotspotBinder?.fragment = null
+ hotspotBinder = null
+ }
+ else -> throw IllegalArgumentException("name")
+ }
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt
index ce9c6263..f002babc 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt
@@ -1,16 +1,18 @@
package be.mygod.vpnhotspot
-import android.app.Service
import android.content.Intent
import android.os.Binder
import android.support.v4.content.LocalBroadcastManager
import android.widget.Toast
import be.mygod.vpnhotspot.App.Companion.app
-import be.mygod.vpnhotspot.net.*
+import be.mygod.vpnhotspot.net.IpNeighbourMonitor
+import be.mygod.vpnhotspot.net.Routing
+import be.mygod.vpnhotspot.net.TetheringManager
+import be.mygod.vpnhotspot.net.VpnMonitor
import java.net.InetAddress
import java.net.SocketException
-class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Callback {
+class TetheringService : IpNeighbourMonitoringService(), VpnMonitor.Callback {
companion object {
const val EXTRA_ADD_INTERFACE = "interface.add"
const val EXTRA_REMOVE_INTERFACE = "interface.remove"
@@ -24,7 +26,6 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
private val binder = TetheringBinder()
private val routings = HashMap()
- private var neighbours = emptyList()
private var upstream: String? = null
private var dns: List = emptyList()
private var receiverRegistered = false
@@ -32,9 +33,8 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
synchronized(routings) {
when (intent.action) {
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 }
+ val failed = (routings.keys - TetheringManager.getTetheredIfaces(intent.extras))
+ .any { routings.remove(it)?.stop() == false }
if (failed) Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
}
App.ACTION_CLEAN_ROUTINGS -> for (iface in routings.keys) routings[iface] = null
@@ -42,13 +42,10 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
updateRoutingsLocked()
}
}
+ override val activeIfaces get() = synchronized(routings) { routings.keys.toList() }
- private fun updateRoutingsLocked() {
- if (routings.isEmpty()) {
- unregisterReceiver()
- ServiceNotification.stopForeground(this)
- stopSelf()
- } else {
+ fun updateRoutingsLocked() {
+ if (routings.isNotEmpty()) {
val upstream = upstream
if (upstream != null) {
var failed = false
@@ -56,13 +53,11 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
// system tethering already has working forwarding rules
// so it doesn't make sense to add additional forwarding rules
val routing = Routing(upstream, downstream).rule().forward().masquerade().dnsRedirect(dns)
- if (routing.start()) routings[downstream] = routing else {
- failed = true
- routing.stop()
- routings.remove(downstream)
- }
+ routings[downstream] = routing
+ if (!routing.start()) failed = true
} catch (e: SocketException) {
e.printStackTrace()
+ routings.remove(downstream)
failed = true
}
if (failed) Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
@@ -76,6 +71,11 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
}
postIpNeighbourAvailable()
}
+ if (routings.isEmpty()) {
+ unregisterReceiver()
+ ServiceNotification.stopForeground(this)
+ stopSelf()
+ }
app.handler.post { binder.fragment?.adapter?.notifyDataSetChanged() }
}
@@ -113,21 +113,6 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
if (failed) Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
}
- override fun onIpNeighbourAvailable(neighbours: Map) {
- this.neighbours = neighbours.values.toList()
- }
- override fun postIpNeighbourAvailable() {
- val sizeLookup = neighbours.groupBy { it.dev }.mapValues { (_, neighbours) ->
- neighbours
- .filter { it.state != IpNeighbour.State.FAILED }
- .distinctBy { it.lladdr }
- .size
- }
- ServiceNotification.startForeground(this, synchronized(routings) {
- routings.keys.associate { Pair(it, sizeLookup[it] ?: 0) }
- })
- }
-
override fun onDestroy() {
unregisterReceiver()
super.onDestroy()
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt
index f0e4bec5..daa60ef2 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt
@@ -113,7 +113,8 @@ class Routing(val upstream: String?, val downstream: String, ownerAddress: InetA
fun start(): Boolean {
if (started) return true
started = true
- return noisySu(startScript) == true
+ if (noisySu(startScript) != true) stop()
+ return started
}
fun stop(): Boolean {
if (!started) return true
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt
index ae3126f7..6cc910dc 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt
@@ -125,7 +125,8 @@ object TetheringManager {
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()
+ fun getTetheredIfaces(extras: Bundle) = extras.getStringArrayList(
+ if (Build.VERSION.SDK_INT >= 26) EXTRA_ACTIVE_TETHER else EXTRA_ACTIVE_TETHER_LEGACY)
+ fun getLocalOnlyTetheredIfaces(extras: Bundle) =
+ if (Build.VERSION.SDK_INT >= 26) extras.getStringArrayList(EXTRA_ACTIVE_LOCAL_ONLY) else emptyList()
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt
index 62238251..27e62a47 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt
@@ -1,13 +1,10 @@
package be.mygod.vpnhotspot.net.wifi
-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)
/**
@@ -23,12 +20,14 @@ object WifiApManager {
private fun WifiManager.setWifiApEnabled(wifiConfig: WifiConfiguration?, enabled: Boolean) =
setWifiApEnabled.invoke(this, wifiConfig, enabled) as Boolean
+ @Deprecated("No longer usable since API 26.")
fun start(wifiConfig: WifiConfiguration? = null) {
- wifi.isWifiEnabled = false
- wifi.setWifiApEnabled(wifiConfig, true)
+ app.wifi.isWifiEnabled = false
+ app.wifi.setWifiApEnabled(wifiConfig, true)
}
+ @Deprecated("No longer usable since API 26.")
fun stop() {
- wifi.setWifiApEnabled(null, false)
- wifi.isWifiEnabled = true
+ app.wifi.setWifiApEnabled(null, false)
+ app.wifi.isWifiEnabled = true
}
}
diff --git a/mobile/src/main/res/layout/listitem_interface.xml b/mobile/src/main/res/layout/listitem_interface.xml
index 5e0f6146..f43d21c5 100644
--- a/mobile/src/main/res/layout/listitem_interface.xml
+++ b/mobile/src/main/res/layout/listitem_interface.xml
@@ -35,7 +35,7 @@
@@ -43,7 +43,7 @@
diff --git a/mobile/src/main/res/values-zh-rCN/strings.xml b/mobile/src/main/res/values-zh-rCN/strings.xml
index 01c236c4..da9f64c7 100644
--- a/mobile/src/main/res/values-zh-rCN/strings.xml
+++ b/mobile/src/main/res/values-zh-rCN/strings.xml
@@ -4,6 +4,7 @@
无线中继
系统共享
设置选项
+ 未打开
中继地址
输入 PIN
@@ -17,7 +18,6 @@
凭据已重置。
重置凭据失败(原因:%s)
- 未打开
Wi\u2011Fi 直连不可用
创建 P2P 群组失败(原因:%s)
关闭已有 P2P 群组失败(原因:%s)
@@ -28,7 +28,13 @@
设备不支持 Wi\u2011Fi 直连
系统忙
未添加服务请求
- 未知 #%d
+
+ 临时 WLAN 热点
+ 打开热点失败 (原因:%s)
+ 无频段
+ 通用错误
+ 模式不兼容
+ 共享被禁用
管理…