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:
@@ -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)
|
||||
}
|
||||
14
mobile/src/main/java/be/mygod/vpnhotspot/util/MainScope.kt
Normal file
14
mobile/src/main/java/be/mygod/vpnhotspot/util/MainScope.kt
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user