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" /> + +