Rewrite SpanFormatter for more kotlin
This commit is contained in:
@@ -37,7 +37,7 @@ import be.mygod.vpnhotspot.net.monitor.TrafficRecorder
|
|||||||
import be.mygod.vpnhotspot.room.AppDatabase
|
import be.mygod.vpnhotspot.room.AppDatabase
|
||||||
import be.mygod.vpnhotspot.room.ClientStats
|
import be.mygod.vpnhotspot.room.ClientStats
|
||||||
import be.mygod.vpnhotspot.room.TrafficRecord
|
import be.mygod.vpnhotspot.room.TrafficRecord
|
||||||
import be.mygod.vpnhotspot.util.SpanFormatter
|
import be.mygod.vpnhotspot.util.format
|
||||||
import be.mygod.vpnhotspot.util.showAllowingStateLoss
|
import be.mygod.vpnhotspot.util.showAllowingStateLoss
|
||||||
import be.mygod.vpnhotspot.util.toPluralInt
|
import be.mygod.vpnhotspot.util.toPluralInt
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
@@ -82,10 +82,11 @@ class ClientsFragment : Fragment() {
|
|||||||
data class StatsArg(val title: CharSequence, val stats: ClientStats) : Parcelable
|
data class StatsArg(val title: CharSequence, val stats: ClientStats) : Parcelable
|
||||||
class StatsDialogFragment : AlertDialogFragment<StatsArg, Empty>() {
|
class StatsDialogFragment : AlertDialogFragment<StatsArg, Empty>() {
|
||||||
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
|
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
|
||||||
setTitle(SpanFormatter.format(getText(R.string.clients_stats_title), arg.title))
|
|
||||||
val context = context
|
val context = context
|
||||||
val resources = resources
|
val resources = resources
|
||||||
val format = NumberFormat.getIntegerInstance(resources.configuration.locale)
|
val locale = resources.configuration.locale
|
||||||
|
setTitle(getText(R.string.clients_stats_title).format(locale, arg.title))
|
||||||
|
val format = NumberFormat.getIntegerInstance(locale)
|
||||||
setMessage("%s\n%s\n%s".format(
|
setMessage("%s\n%s\n%s".format(
|
||||||
resources.getQuantityString(R.plurals.clients_stats_message_1, arg.stats.count.toPluralInt(),
|
resources.getQuantityString(R.plurals.clients_stats_message_1, arg.stats.count.toPluralInt(),
|
||||||
format.format(arg.stats.count), DateUtils.formatDateTime(context, arg.stats.timestamp,
|
format.format(arg.stats.count), DateUtils.formatDateTime(context, arg.stats.timestamp,
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import androidx.preference.Preference
|
|||||||
import be.mygod.vpnhotspot.R
|
import be.mygod.vpnhotspot.R
|
||||||
import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor
|
import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor
|
||||||
import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
|
import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
|
||||||
import be.mygod.vpnhotspot.util.SpanFormatter
|
|
||||||
import be.mygod.vpnhotspot.util.allRoutes
|
import be.mygod.vpnhotspot.util.allRoutes
|
||||||
|
import be.mygod.vpnhotspot.util.format
|
||||||
import be.mygod.vpnhotspot.util.parseNumericAddress
|
import be.mygod.vpnhotspot.util.parseNumericAddress
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(co
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onUpdate() = (context as LifecycleOwner).lifecycleScope.launchWhenStarted {
|
private fun onUpdate() = (context as LifecycleOwner).lifecycleScope.launchWhenStarted {
|
||||||
summary = SpanFormatter.format(context.getText(R.string.settings_service_upstream_monitor_summary),
|
summary = context.getText(R.string.settings_service_upstream_monitor_summary).format(
|
||||||
primary.charSequence, fallback.charSequence)
|
context.resources.configuration.locale, primary.charSequence, fallback.charSequence)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
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 cookedArg = when (val typeTerm = m.group(3)) {
|
|
||||||
"%" -> "%"
|
|
||||||
"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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,9 +12,7 @@ import android.os.RemoteException
|
|||||||
import android.system.ErrnoException
|
import android.system.ErrnoException
|
||||||
import android.system.Os
|
import android.system.Os
|
||||||
import android.system.OsConstants
|
import android.system.OsConstants
|
||||||
import android.text.Spannable
|
import android.text.*
|
||||||
import android.text.SpannableString
|
|
||||||
import android.text.SpannableStringBuilder
|
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
@@ -39,6 +37,7 @@ import java.lang.reflect.Method
|
|||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.NetworkInterface
|
import java.net.NetworkInterface
|
||||||
import java.net.SocketException
|
import java.net.SocketException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
tailrec fun Throwable.getRootCause(): Throwable {
|
tailrec fun Throwable.getRootCause(): Throwable {
|
||||||
if (this is InvocationTargetException || this is RemoteException) return (cause ?: return this).getRootCause()
|
if (this is InvocationTargetException || this is RemoteException) return (cause ?: return this).getRootCause()
|
||||||
@@ -80,6 +79,53 @@ fun setVisibility(view: View, value: Boolean) {
|
|||||||
view.isVisible = value
|
view.isVisible = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* See also: https://github.com/george-steel/android-utils/blob/289aff11e53593a55d780f9f5986e49343a79e55/src/org/oshkimaadziig/george/androidutils/SpanFormatter.java
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
* @author George T. Steel
|
||||||
|
*/
|
||||||
|
fun CharSequence.format(locale: Locale, vararg args: Any) = SpannableStringBuilder(this).apply {
|
||||||
|
var i = 0
|
||||||
|
var argAt = -1
|
||||||
|
while (i < length) {
|
||||||
|
val m = formatSequence.matcher(this)
|
||||||
|
if (!m.find(i)) break
|
||||||
|
i = m.start()
|
||||||
|
val exprEnd = m.end()
|
||||||
|
val argTerm = m.group(1)!!
|
||||||
|
val modTerm = m.group(2)
|
||||||
|
val cookedArg = when (val typeTerm = m.group(3)) {
|
||||||
|
"%" -> "%"
|
||||||
|
"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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
replace(i, exprEnd, cookedArg)
|
||||||
|
i += cookedArg.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun makeIpSpan(ip: InetAddress) = ip.hostAddress.let {
|
fun makeIpSpan(ip: InetAddress) = ip.hostAddress.let {
|
||||||
// exclude all bogon IP addresses supported by Android APIs
|
// exclude all bogon IP addresses supported by Android APIs
|
||||||
if (!app.hasTouch || ip.isMulticastAddress || ip.isAnyLocalAddress || ip.isLoopbackAddress ||
|
if (!app.hasTouch || ip.isMulticastAddress || ip.isAnyLocalAddress || ip.isLoopbackAddress ||
|
||||||
|
|||||||
Reference in New Issue
Block a user