diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4903fd7ed..14189f7f7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -95,6 +95,12 @@
android:parentActivityName=".activity.MainActivity"
android:screenOrientation="userPortrait"
android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar.App.NoActionBar" />
+
+
1) {
+ getSupportFragmentManager().beginTransaction().add(binding.revisionsContainer.getId(), RevisionsFragment.newInstance(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..43882c81d
--- /dev/null
+++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/overview/RevisionsView.kt
@@ -0,0 +1,194 @@
+package org.openobservatory.ooniprobe.activity.overview
+
+import android.content.Context
+import android.content.Intent
+import android.graphics.Paint
+import android.graphics.drawable.ColorDrawable
+import android.os.Build
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.app.ActivityCompat
+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.OONIRunDescriptor
+import org.openobservatory.ooniprobe.R
+import org.openobservatory.ooniprobe.activity.AbstractActivity
+import org.openobservatory.ooniprobe.activity.reviewdescriptorupdates.DescriptorUpdateFragment
+import org.openobservatory.ooniprobe.common.toTestDescriptor
+import org.openobservatory.ooniprobe.databinding.FragmentDescriptorUpdateBinding
+import org.openobservatory.ooniprobe.databinding.FragmentRevisionsBinding
+import org.openobservatory.ooniprobe.databinding.ItemTextBinding
+import org.openobservatory.ooniprobe.model.database.TestDescriptor
+import java.text.SimpleDateFormat
+import java.time.format.DateTimeFormatter
+import java.util.Locale
+
+
+class RevisionsFragment : Fragment() {
+
+ companion object {
+
+ const val ARG_PREVIOUS_REVISIONS = "previous-revisions"
+
+ /**
+ * 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(previousRevisions: String) =
+ RevisionsFragment().apply {
+ arguments = Bundle().apply {
+ putString(ARG_PREVIOUS_REVISIONS, previousRevisions)
+ }
+ }
+ }
+
+ private var revisions = emptyList()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ arguments?.let {
+ revisions = Gson().fromJson(
+ it.getString(ARG_PREVIOUS_REVISIONS), Array::class.java
+ ).toList()
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ val binding = FragmentRevisionsBinding.inflate(inflater, container, false)
+
+ with(binding.list) {
+ layoutManager = LinearLayoutManager(context)
+ adapter = RevisionsRecyclerViewAdapter(revisions, object : OnItemClickListener {
+ override fun onItemClick(position: Int) {
+ ActivityCompat.startActivity(
+ requireActivity(),
+ RevisionsViewActivity.newIntent(
+ requireContext(),
+ revisions[position].toTestDescriptor()
+ ),
+ null
+ )
+ }
+ })
+ }
+
+ 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 = SimpleDateFormat(
+ "MMMM d, yyyy HH:mm:ss z",
+ Locale.ENGLISH
+ ).format(item.dateCreated)
+ 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)
+}
+
+/**
+ * Activity for displaying a single revision.
+ */
+class RevisionsViewActivity : AbstractActivity() {
+
+ companion object {
+ private const val ARG_REVISION = "revision"
+
+ /**
+ * Create an intent for starting this activity.
+ *
+ * @param context The context from which to create the intent.
+ * @param revision The revision to display.
+ * @return The intent for starting this activity.
+ */
+ fun newIntent(context: Context, revision: TestDescriptor) =
+ Intent(context, RevisionsViewActivity::class.java).putExtra(ARG_REVISION, revision)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val binding = FragmentDescriptorUpdateBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ supportActionBar?.setDisplayShowHomeEnabled(true)
+ val descriptorExtra = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ intent.getParcelableExtra(ARG_REVISION, TestDescriptor::class.java)
+ } else {
+ intent.getSerializableExtra(ARG_REVISION) as TestDescriptor?
+ }
+
+ supportActionBar?.title = descriptorExtra?.dateCreated?.let { date ->
+ SimpleDateFormat(
+ "MMMM d, yyyy HH:mm:ss z",
+ Locale.ENGLISH
+ ).format(date)
+ }
+
+ descriptorExtra?.let { descriptor ->
+ binding.testsLabel.text = "TESTS"
+ DescriptorUpdateFragment.bindData(this@RevisionsViewActivity, descriptor, binding)
+ .apply {
+ supportActionBar?.setBackgroundDrawable(ColorDrawable(color))
+ window.statusBarColor = color
+ }
+ }
+ }
+}
\ 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..3f67ad67d 100644
--- a/app/src/main/java/org/openobservatory/ooniprobe/common/TestDescriptorManager.kt
+++ b/app/src/main/java/org/openobservatory/ooniprobe/common/TestDescriptorManager.kt
@@ -1,6 +1,7 @@
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
@@ -60,26 +61,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: List = emptyList()
+
+ 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 +150,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" />
+
+
diff --git a/app/src/main/res/layout/fragment_descriptor_update.xml b/app/src/main/res/layout/fragment_descriptor_update.xml
index 651ad0184..31057368e 100644
--- a/app/src/main/res/layout/fragment_descriptor_update.xml
+++ b/app/src/main/res/layout/fragment_descriptor_update.xml
@@ -62,7 +62,7 @@
tools:text="lore ipsum" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_revisions.xml b/app/src/main/res/layout/fragment_revisions.xml
new file mode 100644
index 000000000..59e7a1121
--- /dev/null
+++ b/app/src/main/res/layout/fragment_revisions.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/engine/src/main/java/org/openobservatory/engine/OONIRunDescriptor.kt b/engine/src/main/java/org/openobservatory/engine/OONIRunDescriptor.kt
index a49e08cff..0712986d4 100644
--- a/engine/src/main/java/org/openobservatory/engine/OONIRunDescriptor.kt
+++ b/engine/src/main/java/org/openobservatory/engine/OONIRunDescriptor.kt
@@ -69,3 +69,8 @@ open class OONIRunNettest(
open var inputs: List?
) : Serializable
+
+
+class OONIRunRevisions(
+ val revisions: List
+) : Serializable
\ No newline at end of file
diff --git a/engine/src/main/java/org/openobservatory/engine/OONISession.java b/engine/src/main/java/org/openobservatory/engine/OONISession.java
index 9d8df4c71..84e3a632f 100644
--- a/engine/src/main/java/org/openobservatory/engine/OONISession.java
+++ b/engine/src/main/java/org/openobservatory/engine/OONISession.java
@@ -1,5 +1,10 @@
package org.openobservatory.engine;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
/**
* OONISession contains shared state for running experiments and/or other
* related task (e.g. geolocation). Note that a OONISession is not meant to
@@ -30,6 +35,7 @@ public interface OONISession {
* @param id ooni run id
* @return [OONIRunFetchResponse] with the contents of the ooni run descriptor.
*/
- OONIRunDescriptor ooniRunFetch(OONIContext ctx, String probeServicesURL, long id) throws Exception;
+ OONIRunDescriptor getLatestOONIRunLink(OONIContext ctx, String probeServicesURL, long id) throws Exception;
+ List getOONIRunLinkRevisions(@Nullable OONIContext ooniContext, @NotNull String probeServicesURL, long runId) throws Exception ;
}
diff --git a/engine/src/main/java/org/openobservatory/engine/PESession.java b/engine/src/main/java/org/openobservatory/engine/PESession.java
index 9b20238d7..d548101f8 100644
--- a/engine/src/main/java/org/openobservatory/engine/PESession.java
+++ b/engine/src/main/java/org/openobservatory/engine/PESession.java
@@ -4,6 +4,11 @@
import com.google.gson.Gson;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.List;
+
import oonimkall.HTTPRequest;
import oonimkall.Oonimkall;
import oonimkall.Session;
@@ -32,12 +37,32 @@ public OONICheckInResults checkIn(OONIContext ctx, OONICheckInConfig config) thr
}
@Override
- public OONIRunDescriptor ooniRunFetch(OONIContext ctx, String probeServicesURL, long id) throws Exception {
+ public OONIRunDescriptor getLatestOONIRunLink(OONIContext ctx, String probeServicesURL, long id) throws Exception {
HTTPRequest request = new HTTPRequest();
request.setMethod("GET");
request.setURL(probeServicesURL + "/api/v2/oonirun/links/" + id);
String response = session.httpDo(ctx.ctx, request).getBody();
- Log.d(PESession.class.getName(), response);
return new Gson().fromJson(response, OONIRunDescriptor.class);
}
+
+ @Override
+ public List getOONIRunLinkRevisions(OONIContext ooniContext, @NotNull String probeServicesURL, long runId) throws Exception {
+ HTTPRequest request = new HTTPRequest();
+ request.setMethod("GET");
+ request.setURL(probeServicesURL + "/api/v2/oonirun/links/" + runId + "/revisions");
+ String response = session.httpDo(ooniContext.ctx, request).getBody();
+ OONIRunRevisions revisions = new Gson().fromJson(response, OONIRunRevisions.class);
+
+ //remove the first element of the list, which is the latest revision
+ revisions.getRevisions().remove(0);
+ List descriptors = new ArrayList<>();
+ for (String revision : revisions.getRevisions()) {
+ request.setURL(probeServicesURL + "/api/v2/oonirun/links/" + runId + "/full-descriptor/" + revision);
+ response = session.httpDo(ooniContext.ctx, request).getBody();
+ OONIRunDescriptor descriptor = new Gson().fromJson(response, OONIRunDescriptor.class);
+ Log.d("OONI", "Revision: " + descriptor.getRevision());
+ descriptors.add(descriptor);
+ }
+ return descriptors;
+ }
}