@@ -15,6 +15,7 @@ import com.bringyour.sdk.ContractViewController
1515import com.bringyour.sdk.DeviceLocal
1616import com.bringyour.sdk.Sub
1717import com.bringyour.sdk.ThroughputPointList
18+ import com.bringyour.sdk.TransportDistribution
1819import dagger.hilt.android.lifecycle.HiltViewModel
1920import kotlinx.coroutines.launch
2021import javax.inject.Inject
@@ -58,6 +59,125 @@ enum class ThroughputRoute {
5859 }
5960}
6061
62+ /* *
63+ * One transport's slice of the window's remote traffic, ready to render as a
64+ * segment of the transport bar plus its legend entry. A mirror of the sdk's
65+ * `TransportShare`: every render value (share, cumulative boundary, whole
66+ * percent, used, enabled) is computed by the sdk view controller so the math
67+ * is shared and tested once for every platform.
68+ */
69+ data class TransportShareUi (
70+ val transportType : TransportTypeUi ,
71+ val egressBytes : Long = 0 ,
72+ val ingressBytes : Long = 0 ,
73+ val egressPackets : Long = 0 ,
74+ val ingressPackets : Long = 0 ,
75+ /* *
76+ * fraction of the window's remote bytes, 0..1; 0 while idle
77+ */
78+ val share : Double = 0.0 ,
79+ /* *
80+ * the right edge of the segment as a fraction of the bar width: the
81+ * cumulative share through this transport in stable order. Rendering every
82+ * segment from its neighbours' boundaries tiles exactly 100% of the bar
83+ */
84+ val boundary : Double = 0.0 ,
85+ /* *
86+ * whole percent for the legend; the used percents sum to exactly 100.
87+ * A sliver can round to 0 while still used
88+ */
89+ val percent : Int = 0 ,
90+ /* *
91+ * carried traffic in the window: draws a segment and a legend entry
92+ */
93+ val used : Boolean = false ,
94+ /* *
95+ * enabled by the transport settings; unused footer entry when idle
96+ */
97+ val enabled : Boolean = false ,
98+ ) {
99+ val byteCount: Long
100+ get() = egressBytes + ingressBytes
101+ }
102+
103+ /* *
104+ * The window's remote traffic partitioned by the transport that carried it,
105+ * in the sdk's stable order with every transport present. Follows the same
106+ * window as the throughput points, so it drains to inactive as traffic ages
107+ * out. A mirror of the sdk's `TransportDistribution`.
108+ */
109+ data class TransportDistributionUi (
110+ /* *
111+ * stable order: h3, h1, dns, dnspump, p2p, unknown
112+ */
113+ val shares : List <TransportShareUi >,
114+ val byteCount : Long ,
115+ /* *
116+ * whether any transport carried traffic in the window
117+ */
118+ val active : Boolean ,
119+ ) {
120+ /* *
121+ * the segment boundaries in stable order, the vector the bar animates
122+ */
123+ val boundaries: List <Float >
124+ get() = shares.map { it.boundary.toFloat() }
125+
126+ /* *
127+ * the transports with traffic in the window, stable order
128+ */
129+ val used: List <TransportShareUi >
130+ get() = shares.filter { it.used }
131+
132+ /* *
133+ * the enabled transports without traffic in the window, stable order
134+ */
135+ val unused: List <TransportShareUi >
136+ get() = shares.filter { it.enabled && ! it.used }
137+
138+ companion object {
139+ val Empty = TransportDistributionUi (shares = listOf (), byteCount = 0 , active = false )
140+
141+ /* *
142+ * maps the sdk distribution, dropping transport types this app does
143+ * not know (a newer sdk vocabulary)
144+ */
145+ fun fromSdk (distribution : TransportDistribution ? ): TransportDistributionUi {
146+ if (distribution == null ) {
147+ return Empty
148+ }
149+ val shares = mutableListOf<TransportShareUi >()
150+ val list = distribution.shares
151+ if (list != null ) {
152+ val n = list.len()
153+ for (i in 0 until n) {
154+ val share = list.get(i) ? : continue
155+ val transportType = TransportTypeUi .fromRawValue(share.transportType) ? : continue
156+ shares.add(
157+ TransportShareUi (
158+ transportType = transportType,
159+ egressBytes = share.egressByteCount,
160+ ingressBytes = share.ingressByteCount,
161+ egressPackets = share.egressPacketCount,
162+ ingressPackets = share.ingressPacketCount,
163+ share = share.share,
164+ boundary = share.boundary,
165+ percent = share.percent.toInt(),
166+ used = share.used,
167+ enabled = share.enabled,
168+ )
169+ )
170+ }
171+ }
172+ return TransportDistributionUi (
173+ shares = shares,
174+ byteCount = distribution.byteCount,
175+ active = distribution.active,
176+ )
177+ }
178+ }
179+ }
180+
61181/* *
62182 * Wraps the sdk contract view controller and publishes the live
63183 * client and provider throughput series
@@ -84,6 +204,16 @@ class ThroughputViewModel @Inject constructor(
84204 var providerPoints by mutableStateOf<List <ThroughputPointUi >>(listOf ())
85205 private set
86206
207+ /* *
208+ * the remote traffic of the window partitioned by transport, ready to
209+ * render (see `TransportDistributionUi`)
210+ */
211+ var clientTransportDistribution by mutableStateOf(TransportDistributionUi .Empty )
212+ private set
213+
214+ var providerTransportDistribution by mutableStateOf(TransportDistributionUi .Empty )
215+ private set
216+
87217 /* *
88218 * false when the device has no provider (providing disabled)
89219 */
@@ -121,6 +251,8 @@ class ThroughputViewModel @Inject constructor(
121251 private fun setupDevice (device : DeviceLocal ? ) {
122252 clientPoints = listOf ()
123253 providerPoints = listOf ()
254+ clientTransportDistribution = TransportDistributionUi .Empty
255+ providerTransportDistribution = TransportDistributionUi .Empty
124256 hasProviderStats = false
125257 controllerOwner.setDevice(device)
126258 }
@@ -135,6 +267,20 @@ class ThroughputViewModel @Inject constructor(
135267 update()
136268 }
137269 })
270+ // the distribution's enabled flags follow the device transport settings
271+ // (the view controller caches them from these same listeners). Re-read
272+ // the distributions on a policy change so the unused footer is right
273+ // while the window is idle and no throughput tick is due.
274+ subs.add(device.addTransportSettingsChangeListener {
275+ viewModelScope.launch {
276+ updateTransportDistributions()
277+ }
278+ })
279+ subs.add(device.addProviderTransportSettingsChangeListener {
280+ viewModelScope.launch {
281+ updateTransportDistributions()
282+ }
283+ })
138284 vc.start()
139285 update()
140286 return vc
@@ -155,9 +301,28 @@ class ThroughputViewModel @Inject constructor(
155301 val vc = contractVc ? : return
156302 clientPoints = mapPoints(vc.throughputPoints)
157303 providerPoints = mapPoints(vc.providerThroughputPoints)
304+ updateTransportDistributions()
158305 hasProviderStats = vc.providerPacketStats != null
159306 }
160307
308+ /* *
309+ * Publishes the window transport distributions from the same view
310+ * controller snapshot as the points. The distribution is inactive while
311+ * the window is idle; only a real change is published so an idle tick
312+ * doesn't retrigger the bar.
313+ */
314+ private fun updateTransportDistributions () {
315+ val vc = contractVc ? : return
316+ val clientDistribution = TransportDistributionUi .fromSdk(vc.transportDistribution)
317+ if (clientDistribution != clientTransportDistribution) {
318+ clientTransportDistribution = clientDistribution
319+ }
320+ val providerDistribution = TransportDistributionUi .fromSdk(vc.providerTransportDistribution)
321+ if (providerDistribution != providerTransportDistribution) {
322+ providerTransportDistribution = providerDistribution
323+ }
324+ }
325+
161326 private fun mapPoints (list : ThroughputPointList ? ): List <ThroughputPointUi > {
162327 if (list == null ) {
163328 return listOf ()
0 commit comments