Skip to content
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

Feat: Optionally group results #613

Merged
merged 9 commits into from
Dec 5, 2023
Prev Previous commit
Next Next commit
Add grouping to items with duplicates
  • Loading branch information
aanorbel committed Sep 3, 2023
commit 16144a9a38de3303d9219401a2682cfd34db2321
Original file line number Diff line number Diff line change
@@ -10,17 +10,15 @@ import android.view.View
import androidx.core.app.ActivityCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import localhost.toolkit.app.fragment.ConfirmDialogFragment
import localhost.toolkit.app.fragment.ConfirmDialogFragment.OnConfirmedListener
import localhost.toolkit.widget.recyclerview.HeterogeneousRecyclerAdapter
import localhost.toolkit.widget.recyclerview.HeterogeneousRecyclerItem
import org.openobservatory.ooniprobe.R
import org.openobservatory.ooniprobe.adapters.MeasurementGroup
import org.openobservatory.ooniprobe.adapters.ResultDetailExpandableListAdapter
import org.openobservatory.ooniprobe.common.PreferenceManager
import org.openobservatory.ooniprobe.common.ResubmitTask
import org.openobservatory.ooniprobe.databinding.ActivityResultDetailBinding
@@ -30,8 +28,6 @@ import org.openobservatory.ooniprobe.fragment.resultHeader.ResultHeaderDetailFra
import org.openobservatory.ooniprobe.fragment.resultHeader.ResultHeaderMiddleboxFragment
import org.openobservatory.ooniprobe.fragment.resultHeader.ResultHeaderPerformanceFragment
import org.openobservatory.ooniprobe.fragment.resultHeader.ResultHeaderTBAFragment
import org.openobservatory.ooniprobe.item.MeasurementItem
import org.openobservatory.ooniprobe.item.MeasurementPerfItem
import org.openobservatory.ooniprobe.model.database.Measurement
import org.openobservatory.ooniprobe.model.database.Network
import org.openobservatory.ooniprobe.model.database.Result
@@ -52,10 +48,9 @@ class ResultDetailActivity : AbstractActivity(), View.OnClickListener, OnConfirm
}
}

private var items: ArrayList<HeterogeneousRecyclerItem<*, *>>? = null
private var adapter: HeterogeneousRecyclerAdapter<HeterogeneousRecyclerItem<*, *>>? = null
private lateinit var result: Result
private lateinit var snackbar: Snackbar
private lateinit var binding: ActivityResultDetailBinding

@Inject
lateinit var getTestSuite: GetTestSuite
@@ -82,7 +77,7 @@ class ResultDetailActivity : AbstractActivity(), View.OnClickListener, OnConfirm
else -> {
result = iResult
setTheme(result.testSuite.themeLight)
val binding = ActivityResultDetailBinding.inflate(layoutInflater)
binding = ActivityResultDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
supportActionBar?.let { actionBar ->
@@ -98,28 +93,17 @@ class ResultDetailActivity : AbstractActivity(), View.OnClickListener, OnConfirm
is_viewed = true
save()
}
items = ArrayList()
adapter = HeterogeneousRecyclerAdapter(this, items)
binding.recyclerView.apply {
val layoutManager = LinearLayoutManager(this@ResultDetailActivity)
setLayoutManager(layoutManager)
addItemDecoration(DividerItemDecoration(this@ResultDetailActivity, layoutManager.orientation))
setAdapter(this@ResultDetailActivity.adapter)
}

snackbar = Snackbar.make(
binding.coordinatorLayout,
R.string.Snackbar_ResultsSomeNotUploaded_Text,
Snackbar.LENGTH_INDEFINITE
).setAction(R.string.Snackbar_ResultsSomeNotUploaded_UploadAll) { runAsyncTask() }
load()
}
}
}

override fun onResume() {
super.onResume()
load()
}

override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.rerun, menu)
return super.onCreateOptionsMenu(menu)
@@ -166,18 +150,28 @@ class ResultDetailActivity : AbstractActivity(), View.OnClickListener, OnConfirm

