Skip to content

Filter results by network #788

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions composeApp/src/commonMain/composeResources/drawable/ic_network.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FF000000"
android:pathData="M220,880Q162,880 121,839Q80,798 80,740Q80,682 121,641Q162,600 220,600Q238,600 255,604.5Q272,609 287,617L440,464L440,354Q396,341 368,304.5Q340,268 340,220Q340,162 381,121Q422,80 480,80Q538,80 579,121Q620,162 620,220Q620,268 592,304.5Q564,341 520,354L520,464L674,617Q689,609 705.5,604.5Q722,600 740,600Q798,600 839,641Q880,682 880,740Q880,798 839,839Q798,880 740,880Q682,880 641,839Q600,798 600,740Q600,722 604.5,705Q609,688 617,673L480,536L343,673Q351,688 355.5,705Q360,722 360,740Q360,798 319,839Q278,880 220,880ZM740,800Q765,800 782.5,782.5Q800,765 800,740Q800,715 782.5,697.5Q765,680 740,680Q715,680 697.5,697.5Q680,715 680,740Q680,765 697.5,782.5Q715,800 740,800ZM480,280Q505,280 522.5,262.5Q540,245 540,220Q540,195 522.5,177.5Q505,160 480,160Q455,160 437.5,177.5Q420,195 420,220Q420,245 437.5,262.5Q455,280 480,280ZM220,800Q245,800 262.5,782.5Q280,765 280,740Q280,715 262.5,697.5Q245,680 220,680Q195,680 177.5,697.5Q160,715 160,740Q160,765 177.5,782.5Q195,800 220,800Z"/>
</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,20 @@

<string name="TestResults_Filters_Title">Filter Test Results</string>
<string name="TestResults_Filters_Short">Filters:</string>
<plurals name="TestResults_Filters_Count">
<item quantity="one">%1$d filter</item>
<item quantity="other">%1$d filters</item>
</plurals>
<string name="TestResults_Filter_Tests">Tests</string>
<plurals name="TestResults_Filter_Tests_Count">
<item quantity="one">%1$d test</item>
<item quantity="other">%1$d tests</item>
</plurals>
<string name="TestResults_Filter_Networks">Networks</string>
<plurals name="TestResults_Filter_Networks_Count">
<item quantity="one">%1$d network</item>
<item quantity="other">%1$d networks</item>
</plurals>
<string name="TestResults_Filter_Source">Source</string>
<string name="TestResults_Filter_Date">Date</string>
<string name="TestResults_Filter_Date_Any">Any date</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,19 @@ import org.ooni.probe.shared.today

