Support monitoring tethered interface

This would be useful to be used in together with Instant Tethering + Turn off hotspot automatically.

Refine #26, #53.
This commit is contained in:
Mygod
2019-02-06 01:26:06 +08:00
parent 6d6418b8e0
commit cbc65f989c
14 changed files with 174 additions and 61 deletions

View File

@@ -8,6 +8,7 @@ abstract class IpNeighbourMonitoringService : Service(), IpNeighbourMonitor.Call
private var neighbours = emptyList<IpNeighbour>() private var neighbours = emptyList<IpNeighbour>()
protected abstract val activeIfaces: List<String> protected abstract val activeIfaces: List<String>
protected open val inactiveIfaces get() = emptyList<String>()
override fun onIpNeighbourAvailable(neighbours: List<IpNeighbour>) { override fun onIpNeighbourAvailable(neighbours: List<IpNeighbour>) {
this.neighbours = neighbours this.neighbours = neighbours
@@ -20,6 +21,8 @@ abstract class IpNeighbourMonitoringService : Service(), IpNeighbourMonitor.Call
.distinctBy { it.lladdr } .distinctBy { it.lladdr }
.size .size
} }
ServiceNotification.startForeground(this, activeIfaces.associate { Pair(it, sizeLookup[it] ?: 0) }) ServiceNotification.startForeground(this,
activeIfaces.associate { Pair(it, sizeLookup[it] ?: 0) },
inactiveIfaces)
} }
} }

View File

