Skip to content

Commit 434de99

Browse files
committed
Measurement stats
1 parent 59041cf commit 434de99

File tree

18 files changed

+251
-26
lines changed

18 files changed

+251
-26
lines changed

composeApp/src/commonMain/composeResources/values/strings-common.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@
3131
<item quantity="other">Run %1$d tests</item>
3232
</plurals>
3333

34+
<string name="Dashboard_Stats_Title">Your Measurements</string>
35+
<plurals name="Dashboard_Stats_Networks">
36+
<item quantity="one">Network</item>
37+
<item quantity="other">Networks</item>
38+
</plurals>
39+
<plurals name="Dashboard_Stats_Countries">
40+
<item quantity="one">Country</item>
41+
<item quantity="other">Countries</item>
42+
</plurals>
43+
<string name="Dashboard_Stats_Empty">Start running tests to see your statistics here.</string>
44+
3445
<string name="Dashboard_RunV2_Ooni_Title">OONI Tests</string>
3546
<string name="Dashboard_RunV2_Title">OONI Run Links</string>
3647
<string name="Dashboard_Runv2_Overview_Description">Created by %1$s on %2$s</string>
@@ -450,6 +461,9 @@
450461

451462
<string name="Common_Today">Today</string>
452463
<string name="Common_Yesterday">Yesterday</string>
464+
<string name="Common_Week">Week</string>
465+
<string name="Common_Month">Month</string>
466+
<string name="Common_Total">Total</string>
453467
<string name="Common_Ago">%1$s ago</string>
454468
<plurals name="Common_Seconds">
455469
<item quantity="one">%1$d second</item>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.ooni.probe.data.models
2+
3+
data class MeasurementStats(
4+
val measurementsToday: Long,
5+
val measurementsWeek: Long,
6+
val measurementsMonth: Long,
7+
val measurementsTotal: Long,
8+
val networks: Long,
9+
val countries: Long,
10+
)

composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/TestKeysWithResultId.kt

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import ooniprobe.composeapp.generated.resources.r720p_ext
1919
import org.jetbrains.compose.resources.StringResource
2020
import org.ooni.engine.models.TestKeys
2121
import org.ooni.engine.models.TestType
22-
import org.ooni.probe.ui.shared.format
22+
import org.ooni.probe.shared.format
23+
import org.ooni.probe.shared.withFractionalDigits
2324

