Implement MAC lookup (#68)

* Implement MAC lookup

* Refine error processing

* Use long to store MAC consistently

* Link back to macvendors.co

* Undo some havoc

* Do not show mac spans for TV

* Show MAC and IP in a consistent order

* Add IP spans by ipinfo.io

* Add SpanFormatter

* Fix IPv6 ipinfo.io link

* Refine SpanFormatter

* Fix pressing the link
This commit is contained in:
Mygod
2019-01-26 21:20:40 +08:00
committed by GitHub
parent 94114f7a4b
commit d4208affbb
38 changed files with 562 additions and 112 deletions

View File

@@ -0,0 +1,8 @@
package be.mygod.vpnhotspot.util
import android.text.style.URLSpan
import android.view.View
class CustomTabsUrlSpan(url: String) : URLSpan(url) {
override fun onClick(widget: View) = widget.context.launchUrl(url)
}

View File

@@ -0,0 +1,14 @@
package be.mygod.vpnhotspot.util
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
interface MainScope : CoroutineScope {
class Supervisor : MainScope {
override val job = SupervisorJob()
}
val job: Job
override val coroutineContext get() = Dispatchers.Main + job
}

View File

@@ -0,0 +1,85 @@
package be.mygod.vpnhotspot.util
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.SpannedString
import java.util.*
/**
* Provides [String.format] style functions that work with [Spanned] strings and preserve formatting.
*
* https://github.com/george-steel/android-utils/blob/289aff11e53593a55d780f9f5986e49343a79e55/src/org/oshkimaadziig/george/androidutils/SpanFormatter.java
*
* @author George T. Steel
*/
object SpanFormatter {
private val formatSequence = "%([0-9]+\\$|<?)([^a-zA-z%]*)([[a-zA-Z%]&&[^tT]]|[tT][a-zA-Z])".toPattern()
/**
* Version of [String.format] that works on [Spanned] strings to preserve rich text formatting.
* Both the `format` as well as any `%s args` can be Spanned and will have their formatting preserved.
* Due to the way [Spannable]s work, any argument's spans will can only be included **once** in the result.
* Any duplicates will appear as text only.
*
* @param format the format string (see [java.util.Formatter.format])
* @param args
* the list of arguments passed to the formatter. If there are
* more arguments than required by `format`,
* additional arguments are ignored.
* @return the formatted string (with spans).
*/
fun format(format: CharSequence, vararg args: Any) = format(Locale.getDefault(), format, *args)
/**
* Version of [String.format] that works on [Spanned] strings to preserve rich text formatting.
* Both the `format` as well as any `%s args` can be Spanned and will have their formatting preserved.
* Due to the way [Spannable]s work, any argument's spans will can only be included **once** in the result.
* Any duplicates will appear as text only.
*
* @param locale
* the locale to apply; `null` value means no localization.
* @param format the format string (see [java.util.Formatter.format])
* @param args
* the list of arguments passed to the formatter.
* @return the formatted string (with spans).
* @see String.format
*/
fun format(locale: Locale, format: CharSequence, vararg args: Any): SpannedString {
val out = SpannableStringBuilder(format)
var i = 0
var argAt = -1
while (i < out.length) {
val m = formatSequence.matcher(out)
if (!m.find(i)) break
i = m.start()
val exprEnd = m.end()
val argTerm = m.group(1)
val modTerm = m.group(2)
val typeTerm = m.group(3)
val cookedArg = when (typeTerm) {
"%" -> "%"
"n" -> "\n"
else -> {
val argItem = args[when (argTerm) {
"" -> ++argAt
"<" -> argAt
else -> Integer.parseInt(argTerm.substring(0, argTerm.length - 1)) - 1
}]
if (typeTerm == "s" && argItem is Spanned) argItem else {
String.format(locale, "%$modTerm$typeTerm", argItem)
}
}
}
out.replace(i, exprEnd, cookedArg)
i += cookedArg.length
}
return SpannedString(out)
}
}

View File

@@ -4,11 +4,18 @@ import android.content.*
import android.os.Build
import android.system.Os
import android.system.OsConstants
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.view.View
import android.widget.ImageView
import androidx.annotation.DrawableRes
import androidx.core.net.toUri
import androidx.core.view.isVisible
import androidx.databinding.BindingAdapter
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.room.macToString
import be.mygod.vpnhotspot.widget.SmartSnackbar
import java.net.InetAddress
import java.net.NetworkInterface
import java.net.SocketException
@@ -43,20 +50,40 @@ fun setVisibility(view: View, value: Boolean) {
view.isVisible = value
}
fun NetworkInterface.formatAddresses() =
(interfaceAddresses.asSequence()
.map { "${it.address.hostAddress}/${it.networkPrefixLength}" }
.toList() +
listOfNotNull(try {
hardwareAddress?.joinToString(":") { "%02x".format(it) }
} catch (_: SocketException) {
null
}))
.joinToString("\n")
fun makeIpSpan(ip: String) = SpannableString(ip).apply {
if (app.hasTouch) {
val filteredIp = ip.split('%', limit = 2).first()
setSpan(CustomTabsUrlSpan("https://ipinfo.io/$filteredIp"), 0, filteredIp.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
fun makeMacSpan(mac: String) = SpannableString(mac).apply {
if (app.hasTouch) {
setSpan(CustomTabsUrlSpan("https://macvendors.co/results/$mac"), 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
fun NetworkInterface.formatAddresses() = SpannableStringBuilder().apply {
try {
hardwareAddress?.apply { appendln(makeMacSpan(asIterable().macToString())) }
} catch (_: SocketException) { }
for (address in interfaceAddresses) {
append(makeIpSpan(address.address.hostAddress))
appendln("/${address.networkPrefixLength}")
}
}.trimEnd()
fun parseNumericAddress(address: String?): InetAddress? =
Os.inet_pton(OsConstants.AF_INET, address) ?: Os.inet_pton(OsConstants.AF_INET6, address)
fun Context.launchUrl(url: String) {
if (app.hasTouch) try {
app.customTabsIntent.launchUrl(this, url.toUri())
return
} catch (_: ActivityNotFoundException) { } catch (_: SecurityException) { }
SmartSnackbar.make(url).show()
}
fun Context.stopAndUnbind(connection: ServiceConnection) {
connection.onServiceDisconnected(null)
unbindService(connection)