data class ResultFilter(
val descriptors: List<Descriptor> = emptyList(),
val networks: List<NetworkModel> = emptyList(),
val taskOrigin: TaskOrigin? = null,
val dates: Date = Date.AnyDate,
val limit: Long = LIMIT,
) {
val isAll get() = this == ResultFilter()

val filterCount
get() = descriptors.size +
networks.size +
(if (taskOrigin == ResultFilter().taskOrigin) 0 else 1) +
(if (dates == ResultFilter().dates) 0 else 1)

sealed class Date(
val range: ClosedRange<LocalDate>,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class ResultRepository(
.selectAllWithNetwork(
filterByDescriptors = params.filterByDescriptors,
descriptorsKeys = params.descriptorsKeys,
filterByNetworks = params.filterByNetworks,
networkIds = params.networkIds,
filterByTaskOrigin = params.filterByTaskOrigin,
taskOrigin = params.taskOrigin,
startFrom = params.startFrom,
Expand Down Expand Up @@ -116,6 +118,8 @@ class ResultRepository(
database.resultQueries.markAllAsViewed(
filterByDescriptors = params.filterByDescriptors,
descriptorsKeys = params.descriptorsKeys,
filterByNetworks = params.filterByNetworks,
networkIds = params.networkIds,
filterByTaskOrigin = params.filterByTaskOrigin,
taskOrigin = params.taskOrigin,
startFrom = params.startFrom,
Expand Down Expand Up @@ -144,6 +148,8 @@ class ResultRepository(
database.resultQueries.deleteByFilter(
filterByDescriptors = params.filterByDescriptors,
descriptorsKeys = params.descriptorsKeys,
filterByNetworks = params.filterByNetworks,
networkIds = params.networkIds,
filterByTaskOrigin = params.filterByTaskOrigin,
taskOrigin = params.taskOrigin,
startFrom = params.startFrom,
Expand Down Expand Up @@ -257,6 +263,8 @@ class ResultRepository(
data class FilterParams(
val filterByDescriptors: Long,
val descriptorsKeys: List<String>,
val filterByNetworks: Long,
val networkIds: List<Long>,
val filterByTaskOrigin: Long,
val taskOrigin: String?,
val startFrom: Long,
Expand All @@ -268,6 +276,8 @@ class ResultRepository(
FilterParams(
filterByDescriptors = if (filter.descriptors.any()) 1 else 0,
descriptorsKeys = filter.descriptors.map { it.key },
filterByNetworks = if (filter.networks.any()) 1 else 0,
networkIds = filter.networks.mapNotNull { it.id?.value },
filterByTaskOrigin = if (filter.taskOrigin != null) 1 else 0,
taskOrigin = filter.taskOrigin?.value,
startFrom = filter.dates.range.start.toEpoch(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,7 @@ class Dependencies(
goToUpload = goToUpload,
getResults = getResults::invoke,
getDescriptors = getTestDescriptors::latest,
getNetworks = networkRepository::list,
deleteResultsByFilter = deleteResults::byFilter,
deleteResults = deleteResults::byIds,
markJustFinishedTestAsSeen = markJustFinishedTestAsSeen::invoke,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,27 @@ import ooniprobe.composeapp.generated.resources.TestResults_Filter_Date_FromOneM
import ooniprobe.composeapp.generated.resources.TestResults_Filter_Date_FromSevenDaysAgo
import ooniprobe.composeapp.generated.resources.TestResults_Filter_Date_Picker
import ooniprobe.composeapp.generated.resources.TestResults_Filter_Date_Today
import ooniprobe.composeapp.generated.resources.TestResults_Filter_Networks
import ooniprobe.composeapp.generated.resources.TestResults_Filter_Networks_Count
import ooniprobe.composeapp.generated.resources.TestResults_Filter_Source
import ooniprobe.composeapp.generated.resources.TestResults_Filter_Tests
import ooniprobe.composeapp.generated.resources.TestResults_Filter_Tests_Count
import ooniprobe.composeapp.generated.resources.TestResults_Filters_Count
import ooniprobe.composeapp.generated.resources.TestResults_Filters_Short
import ooniprobe.composeapp.generated.resources.TestResults_Filters_Title
import ooniprobe.composeapp.generated.resources.TestResults_UnknownASN
import ooniprobe.composeapp.generated.resources.ic_check
import ooniprobe.composeapp.generated.resources.ic_close
import ooniprobe.composeapp.generated.resources.ic_date_range
import ooniprobe.composeapp.generated.resources.ic_network
import ooniprobe.composeapp.generated.resources.ic_tests
import ooniprobe.composeapp.generated.resources.ic_timer
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.pluralStringResource
import org.jetbrains.compose.resources.stringResource
import org.ooni.engine.models.TaskOrigin
import org.ooni.probe.data.models.Descriptor
import org.ooni.probe.data.models.NetworkModel
import org.ooni.probe.data.models.ResultFilter
import org.ooni.probe.shared.toEpochInUTC
import org.ooni.probe.shared.toLocalDateFromUtc
Expand All @@ -97,6 +103,24 @@ fun ResultFiltersRow(
FlowRow(
modifier = Modifier.weight(1f).fillMaxWidth(),
) {
if (filter.filterCount > 2) {
InputChip(
selected = true,
onClick = onOpen,
label = {
Text(
pluralStringResource(
Res.plurals.TestResults_Filters_Count,
filter.filterCount,
filter.filterCount,
),
)
},
modifier = Modifier.padding(start = 8.dp),
)
return@FlowRow
}

if (filter.descriptors.any()) {
InputChip(
selected = true,
Expand All @@ -118,6 +142,27 @@ fun ResultFiltersRow(
)
}

if (filter.networks.any()) {
InputChip(
selected = true,
onClick = onOpen,
label = {
Text(
if (filter.networks.size == 1) {
filter.networks.first().name()
} else {
pluralStringResource(
Res.plurals.TestResults_Filter_Networks_Count,
filter.networks.size,
filter.networks.size,
)
},
)
},
modifier = Modifier.padding(start = 8.dp),
)
}

if (filter.taskOrigin != null) {
InputChip(
selected = true,
Expand All @@ -143,6 +188,7 @@ fun ResultFiltersRow(
fun ResultFiltersDialog(
initialFilter: ResultFilter,
descriptors: List<Descriptor>,
networks: List<NetworkModel>,
onSave: (ResultFilter) -> Unit,
onDismiss: () -> Unit,
) {
Expand Down Expand Up @@ -185,6 +231,7 @@ fun ResultFiltersDialog(
currentFilter,
{ currentFilter = it },
descriptors,
networks,
)

Button(
Expand All @@ -211,6 +258,7 @@ private fun ResultsFiltersDialogContent(
filter: ResultFilter,
updateFilter: (ResultFilter) -> Unit,
descriptors: List<Descriptor>,
networks: List<NetworkModel>,
) {
Column(
modifier = Modifier
Expand All @@ -219,11 +267,13 @@ private fun ResultsFiltersDialogContent(
// So content can scroll above the Save button
.padding(bottom = 64.dp),
) {
TestsFilter(filter, updateFilter, descriptors)
DateFilter(filter, updateFilter)
HorizontalDivider(Modifier.padding(vertical = 8.dp))
OriginFilter(filter, updateFilter)
HorizontalDivider(Modifier.padding(vertical = 8.dp))
DateFilter(filter, updateFilter)
TestsFilter(filter, updateFilter, descriptors)
HorizontalDivider(Modifier.padding(vertical = 8.dp))
NetworksFilter(filter, updateFilter, networks)
}
}

Expand Down Expand Up @@ -421,6 +471,47 @@ private fun ResultFilter.Date.display() =
}
}

@Composable
private fun NetworksFilter(
filter: ResultFilter,
updateFilter: (ResultFilter) -> Unit,
networks: List<NetworkModel>,
) {
FilterTitle(
stringResource(Res.string.TestResults_Filter_Networks),
painterResource(Res.drawable.ic_network),
)

FlowRow(
modifier = Modifier.padding(start = 16.dp, end = 8.dp),
) {
networks.forEach { network ->
val isSelected = filter.networks.contains(network)
ResultFilterChip(
text = network.name(),
isSelected = isSelected,
onClick = {
updateFilter(
if (isSelected) {
filter.copy(networks = filter.networks - network)
} else {
filter.copy(networks = filter.networks + network)
},
)
},
)
}
}
}

@Composable
private fun NetworkModel.name(): String {
val name = networkName?.ellipsize(20)
?: asn
?: stringResource(Res.string.TestResults_UnknownASN)
return name + countryCode?.let { " ($it)" }
}

@Composable
private fun FilterTitle(
text: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ fun ResultsScreen(
ResultFiltersDialog(
initialFilter = state.filter,
descriptors = state.descriptors,
networks = state.networks,
onSave = {
onEvent(ResultsViewModel.Event.FilterChanged(it))
showFiltersDialog = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.datetime.LocalDate
import org.ooni.probe.data.models.Descriptor
import org.ooni.probe.data.models.NetworkModel
import org.ooni.probe.data.models.ResultFilter
import org.ooni.probe.data.models.ResultListItem
import org.ooni.probe.data.models.ResultModel
Expand All @@ -25,6 +26,7 @@ class ResultsViewModel(
goToUpload: () -> Unit,
getResults: (ResultFilter) -> Flow<List<ResultListItem>>,
getDescriptors: () -> Flow<List<Descriptor>>,
getNetworks: () -> Flow<List<NetworkModel>>,
deleteResultsByFilter: suspend (ResultFilter) -> Unit,
markJustFinishedTestAsSeen: () -> Unit,
markAsViewed: suspend (ResultFilter) -> Unit,
Expand Down Expand Up @@ -67,11 +69,11 @@ class ResultsViewModel(
.launchIn(viewModelScope)

getDescriptors()
.onEach { descriptors ->
_state.update { state ->
state.copy(descriptors = descriptors)
}
}
.onEach { descriptors -> _state.update { it.copy(descriptors = descriptors) } }
.launchIn(viewModelScope)

getNetworks()
.onEach { networks -> _state.update { it.copy(networks = networks) } }
.launchIn(viewModelScope)

events
Expand Down Expand Up @@ -190,6 +192,7 @@ class ResultsViewModel(
data class State(
val filter: ResultFilter = ResultFilter(),
val descriptors: List<Descriptor> = emptyList(),
val networks: List<NetworkModel> = emptyList(),
val results: Map<LocalDate, List<SelectableItem<ResultListItem>>> = emptyMap(),
val summary: Summary? = null,
val isLoading: Boolean = true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ UPDATE Result SET is_viewed = 1
WHERE (
:filterByDescriptors = 0 OR
Result.descriptor_name IN :descriptorsKeys OR Result.descriptor_runId IN :descriptorsKeys
) AND (
:filterByNetworks = 0 OR Result.network_id IN :networkIds
) AND (
:filterByTaskOrigin = 0 OR Result.task_origin = :taskOrigin
) AND (
Expand Down Expand Up @@ -72,6 +74,8 @@ deleteByFilter:
DELETE FROM Result WHERE (
:filterByDescriptors = 0 OR
Result.descriptor_name IN :descriptorsKeys OR Result.descriptor_runId IN :descriptorsKeys
) AND (
:filterByNetworks = 0 OR Result.network_id IN :networkIds
) AND (
:filterByTaskOrigin = 0 OR Result.task_origin = :taskOrigin
) AND (
Expand Down Expand Up @@ -125,6 +129,8 @@ FROM (
WHERE (
:filterByDescriptors = 0 OR
Result.descriptor_name IN :descriptorsKeys OR Result.descriptor_runId IN :descriptorsKeys
) AND (
:filterByNetworks = 0 OR Result.network_id IN :networkIds
) AND (
:filterByTaskOrigin = 0 OR Result.task_origin = :taskOrigin
) AND (
Expand Down
Loading