@@ -52,7 +52,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService() {
} else { } else {
val routingManager = routingManager val routingManager = routingManager
if (routingManager == null) { if (routingManager == null) {
this.routingManager = RoutingManager.LocalOnly(this, iface).apply { initRouting() } this.routingManager = RoutingManager.LocalOnly(this, iface).apply { start() }
IpNeighbourMonitor.registerCallback(this) IpNeighbourMonitor.registerCallback(this)
} else check(iface == routingManager.downstream) } else check(iface == routingManager.downstream)
} }
@@ -128,7 +128,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService() {
} }
private fun unregisterReceiver() { private fun unregisterReceiver() {
routingManager?.stop() routingManager?.destroy()
routingManager = null routingManager = null
if (receiverRegistered) { if (receiverRegistered) {
unregisterReceiver(receiver) unregisterReceiver(receiver)

View File

@@ -277,7 +277,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
private fun doStart(group: WifiP2pGroup) { private fun doStart(group: WifiP2pGroup) {
binder.group = group binder.group = group
check(routingManager == null) check(routingManager == null)
routingManager = RoutingManager.LocalOnly(this, group.`interface`!!).apply { initRouting() } routingManager = RoutingManager.LocalOnly(this, group.`interface`!!).apply { start() }
status = Status.ACTIVE status = Status.ACTIVE
showNotification(group) showNotification(group)
} }
@@ -309,7 +309,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
} }
private fun clean() { private fun clean() {
unregisterReceiver() unregisterReceiver()
routingManager?.stop() routingManager?.destroy()
routingManager = null routingManager = null
status = Status.IDLE status = Status.IDLE
ServiceNotification.stopForeground(this) ServiceNotification.stopForeground(this)

View File

@@ -30,15 +30,21 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
} }
} }
var routing: Routing? = null var started = false
private var routing: Routing? = null
init { init {
app.onPreCleanRoutings[this] = { routing?.stop() }
app.onRoutingsCleaned[this] = { initRouting() }
if (isWifi) WifiDoubleLock.acquire(this) if (isWifi) WifiDoubleLock.acquire(this)
} }
fun initRouting() = try { fun start(): Boolean {
check(!started)
started = true
app.onPreCleanRoutings[this] = { routing?.stop() }
app.onRoutingsCleaned[this] = { initRouting() }
return initRouting()
}
private fun initRouting() = try {
routing = Routing(caller, downstream).apply { routing = Routing(caller, downstream).apply {
try { try {
configure() configure()
@@ -58,9 +64,15 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
protected abstract fun Routing.configure() protected abstract fun Routing.configure()
fun stop() { fun stop() {
if (!started) return
routing?.revert() routing?.revert()
if (isWifi) WifiDoubleLock.release(this)
app.onPreCleanRoutings -= this app.onPreCleanRoutings -= this
app.onRoutingsCleaned -= this app.onRoutingsCleaned -= this
started = false
}
fun destroy() {
if (isWifi) WifiDoubleLock.release(this)
stop()
} }
} }

View File

@@ -9,12 +9,14 @@ import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import java.util.*
object ServiceNotification { object ServiceNotification {
private const val CHANNEL = "tethering" private const val CHANNEL = "tethering"
private const val CHANNEL_ID = 1 private const val CHANNEL_ID = 1
private val deviceCountsMap = HashMap<Service, Map<String, Int>>() private val deviceCountsMap = WeakHashMap<Service, Map<String, Int>>()
private val inactiveMap = WeakHashMap<Service, List<String>>()
private val manager = app.getSystemService<NotificationManager>()!! private val manager = app.getSystemService<NotificationManager>()!!
private fun buildNotification(context: Context): Notification { private fun buildNotification(context: Context): Notification {
@@ -27,32 +29,29 @@ object ServiceNotification {
Intent(context, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT)) Intent(context, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
val deviceCounts = deviceCountsMap.values.flatMap { it.entries }.sortedBy { it.key } val deviceCounts = deviceCountsMap.values.flatMap { it.entries }.sortedBy { it.key }
return when (deviceCounts.size) { val inactive = inactiveMap.values.flatten()
0 -> builder.build() var lines = deviceCounts.map { (dev, size) ->
1 -> { context.resources.getQuantityString(R.plurals.notification_connected_devices, size, size, dev)
val (dev, size) = deviceCounts.single() }
builder.setContentText(context.resources.getQuantityString(R.plurals.notification_connected_devices, if (inactive.isNotEmpty()) {
size, size, dev)) lines += context.getString(R.string.notification_interfaces_inactive) + inactive.joinToString()
.build() }
} return if (lines.size <= 1) builder.setContentText(lines.singleOrNull()).build() else {
else -> { val deviceCount = deviceCounts.sumBy { it.value }
val deviceCount = deviceCounts.sumBy { it.value } val interfaceCount = deviceCounts.size + inactive.size
NotificationCompat.BigTextStyle(builder NotificationCompat.BigTextStyle(builder
.setContentText(context.resources.getQuantityString(R.plurals.notification_connected_devices, .setContentText(context.resources.getQuantityString(R.plurals.notification_connected_devices,
deviceCount, deviceCount, deviceCount, deviceCount,
context.resources.getQuantityString(R.plurals.notification_interfaces, context.resources.getQuantityString(R.plurals.notification_interfaces,
deviceCounts.size, deviceCounts.size)))) interfaceCount, interfaceCount))))
.bigText(deviceCounts.joinToString("\n") { (dev, size) -> .bigText(lines.joinToString("\n"))
context.resources.getQuantityString(R.plurals.notification_connected_devices, .build()
size, size, dev)
})
.build()
}
} }
} }
fun startForeground(service: Service, deviceCounts: Map<String, Int>) { fun startForeground(service: Service, deviceCounts: Map<String, Int>, inactive: List<String> = emptyList()) {
deviceCountsMap[service] = deviceCounts deviceCountsMap[service] = deviceCounts
if (inactive.isEmpty()) inactiveMap.remove(service) else inactiveMap[service] = inactive
service.startForeground(CHANNEL_ID, buildNotification(service)) service.startForeground(CHANNEL_ID, buildNotification(service))
} }
fun stopForeground(service: Service) { fun stopForeground(service: Service) {

View File

@@ -16,16 +16,22 @@ import kotlinx.coroutines.launch
class TetheringService : IpNeighbourMonitoringService() { class TetheringService : IpNeighbourMonitoringService() {
companion object { companion object {
const val EXTRA_ADD_INTERFACES = "interface.add" const val EXTRA_ADD_INTERFACES = "interface.add"
const val EXTRA_ADD_INTERFACE_MONITOR = "interface.add.monitor"
const val EXTRA_REMOVE_INTERFACE = "interface.remove" const val EXTRA_REMOVE_INTERFACE = "interface.remove"
} }
inner class Binder : android.os.Binder() { inner class Binder : android.os.Binder() {
val routingsChanged = Event0() val routingsChanged = Event0()
val monitoredIfaces get() = synchronized(downstreams) {
downstreams.values.filter { it.monitor }.map { it.downstream }
}
fun isActive(iface: String): Boolean = synchronized(downstreams) { downstreams.containsKey(iface) } fun isActive(iface: String) = synchronized(downstreams) { downstreams.containsKey(iface) }
fun isInactive(iface: String) = synchronized(downstreams) { downstreams[iface] }?.run { !started && monitor }
fun monitored(iface: String) = synchronized(downstreams) { downstreams[iface] }?.monitor
} }
private inner class Downstream(caller: Any, downstream: String) : private inner class Downstream(caller: Any, downstream: String, var monitor: Boolean = false) :
RoutingManager(caller, downstream, TetherType.ofInterface(downstream).isWifi) { RoutingManager(caller, downstream, TetherType.ofInterface(downstream).isWifi) {
override fun Routing.configure() { override fun Routing.configure() {
forward() forward()
@@ -41,12 +47,23 @@ class TetheringService : IpNeighbourMonitoringService() {
private val receiver = broadcastReceiver { _, intent -> private val receiver = broadcastReceiver { _, intent ->
val extras = intent.extras ?: return@broadcastReceiver val extras = intent.extras ?: return@broadcastReceiver
synchronized(downstreams) { synchronized(downstreams) {
for (iface in downstreams.keys - TetheringManager.getTetheredIfaces(extras)) val toRemove = downstreams.toMutableMap() // make a copy
downstreams.remove(iface)?.stop() for (iface in TetheringManager.getTetheredIfaces(extras)) {
val downstream = toRemove.remove(iface) ?: continue
if (downstream.monitor && !downstream.started) downstream.start()
}
for ((iface, downstream) in toRemove) {
if (downstream.monitor) downstream.stop() else downstreams.remove(iface)?.destroy()
}
onDownstreamsChangedLocked() onDownstreamsChangedLocked()
} }
} }
override val activeIfaces get() = synchronized(downstreams) { downstreams.keys.toList() } override val activeIfaces get() = synchronized(downstreams) {
downstreams.values.filter { it.started }.map { it.downstream }
}
override val inactiveIfaces get() = synchronized(downstreams) {
downstreams.values.filter { !it.started }.map { it.downstream }
}
private fun onDownstreamsChangedLocked() { private fun onDownstreamsChangedLocked() {
if (downstreams.isEmpty()) { if (downstreams.isEmpty()) {
@@ -67,22 +84,30 @@ class TetheringService : IpNeighbourMonitoringService() {
override fun onBind(intent: Intent?) = binder override fun onBind(intent: Intent?) = binder
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent != null) { if (intent != null) synchronized(downstreams) {
val ifaces = intent.getStringArrayExtra(EXTRA_ADD_INTERFACES) ?: emptyArray() for (iface in intent.getStringArrayExtra(EXTRA_ADD_INTERFACES) ?: emptyArray()) {
synchronized(downstreams) { if (downstreams[iface] == null) Downstream(this, iface).apply {
for (iface in ifaces) Downstream(this, iface).let { downstream -> if (start()) check(downstreams.put(iface, this) == null) else destroy()
if (downstream.initRouting()) downstreams[iface] = downstream else downstream.stop()
} }
downstreams.remove(intent.getStringExtra(EXTRA_REMOVE_INTERFACE))?.stop()
onDownstreamsChangedLocked()
} }
intent.getStringExtra(EXTRA_ADD_INTERFACE_MONITOR)?.let { iface ->
val downstream = downstreams[iface]
if (downstream == null) Downstream(this, iface, true).apply {
start()
check(downstreams.put(iface, this) == null)
downstreams[iface] = this
} else downstream.monitor = true
}
downstreams.remove(intent.getStringExtra(EXTRA_REMOVE_INTERFACE))?.destroy()
updateNotification() // call this first just in case we are shutting down immediately
onDownstreamsChangedLocked()
} else if (downstreams.isEmpty()) stopSelf(startId) } else if (downstreams.isEmpty()) stopSelf(startId)
return START_NOT_STICKY return START_NOT_STICKY
} }
override fun onDestroy() { override fun onDestroy() {
synchronized(downstreams) { synchronized(downstreams) {
downstreams.values.forEach { it.stop() } // force clean to prevent leakage downstreams.values.forEach { it.destroy() } // force clean to prevent leakage
unregisterReceiver() unregisterReceiver()
} }
super.onDestroy() super.onDestroy()

View File

@@ -4,6 +4,7 @@ import android.content.Intent
import android.view.View import android.view.View
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.TetheringService import be.mygod.vpnhotspot.TetheringService
import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetherType
@@ -30,12 +31,14 @@ class InterfaceManager(private val parent: TetheringFragment, val iface: String)
} }
private inner class Data : be.mygod.vpnhotspot.manage.Data() { private inner class Data : be.mygod.vpnhotspot.manage.Data() {
override val icon get() = TetherType.ofInterface(iface).icon override val icon get() = TetherType.ofInterface(iface).icon
override val title get() = iface override val title get() = if (parent.binder?.monitored(iface) == true) {
parent.getString(R.string.tethering_state_monitored, iface)
} else iface
override val text get() = addresses override val text get() = addresses
override val active get() = parent.binder?.isActive(iface) == true override val active get() = parent.binder?.isActive(iface) == true
} }
private val addresses = parent.ifaceLookup[iface]?.formatAddresses() ?: "" private val addresses = parent.ifaceLookup[iface] ?.formatAddresses(parent.binder?.isInactive(iface) == true) ?: ""
override val type get() = VIEW_TYPE_INTERFACE override val type get() = VIEW_TYPE_INTERFACE
private val data = Data() private val data = Data()

View File

@@ -9,29 +9,26 @@ import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.view.LayoutInflater import android.view.*
import android.view.View import androidx.core.content.ContextCompat
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import be.mygod.vpnhotspot.LocalOnlyHotspotService import be.mygod.vpnhotspot.*
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.RepeaterService
import be.mygod.vpnhotspot.TetheringService
import be.mygod.vpnhotspot.databinding.FragmentTetheringBinding import be.mygod.vpnhotspot.databinding.FragmentTetheringBinding
import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.util.ServiceForegroundConnector import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.broadcastReceiver import be.mygod.vpnhotspot.util.broadcastReceiver
import kotlinx.android.synthetic.main.activity_main.*
import timber.log.Timber import timber.log.Timber
import java.net.NetworkInterface import java.net.NetworkInterface
import java.net.SocketException import java.net.SocketException
class TetheringFragment : Fragment(), ServiceConnection { class TetheringFragment : Fragment(), ServiceConnection, MenuItem.OnMenuItemClickListener {
companion object { companion object {
const val START_LOCAL_ONLY_HOTSPOT = 1 const val START_LOCAL_ONLY_HOTSPOT = 1
const val REPEATER_EDIT_CONFIGURATION = 2 const val REPEATER_EDIT_CONFIGURATION = 2
@@ -63,7 +60,10 @@ class TetheringFragment : Fragment(), ServiceConnection {
val list = ArrayList<Manager>() val list = ArrayList<Manager>()
if (RepeaterService.supported) list.add(repeaterManager) if (RepeaterService.supported) list.add(repeaterManager)
if (Build.VERSION.SDK_INT >= 26) list.add(localOnlyHotspotManager) if (Build.VERSION.SDK_INT >= 26) list.add(localOnlyHotspotManager)
list.addAll(activeIfaces.map { InterfaceManager(this@TetheringFragment, it) }.sortedBy { it.iface }) val monitoredIfaces = binder?.monitoredIfaces ?: emptyList()
updateMonitorList(activeIfaces - monitoredIfaces)
list.addAll((activeIfaces + monitoredIfaces).toSortedSet()
.map { InterfaceManager(this@TetheringFragment, it) })
list.add(ManageBar) list.add(ManageBar)
if (Build.VERSION.SDK_INT >= 24) { if (Build.VERSION.SDK_INT >= 24) {
list.addAll(tetherManagers) list.addAll(tetherManagers)
@@ -94,6 +94,27 @@ class TetheringFragment : Fragment(), ServiceConnection {
extras.getStringArrayList(TetheringManager.EXTRA_ERRORED_TETHER)!!) extras.getStringArrayList(TetheringManager.EXTRA_ERRORED_TETHER)!!)
} }
private fun updateMonitorList(canMonitor: List<String> = emptyList()) {
val toolbar = requireActivity().toolbar
val menu = toolbar.menu
if (canMonitor.isEmpty()) menu.removeItem(R.id.monitor) else {
var item = menu.findItem(R.id.monitor)
if (item == null) {
toolbar.inflateMenu(R.menu.toolbar_monitor)
item = menu.findItem(R.id.monitor)!!
}
item.subMenu.apply {
clear()
canMonitor.sorted().forEach { add(it).setOnMenuItemClickListener(this@TetheringFragment) }
}
}
}
override fun onMenuItemClick(item: MenuItem?): Boolean {
ContextCompat.startForegroundService(requireContext(), Intent(context, TetheringService::class.java)
.putExtra(TetheringService.EXTRA_ADD_INTERFACE_MONITOR, item?.title ?: return false))
return true
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_tethering, container, false) binding = DataBindingUtil.inflate(inflater, R.layout.fragment_tethering, container, false)
binding.setLifecycleOwner(this) binding.setLifecycleOwner(this)
@@ -127,7 +148,13 @@ class TetheringFragment : Fragment(), ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) { override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
binder = service as TetheringService.Binder binder = service as TetheringService.Binder
service.routingsChanged[this] = { adapter.notifyDataSetChanged() } service.routingsChanged[this] = {
requireContext().apply {
// flush tethered interfaces
unregisterReceiver(receiver)
registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
}
}
requireContext().registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED)) requireContext().registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
} }
@@ -136,4 +163,9 @@ class TetheringFragment : Fragment(), ServiceConnection {
binder = null binder = null
requireContext().unregisterReceiver(receiver) requireContext().unregisterReceiver(receiver)
} }
override fun onDestroy() {
updateMonitorList()
super.onDestroy()
}
} }

View File

@@ -41,6 +41,11 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
*/ */
val IPTABLES = if (Build.VERSION.SDK_INT >= 26) "iptables -w 1" else "iptables -w" val IPTABLES = if (Build.VERSION.SDK_INT >= 26) "iptables -w 1" else "iptables -w"
/**
* For debugging: check that we do not start a Routing for the same interface twice.
*/
private var downstreams = mutableSetOf<String>()
fun clean() { fun clean() {
TrafficRecorder.clean() TrafficRecorder.clean()
RootSession.use { RootSession.use {
@@ -54,6 +59,7 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
it.execQuiet("while ip rule del priority $RULE_PRIORITY_UPSTREAM; do done") it.execQuiet("while ip rule del priority $RULE_PRIORITY_UPSTREAM; do done")
it.execQuiet("while ip rule del priority $RULE_PRIORITY_UPSTREAM_FALLBACK; do done") it.execQuiet("while ip rule del priority $RULE_PRIORITY_UPSTREAM_FALLBACK; do done")
} }
downstreams.clear()
} }
private fun RootSession.Transaction.iptables(command: String, revert: String) { private fun RootSession.Transaction.iptables(command: String, revert: String) {
@@ -82,6 +88,10 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
override val message: String get() = app.getString(R.string.exception_interface_not_found) override val message: String get() = app.getString(R.string.exception_interface_not_found)
} }
init {
check(downstreams.add(downstream)) { "Double routing detected" }
}
private val hostAddress = try { private val hostAddress = try {
NetworkInterface.getByName(downstream)!!.interfaceAddresses!!.asSequence().single { it.address is Inet4Address } NetworkInterface.getByName(downstream)!!.interfaceAddresses!!.asSequence().single { it.address is Inet4Address }
} catch (e: Exception) { } catch (e: Exception) {
@@ -302,5 +312,6 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
fallbackUpstream.subrouting?.transaction?.revert() fallbackUpstream.subrouting?.transaction?.revert()
upstream.subrouting?.transaction?.revert() upstream.subrouting?.transaction?.revert()
transaction.revert() transaction.revert()
check(downstreams.remove(downstream)) { "Double reverting detected" }
} }
} }

View File

@@ -58,11 +58,11 @@ fun makeMacSpan(mac: String) = if (app.hasTouch) SpannableString(mac).apply {
setSpan(CustomTabsUrlSpan("https://macvendors.co/results/$mac"), 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) setSpan(CustomTabsUrlSpan("https://macvendors.co/results/$mac"), 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
} else mac } else mac
fun NetworkInterface.formatAddresses() = SpannableStringBuilder().apply { fun NetworkInterface.formatAddresses(macOnly: Boolean = false) = SpannableStringBuilder().apply {
try { try {
hardwareAddress?.apply { appendln(makeMacSpan(asIterable().macToString())) } hardwareAddress?.apply { appendln(makeMacSpan(asIterable().macToString())) }
} catch (_: SocketException) { } } catch (_: SocketException) { }
for (address in interfaceAddresses) { if (!macOnly) for (address in interfaceAddresses) {
append(makeIpSpan(address.address)) append(makeIpSpan(address.address))
appendln("/${address.networkPrefixLength}") appendln("/${address.networkPrefixLength}")
} }

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FF000000"
android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/monitor"
android:icon="@drawable/ic_image_remove_red_eye"
android:title="@string/tethering_monitor"
app:showAsAction="always">
<menu/>
</item>
</menu>

View File

@@ -39,6 +39,9 @@
<string name="tethering_temp_hotspot_failure_incompatible_mode">模式不兼容</string> <string name="tethering_temp_hotspot_failure_incompatible_mode">模式不兼容</string>
<string name="tethering_temp_hotspot_failure_tethering_disallowed">共享被禁用</string> <string name="tethering_temp_hotspot_failure_tethering_disallowed">共享被禁用</string>
<string name="tethering_monitor">监视…</string>
<string name="tethering_state_monitored">%s监视</string>
<string name="tethering_manage">管理系统共享…</string> <string name="tethering_manage">管理系统共享…</string>
<string name="tethering_manage_offload_enabled">若 VPN 共享无法使用,请尝试禁用“开发者选项”中的“网络共享硬件加速”。</string> <string name="tethering_manage_offload_enabled">若 VPN 共享无法使用,请尝试禁用“开发者选项”中的“网络共享硬件加速”。</string>
<!-- <!--
@@ -122,6 +125,7 @@
<plurals name="notification_interfaces"> <plurals name="notification_interfaces">
<item quantity="other">%d 个接口</item> <item quantity="other">%d 个接口</item>
</plurals> </plurals>
<string name="notification_interfaces_inactive">不活跃:</string>
<string name="failure_reason_unknown">未知 #%d</string> <string name="failure_reason_unknown">未知 #%d</string>
<string name="exception_interface_not_found">错误:未找到下游接口</string> <string name="exception_interface_not_found">错误:未找到下游接口</string>

View File

@@ -49,6 +49,9 @@
<string name="tethering_temp_hotspot_failure_incompatible_mode">incompatible mode</string> <string name="tethering_temp_hotspot_failure_incompatible_mode">incompatible mode</string>
<string name="tethering_temp_hotspot_failure_tethering_disallowed">tethering disallowed</string> <string name="tethering_temp_hotspot_failure_tethering_disallowed">tethering disallowed</string>
<string name="tethering_monitor">Monitor…</string>
<string name="tethering_state_monitored">%s (monitored)</string>
<string name="tethering_manage">Manage system tethering…</string> <string name="tethering_manage">Manage system tethering…</string>
<string name="tethering_manage_offload_enabled">Please disable Tethering hardware acceleration in Developer options <string name="tethering_manage_offload_enabled">Please disable Tethering hardware acceleration in Developer options
if VPN tethering does not work.</string> if VPN tethering does not work.</string>
@@ -131,6 +134,7 @@
<item quantity="one">%d interface</item> <item quantity="one">%d interface</item>
<item quantity="other">%d interfaces</item> <item quantity="other">%d interfaces</item>
</plurals> </plurals>
<string name="notification_interfaces_inactive">"Inactive: "</string>
<string name="failure_reason_unknown">unknown #%d</string> <string name="failure_reason_unknown">unknown #%d</string>
<string name="exception_interface_not_found">Fatal: Downstream interface not found</string> <string name="exception_interface_not_found">Fatal: Downstream interface not found</string>