private fun load() {
result = getResults[result.id]
val isPerf = result.test_group_name == PerformanceSuite.NAME
items?.clear()
result.getMeasurementsSorted().let { measurements ->
for (measurement in measurements) items!!.add(
if (isPerf && !measurement.is_failed) MeasurementPerfItem(
measurement,
this
) else MeasurementItem(measurement, this)

val groupedItemList = mutableListOf<Any>()
val groupedItems = result.getMeasurementsSorted().groupBy { it.test_name }
for ((_, itemList) in groupedItems) {
if (itemList.size == 1) {
groupedItemList.add(itemList.first())
} else {
if (groupedItems.size == 1) {
groupedItemList.addAll(itemList)
} else {
groupedItemList.add(MeasurementGroup(title = itemList.first().test.name, measurements = itemList))
}

}
}

binding.recyclerView.apply {
setAdapter(
ResultDetailExpandableListAdapter(groupedItemList, this@ResultDetailActivity)
)
}

adapter?.notifyTypesChanged()
if (Measurement.hasReport(
this,
Measurement.selectUploadableWithResultId(result.id)
@@ -208,7 +202,7 @@ class ResultDetailActivity : AbstractActivity(), View.OnClickListener, OnConfirm
}
}

private class ResubmitAsyncTask internal constructor(activity: ResultDetailActivity) :
private class ResubmitAsyncTask(activity: ResultDetailActivity) :
ResubmitTask<ResultDetailActivity?>(activity, activity.preferenceManager.proxyURL) {
override fun onPostExecute(result: Boolean) {
super.onPostExecute(result)
@@ -233,7 +227,7 @@ class ResultDetailActivity : AbstractActivity(), View.OnClickListener, OnConfirm
}
}

private inner class ResultHeaderAdapter internal constructor(fa: FragmentActivity) : FragmentStateAdapter(fa) {
private inner class ResultHeaderAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
override fun createFragment(position: Int): Fragment {
when (result.test_group_name) {
ExperimentalSuite.NAME -> {
@@ -301,4 +295,4 @@ class ResultDetailActivity : AbstractActivity(), View.OnClickListener, OnConfirm
}
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package org.openobservatory.ooniprobe.adapters

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseExpandableListAdapter
import android.widget.TextView
import org.openobservatory.ooniprobe.R
import org.openobservatory.ooniprobe.model.database.Measurement
import org.openobservatory.ooniprobe.test.test.*

data class MeasurementGroup(val title: String, val measurements: List<Measurement>)


class ResultDetailExpandableListAdapter(
private val items: List<Any>,
private val onClickListener: View.OnClickListener
) : BaseExpandableListAdapter() {

override fun getGroupCount() = items.size

override fun getChildrenCount(listPosition: Int): Int = items[listPosition].let {
when (it) {
is MeasurementGroup -> it.measurements.size

else -> 0
}
}

override fun getGroup(listPosition: Int): Any {
return when {
items[listPosition] is MeasurementGroup -> (items[listPosition] as MeasurementGroup).title
else -> items[listPosition]
}
}

override fun getChild(listPosition: Int, expandedListPosition: Int): Measurement? = when {
items[listPosition] is MeasurementGroup -> (items[listPosition] as MeasurementGroup).measurements[expandedListPosition]
else -> null
}

override fun getGroupId(listPosition: Int): Long = listPosition.toLong()

override fun getChildId(listPosition: Int, expandedListPosition: Int): Long = expandedListPosition.toLong()

override fun hasStableIds(): Boolean = false

override fun isChildSelectable(listPosition: Int, expandedListPosition: Int): Boolean = true

override fun getChildView(
groupPosition: Int,
childPosition: Int,
isLastChild: Boolean,
convertView: View?,
parent: ViewGroup
): View {
val measurement = getChild(groupPosition, childPosition)

val root = convertView ?: LayoutInflater.from(parent.context)
.inflate(R.layout.item_measurement, parent, false)

measurement?.let {
bindMeasurement(it, root)
}

return root
}


override fun getGroupView(
groupPosition: Int,
isExpanded: Boolean,
convertView: View?,
parent: ViewGroup
): View {
val groupItem = getGroup(groupPosition)

val root = convertView ?: LayoutInflater.from(parent.context)
.inflate(R.layout.item_measurement, parent, false)

when (groupItem) {
is Measurement -> bindMeasurement(groupItem, root)

else -> root.findViewById<TextView>(R.id.text).apply {
text = groupItem.toString()
setCompoundDrawablesRelativeWithIntrinsicBounds(
0,
0,
if (isExpanded) R.drawable.advanced else R.drawable.chevron_right,
0
)
}
}
return root
}

private fun bindMeasurement(
measurement: Measurement,
view: View
) {
view.tag = measurement
view.setOnClickListener(onClickListener)
view.findViewById<TextView>(R.id.text).also { textView ->

val test: AbstractTest = measurement.getTest()

val endDrawable: Int = when {
measurement.is_failed -> R.drawable.error_24dp
measurement.is_anomaly && measurement.isUploaded -> R.drawable.exclamation_24dp
measurement.is_anomaly -> R.drawable.exclamation_cloudoff
measurement.isUploaded -> R.drawable.tick_green_24dp
else -> R.drawable.tick_green_cloudoff
}

if (measurement.test_name == WebConnectivity.NAME) {
if (measurement.url != null) {
textView.text = measurement.url.url
textView.setCompoundDrawablesRelativeWithIntrinsicBounds(
measurement.url.getCategoryIcon(textView.context),
0,
endDrawable,
0
)
}
} else {
when (measurement.getTest().labelResId) {
R.string.Test_Experimental_Fullname -> textView.text = measurement.getTest().name
else -> textView.setText(test.labelResId)
}
textView.setCompoundDrawablesRelativeWithIntrinsicBounds(test.iconResId, 0, endDrawable, 0)
}
}
if (arrayListOf(
Dash.NAME,
Ndt.NAME,
HttpHeaderFieldManipulation.NAME,
HttpInvalidRequestLine.NAME
).contains(measurement.test_name)
) {
val c: Context = view.context
view.findViewById<View>(R.id.pref_group).visibility = View.VISIBLE
val data1: TextView = view.findViewById(R.id.data1)
val data2: TextView = view.findViewById(R.id.data2)
view.findViewById<TextView>(R.id.text).setCompoundDrawablesRelativeWithIntrinsicBounds(
0,
0,
if (measurement.is_failed || measurement.isUploaded) 0 else R.drawable.cloudoff,
0
)
when (measurement.test_name) {
Dash.NAME -> {
data1.apply {
setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.video_quality, 0, 0, 0)
setText(measurement.getTestKeys().getVideoQuality(true))
}
data2.visibility = View.GONE
}

Ndt.NAME -> {
data1.apply {
setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.download_black, 0, 0, 0)
text = c.getString(
R.string.twoParam,
measurement.getTestKeys().getDownload(c),
c.getString(measurement.getTestKeys().getDownloadUnit())
)
}
data2.apply {
visibility = View.VISIBLE
setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.upload_black, 0, 0, 0)
text = c.getString(
R.string.twoParam,
measurement.getTestKeys().getUpload(c),
c.getString(measurement.getTestKeys().getUploadUnit())
)
}
}

HttpHeaderFieldManipulation.NAME, HttpInvalidRequestLine.NAME -> {
data1.apply {
setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.test_middle_boxes_small, 0, 0, 0)
(if (measurement.is_anomaly) c.getString(R.string.TestResults_Overview_MiddleBoxes_Found) else c.getString(
R.string.TestResults_Overview_MiddleBoxes_NotFound
)).also { text = it }
}
data2.visibility = View.GONE
}
}
}
}

}
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@

import localhost.toolkit.widget.recyclerview.HeterogeneousRecyclerItem;

@Deprecated
public class MeasurementItem extends HeterogeneousRecyclerItem<Measurement, MeasurementItem.ViewHolder> {
private final View.OnClickListener onClickListener;

Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@
import org.openobservatory.ooniprobe.test.test.Ndt;

import localhost.toolkit.widget.recyclerview.HeterogeneousRecyclerItem;

@Deprecated
public class MeasurementPerfItem extends HeterogeneousRecyclerItem<Measurement, MeasurementPerfItem.ViewHolder> {
private final View.OnClickListener onClickListener;

8 changes: 3 additions & 5 deletions app/src/main/res/layout/activity_result_detail.xml
Original file line number Diff line number Diff line change
@@ -14,8 +14,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:contentScrim="?attr/colorPrimary"
app:titleEnabled="false"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
app:titleEnabled="false">

<LinearLayout
android:layout_width="match_parent"
@@ -58,12 +57,11 @@
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>

<androidx.recyclerview.widget.RecyclerView
<ExpandableListView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="66dp"
android:groupIndicator="@android:color/transparent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />

<View
Loading
Loading