2425
data class TestKeysWithResultId(
2526
val id: MeasurementModel.Id,
@@ -36,7 +37,7 @@ fun List<TestKeysWithResultId>.videoQuality() =
3637
fun List<TestKeysWithResultId>.uploadSpeed() =
3738
this.firstOrNull { TestType.Ndt.name == it.testName }?.testKeys?.let { testKey ->
3839
return@let testKey.summary?.upload?.let {
39-
val upload = setFractionalDigits(getScaledValue(it))
40+
val upload = getScaledValue(it).withFractionalDigits()
4041
val unit = getUnit(it)
4142
upload to unit
4243
}
@@ -45,7 +46,7 @@ fun List<TestKeysWithResultId>.uploadSpeed() =
4546
fun List<TestKeysWithResultId>.downloadSpeed() =
4647
this.firstOrNull { TestType.Ndt.name == it.testName }?.testKeys?.let { testKey ->
4748
return@let testKey.summary?.download?.let {
48-
val download = setFractionalDigits(getScaledValue(it))
49+
val download = getScaledValue(it).withFractionalDigits()
4950
val unit = getUnit(it)
5051
download to unit
5152
}
@@ -59,11 +60,11 @@ fun List<TestKeysWithResultId>.ping() =
5960
?.ping
6061
?.format(1)
6162

62-
fun TestKeys.getVideoQuality(extended: Boolean): StringResource {
63-
return simple?.medianBitrate?.let {
64-
return minimumBitrateForVideo(it, extended)
65-
} ?: Res.string.TestResults_NotAvailable
66-
}
63+
fun TestKeys.getVideoQuality(extended: Boolean): StringResource =
64+
simple
65+
?.medianBitrate
66+
?.let { minimumBitrateForVideo(it, extended) }
67+
?: Res.string.TestResults_NotAvailable
6768

6869
private fun minimumBitrateForVideo(
6970
videoQuality: Double,
@@ -108,8 +109,6 @@ fun getScaledValue(value: Double): Double =
108109
value / 1000 * 1000
109110
}
110111

111-
fun setFractionalDigits(value: Double): String = if (value < 10) value.format(1) else value.format(2)
112-
113112
fun getUnit(value: Double): StringResource {
114113
// We assume there is no Tbit/s (for now!)
115114
return if (value < 1000) {

composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/MeasurementRepository.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import co.touchlab.kermit.Logger
77
import kotlinx.coroutines.flow.Flow
88
import kotlinx.coroutines.flow.map
99
import kotlinx.coroutines.withContext
10+
import kotlinx.datetime.LocalDateTime
1011
import kotlinx.serialization.json.Json
1112
import org.ooni.engine.models.TestKeys
1213
import org.ooni.engine.models.TestType
@@ -94,6 +95,12 @@ class MeasurementRepository(
9495
.mapToOne(backgroundContext)
9596
.map { it.toModel() }
9697

98+
fun countFromStartTime(startTime: LocalDateTime): Flow<Long> =
99+
database.measurementQueries
100+
.countFromStartTime(startTime.toEpoch())
101+
.asFlow()
102+
.mapToOne(backgroundContext)
103+
97104
suspend fun createOrUpdate(model: MeasurementModel): MeasurementModel.Id =
98105
withContext(backgroundContext) {
99106
database.transactionWithResult {

composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/NetworkRepository.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package org.ooni.probe.data.repositories
22

33
import app.cash.sqldelight.coroutines.asFlow
44
import app.cash.sqldelight.coroutines.mapToList
5+
import app.cash.sqldelight.coroutines.mapToOne
6+
import kotlinx.coroutines.flow.Flow
57
import kotlinx.coroutines.flow.map
68
import kotlinx.coroutines.withContext
79
import org.ooni.engine.models.NetworkType
@@ -55,6 +57,18 @@ class NetworkRepository(
5557
.mapToList(backgroundContext)
5658
.map { list -> list.map { it.toModel() } }
5759

60+
fun countAsns(): Flow<Long> =
61+
database.networkQueries
62+
.countAsns()
63+
.asFlow()
64+
.mapToOne(backgroundContext)
65+
66+
fun countCountries(): Flow<Long> =
67+
database.networkQueries
68+
.countCountries()
69+
.asFlow()
70+
.mapToOne(backgroundContext)
71+
5872
suspend fun deleteWithoutResult() =
5973
withContext(backgroundContext) {
6074
database.networkQueries.deleteWithoutResult()

composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import org.ooni.probe.domain.GetFirstRun
5959
import org.ooni.probe.domain.GetLastResultOfDescriptor
6060
import org.ooni.probe.domain.GetMeasurementsNotUploaded
6161
import org.ooni.probe.domain.GetSettings
62+
import org.ooni.probe.domain.GetStats
6263
import org.ooni.probe.domain.GetStorageUsed
6364
import org.ooni.probe.domain.ObserveAndConfigureAutoRun
6465
import org.ooni.probe.domain.ObserveAndConfigureAutoUpdate
@@ -386,6 +387,13 @@ class Dependencies(
386387
cleanupLegacyDirectories = cleanupLegacyDirectories,
387388
)
388389
}
390+
private val getStats by lazy {
391+
GetStats(
392+
countMeasurementsFromStartTime = measurementRepository::countFromStartTime,
393+
countNetworkAsns = networkRepository::countAsns,
394+
countNetworkCountries = networkRepository::countCountries,
395+
)
396+
}
389397

390398
@VisibleForTesting
391399
val getTestDescriptors by lazy {
@@ -593,6 +601,7 @@ class Dependencies(
593601
dismissLastRun = dismissLastRun::invoke,
594602
getPreference = preferenceRepository::getValueByKey,
595603
setPreference = preferenceRepository::setValueByKey,
604+
getStats = getStats::invoke,
596605
batteryOptimization = batteryOptimization,
597606
)
598607

composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetSettings.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,10 @@ import org.ooni.probe.data.models.SettingsItem
6969
import org.ooni.probe.data.models.SettingsKey
7070
import org.ooni.probe.data.repositories.PreferenceRepository
7171
import org.ooni.probe.domain.results.DeleteOldResults
72+
import org.ooni.probe.shared.formatDataUsage
7273
import org.ooni.probe.ui.settings.category.SettingsDescription
7374
import org.ooni.probe.ui.settings.donate.DONATE_SETTINGS_ITEM
7475
import org.ooni.probe.ui.shared.format
75-
import org.ooni.probe.ui.shared.formatDataUsage
7676
import kotlin.time.Duration.Companion.seconds
7777

7878
class GetSettings(
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.ooni.probe.domain
2+
3+
import kotlinx.coroutines.flow.Flow
4+
import kotlinx.coroutines.flow.combine
5+
import kotlinx.datetime.DateTimeUnit
6+
import kotlinx.datetime.LocalDate
7+
import kotlinx.datetime.LocalDateTime
8+
import kotlinx.datetime.isoDayNumber
9+
import kotlinx.datetime.minus
10+
import org.ooni.probe.data.models.MeasurementStats
11+
import org.ooni.probe.shared.toDateTime
12+
import org.ooni.probe.shared.today
13+
14+
class GetStats(
15+
private val countMeasurementsFromStartTime: (LocalDateTime) -> Flow<Long>,
16+
private val countNetworkAsns: () -> Flow<Long>,
17+
private val countNetworkCountries: () -> Flow<Long>,
18+
) {
19+
operator fun invoke(): Flow<MeasurementStats> {
20+
val today = LocalDate.today()
21+
val startOfWeek = today.minus(today.dayOfWeek.isoDayNumber - 1, DateTimeUnit.DAY)
22+
val startOfMonth = today.minus(today.day - 1, DateTimeUnit.DAY)
23+
val startOfTotal = LocalDate.fromEpochDays(0)
24+
return combine<Long, MeasurementStats>(
25+
countMeasurementsFromStartTime(today.toDateTime()),
26+
countMeasurementsFromStartTime(startOfWeek.toDateTime()),
27+
countMeasurementsFromStartTime(startOfMonth.toDateTime()),
28+
countMeasurementsFromStartTime(startOfTotal.toDateTime()),
29+
countNetworkAsns(),
30+
countNetworkCountries(),
31+
) { values ->
32+
MeasurementStats(
33+
values[0],
34+
values[1],
35+
values[2],
36+
1230, // values[3],
37+
values[4],
38+
values[5],
39+
)
40+
}
41+
}
42+
}

composeApp/src/commonMain/kotlin/org/ooni/probe/shared/DateTimeExt.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ fun LocalDate.toEpoch() = atStartOfDayIn(TimeZone.currentSystemDefault()).toEpoc
1616

1717
fun LocalDate.toEpochInUTC() = atStartOfDayIn(TimeZone.UTC).toEpochMilliseconds()
1818

19+
fun LocalDate.toDateTime() = atStartOfDayIn(TimeZone.currentSystemDefault()).toLocalDateTime()
20+
1921
fun Long.toLocalDateTime() = Instant.fromEpochMilliseconds(this).toLocalDateTime()
2022

2123
fun Long.toLocalDateFromUtc() = Instant.fromEpochMilliseconds(this).toLocalDateTime(TimeZone.UTC).date

composeApp/src/commonMain/kotlin/org/ooni/probe/ui/shared/DataUsageFormats.kt renamed to composeApp/src/commonMain/kotlin/org/ooni/probe/shared/NumberExt.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package org.ooni.probe.ui.shared
1+
package org.ooni.probe.shared
22

33
import kotlin.math.abs
44
import kotlin.math.log10
@@ -16,3 +16,12 @@ fun Double.format(decimalChars: Int = 2): String {
1616
val decimalValue = abs((this - absoluteValue) * 10.0.pow(decimalChars)).toInt()
1717
return if (decimalValue == 0) absoluteValue.toString() else "$absoluteValue.$decimalValue"
1818
}
19+
20+
fun Long.largeNumberShort(): String {
21+
if (this <= 0) return "0"
22+
val units = arrayOf("", "K", "M")
23+
val digitGroups = (log10(this.toDouble()) / log10(1000.0)).toInt()
24+
return (this / 1000.0.pow(digitGroups.toDouble())).withFractionalDigits() + units[digitGroups]
25+
}
26+
27+
fun Double.withFractionalDigits(): String = if (this < 10) format(2) else format(1)

0 commit comments

Comments
 (0)