diff --git a/app/src/main/java/org/openobservatory/ooniprobe/activity/OverviewActivity.java b/app/src/main/java/org/openobservatory/ooniprobe/activity/OverviewActivity.java index ad1e5f07f..81663fa64 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/activity/OverviewActivity.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/OverviewActivity.java @@ -34,6 +34,7 @@ import org.openobservatory.ooniprobe.activity.customwebsites.CustomWebsiteActivity; import org.openobservatory.ooniprobe.activity.overview.OverviewTestsExpandableListViewAdapter; import org.openobservatory.ooniprobe.activity.overview.OverviewViewModel; +import org.openobservatory.ooniprobe.activity.overview.RevisionsFragment; import org.openobservatory.ooniprobe.activity.reviewdescriptorupdates.ReviewDescriptorUpdatesActivity; import org.openobservatory.ooniprobe.common.AbstractDescriptor; import org.openobservatory.ooniprobe.common.OONITests; @@ -48,7 +49,6 @@ import java.io.Serializable; import java.text.SimpleDateFormat; -import java.util.Date; import java.util.Locale; import java.util.Objects; @@ -163,7 +163,22 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { if (descriptor instanceof InstalledDescriptor) { binding.uninstallLink.setVisibility(View.VISIBLE); binding.automaticUpdatesContainer.setVisibility(View.VISIBLE); - binding.automaticUpdatesSwitch.setChecked(((InstalledDescriptor) descriptor).getTestDescriptor().isAutoUpdate()); + InstalledDescriptor installedDescriptor = (InstalledDescriptor) descriptor; + binding.automaticUpdatesSwitch.setChecked(installedDescriptor.getTestDescriptor().isAutoUpdate()); + + try { + if (Integer.parseInt(installedDescriptor.getTestDescriptor().getRevision()) > 1) { + getSupportFragmentManager().beginTransaction().add( + binding.revisionsContainer.getId(), + RevisionsFragment.newInstance( + installedDescriptor.getDescriptor().getRunId(), + installedDescriptor.getDescriptor().getPreviousRevision() + ) + ).commit(); + } + } catch (Exception e) { + e.printStackTrace(); + } } else { binding.uninstallLink.setVisibility(View.GONE); /** diff --git a/app/src/main/java/org/openobservatory/ooniprobe/activity/adddescriptor/AddDescriptorActivity.kt b/app/src/main/java/org/openobservatory/ooniprobe/activity/adddescriptor/AddDescriptorActivity.kt index 450b3ae35..2159b9d1d 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/activity/adddescriptor/AddDescriptorActivity.kt +++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/adddescriptor/AddDescriptorActivity.kt @@ -153,6 +153,7 @@ class AddDescriptorActivity : AbstractActivity() { } binding.btnCancel.setOnClickListener { + Toast.makeText(this@AddDescriptorActivity, "Link installation cancelled", Toast.LENGTH_LONG).show() finish() } diff --git a/app/src/main/java/org/openobservatory/ooniprobe/activity/overview/RevisionsView.kt b/app/src/main/java/org/openobservatory/ooniprobe/activity/overview/RevisionsView.kt new file mode 100644 index 000000000..09a37fcf2 --- /dev/null +++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/overview/RevisionsView.kt @@ -0,0 +1,139 @@ +package org.openobservatory.ooniprobe.activity.overview + +import android.content.Intent +import android.graphics.Paint +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.gson.Gson +import org.openobservatory.engine.OONIRunRevisions +import org.openobservatory.ooniprobe.R +import org.openobservatory.ooniprobe.databinding.FragmentRevisionsBinding +import org.openobservatory.ooniprobe.databinding.ItemTextBinding + + +class RevisionsFragment : Fragment() { + + companion object { + + const val ARG_PREVIOUS_REVISIONS = "previous-revisions" + const val ARG_OONI_RUN_LINK_ID = "oonirun-link-id" + + /** + * Use this factory method to create a new instance of + * this fragment using the provided parameters. + * + * @param previousRevisions Previous revisions in JSON format. + * @return A new instance of fragment RevisionsFragment. + */ + @JvmStatic + fun newInstance(runId: Long, previousRevisions: String) = + RevisionsFragment().apply { + arguments = Bundle().apply { + putString(ARG_PREVIOUS_REVISIONS, previousRevisions) + putLong(ARG_OONI_RUN_LINK_ID, runId) + } + } + } + + private var revisions: OONIRunRevisions? = null + private var runId: Long = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + arguments?.let { + revisions = + Gson().fromJson(it.getString(ARG_PREVIOUS_REVISIONS), OONIRunRevisions::class.java) + runId = it.getLong(ARG_OONI_RUN_LINK_ID) + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val binding = FragmentRevisionsBinding.inflate(inflater, container, false) + + with(binding.list) { + layoutManager = LinearLayoutManager(context) + adapter = revisions?.revisions?.let { + RevisionsRecyclerViewAdapter(it, object : OnItemClickListener { + override fun onItemClick(position: Int) { + startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse( + "https://run.test.ooni.org/revisions/%s?revision=%s".format( + runId, + it[position] + ) + ) + ) + ) + } + }) + } + } + + return binding.root + } + +} + +/** + * Interface for handling item clicks in the RecyclerView. + */ +interface OnItemClickListener { + /** + * Called when an item in the RecyclerView is clicked. + * + * @param position The position of the clicked item. + */ + fun onItemClick(position: Int) +} + +/** + * RecyclerView adapter for displaying a list of revisions. + * + * @param values The list of revisions to display. + * @param onClickListener The click listener for handling item clicks. + */ +class RevisionsRecyclerViewAdapter( + private val values: List, + private val onClickListener: OnItemClickListener, +) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + ItemTextBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = values[position] + holder.binding.root.setPadding(0, 10, 0, 10) + holder.binding.textView.apply { + text = "#$item" + setTextColor( + ContextCompat.getColor(holder.binding.root.context, R.color.color_blue6) + ) + paintFlags = paintFlags or Paint.UNDERLINE_TEXT_FLAG + setOnClickListener { onClickListener.onItemClick(position) } + } + } + + override fun getItemCount(): Int = values.size + + inner class ViewHolder(var binding: ItemTextBinding) : RecyclerView.ViewHolder(binding.root) +} \ No newline at end of file diff --git a/app/src/main/java/org/openobservatory/ooniprobe/activity/reviewdescriptorupdates/ReviewDescriptorUpdatesActivity.kt b/app/src/main/java/org/openobservatory/ooniprobe/activity/reviewdescriptorupdates/ReviewDescriptorUpdatesActivity.kt index 824d070fc..b3cdfb738 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/activity/reviewdescriptorupdates/ReviewDescriptorUpdatesActivity.kt +++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/reviewdescriptorupdates/ReviewDescriptorUpdatesActivity.kt @@ -117,7 +117,10 @@ class ReviewDescriptorUpdatesActivity : AbstractActivity() { if ((currPos + 1) != binding.viewpager.adapter?.itemCount) { binding.viewpager.currentItem = currPos + 1 } else { - setResult(RESULT_OK, Intent().putExtra(RESULT_MESSAGE, "Link(s) updated")) + setResult( + RESULT_OK, + Intent().putExtra(RESULT_MESSAGE, "Link(s) updated") + ) finish() } true @@ -201,24 +204,10 @@ private const val DESCRIPTOR = "descriptor" */ class DescriptorUpdateFragment : Fragment() { - private lateinit var binding: FragmentDescriptorUpdateBinding - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = FragmentDescriptorUpdateBinding.inflate(inflater, container, false) - return binding.root - } + companion object { - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - arguments?.takeIf { it.containsKey(DESCRIPTOR) }?.apply { - val descriptor: TestDescriptor = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getSerializable(DESCRIPTOR, TestDescriptor::class.java)!! - } else { - getSerializable(DESCRIPTOR) as TestDescriptor - } + @JvmStatic + fun bindData(context: Context, descriptor: TestDescriptor, binding: FragmentDescriptorUpdateBinding): InstalledDescriptor { val absDescriptor = InstalledDescriptor(descriptor) binding.apply { title.text = absDescriptor.title @@ -229,7 +218,7 @@ class DescriptorUpdateFragment : Fragment() { ).format(descriptor.dateCreated) }" description.text = absDescriptor.description // Use markdown - icon.setImageResource(absDescriptor.getDisplayIcon(requireContext())) + icon.setImageResource(absDescriptor.getDisplayIcon(context)) val adapter = ReviewDescriptorExpandableListAdapter(nettests = absDescriptor.nettests) expandableListView.setAdapter(adapter) @@ -238,6 +227,30 @@ class DescriptorUpdateFragment : Fragment() { expandableListView.expandGroup(i) } } + return absDescriptor + } + } + + private lateinit var binding: FragmentDescriptorUpdateBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentDescriptorUpdateBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + arguments?.takeIf { it.containsKey(DESCRIPTOR) }?.apply { + val descriptor: TestDescriptor = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getSerializable(DESCRIPTOR, TestDescriptor::class.java)!! + } else { + getSerializable(DESCRIPTOR) as TestDescriptor + } + bindData(requireContext(), descriptor, binding) } } } diff --git a/app/src/main/java/org/openobservatory/ooniprobe/common/TestDescriptorManager.kt b/app/src/main/java/org/openobservatory/ooniprobe/common/TestDescriptorManager.kt index 54253b94a..ba764d443 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/common/TestDescriptorManager.kt +++ b/app/src/main/java/org/openobservatory/ooniprobe/common/TestDescriptorManager.kt @@ -1,10 +1,12 @@ package org.openobservatory.ooniprobe.common import android.content.Context +import com.google.gson.Gson import com.raizlabs.android.dbflow.sql.language.SQLite import org.openobservatory.engine.BaseNettest import org.openobservatory.engine.LoggerArray import org.openobservatory.engine.OONIRunDescriptor +import org.openobservatory.engine.OONIRunRevisions import org.openobservatory.ooniprobe.BuildConfig import org.openobservatory.ooniprobe.activity.adddescriptor.adapter.GroupedItem import org.openobservatory.ooniprobe.model.database.InstalledDescriptor @@ -60,26 +62,24 @@ class TestDescriptorManager @Inject constructor( val ooniContext = session.newContextWithTimeout(300) val response: OONIRunDescriptor = - session.ooniRunFetch(ooniContext, BuildConfig.OONI_API_BASE_URL, runId) - return TestDescriptor( - runId = runId, - name = response.name, - shortDescription = response.shortDescription, - description = response.description, - author = response.author, - nettests = response.nettests, - nameIntl = response.nameIntl, - shortDescriptionIntl = response.shortDescriptionIntl, - descriptionIntl = response.descriptionIntl, - icon = response.icon, - color = response.color, - animation = response.animation, - expirationDate = response.expirationDate, - dateCreated = response.dateCreated, - dateUpdated = response.dateUpdated, - revision = response.revision, - isExpired = response.isExpired - ) + session.getLatestOONIRunLink(ooniContext, BuildConfig.OONI_API_BASE_URL, runId) + + var revisions: OONIRunRevisions? = null + + try { + if (Integer.parseInt(response.revision) > 1) { + revisions = session.getOONIRunLinkRevisions( + ooniContext, + BuildConfig.OONI_API_BASE_URL, + runId + ) + } + } catch (e: Exception) { + ThirdPartyServices.logException(e) + } + return response.toTestDescriptor().apply { + previousRevision = Gson().toJson(revisions) + } } fun addDescriptor( @@ -151,3 +151,25 @@ class TestDescriptorManager @Inject constructor( .queryList() } } + +fun OONIRunDescriptor.toTestDescriptor(): TestDescriptor { + return TestDescriptor( + runId = oonirunLinkId.toLong(), + name = name, + shortDescription = shortDescription, + description = description, + author = author, + nettests = nettests, + nameIntl = nameIntl, + shortDescriptionIntl = shortDescriptionIntl, + descriptionIntl = descriptionIntl, + icon = icon, + color = color, + animation = animation, + expirationDate = expirationDate, + dateCreated = dateCreated, + dateUpdated = dateUpdated, + revision = revision, + isExpired = isExpired + ) +} diff --git a/app/src/main/java/org/openobservatory/ooniprobe/model/database/TestDescriptor.kt b/app/src/main/java/org/openobservatory/ooniprobe/model/database/TestDescriptor.kt index 555dd24a7..030d5ec3e 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/model/database/TestDescriptor.kt +++ b/app/src/main/java/org/openobservatory/ooniprobe/model/database/TestDescriptor.kt @@ -74,13 +74,13 @@ class TestDescriptor( @Column var revision: String? = null, + + @Column(name = "previous_revision") + var previousRevision: String? = null, + @Column(name = "is_expired") var isExpired: Boolean? = false, - // TODO(aanorbel): figure out whether we should remove this field. - //@Column - //var isArchived: Boolean = false, - @Column(name = "auto_run") var isAutoRun: Boolean = true, @@ -241,6 +241,8 @@ class ITestDescriptor( val revision: String? = null, + var previousRevision: String? = null, + val isExpired: Boolean? = false, var isAutoUpdate: Boolean = false, @@ -264,6 +266,7 @@ class ITestDescriptor( dateCreated = dateCreated, dateUpdated = dateUpdated, revision = revision, + previousRevision = previousRevision, isExpired = isExpired, isAutoUpdate = isAutoUpdate ) diff --git a/app/src/main/res/layout/activity_overview.xml b/app/src/main/res/layout/activity_overview.xml index baa974034..cbd842cd0 100644 --- a/app/src/main/res/layout/activity_overview.xml +++ b/app/src/main/res/layout/activity_overview.xml @@ -223,6 +223,13 @@ app:layout_constraintTop_toBottomOf="@id/header" tools:listitem="@layout/overview_test_group_list_item" /> + +