From 6263520f6ccb0ed591800588cb0c2fb359ffedec Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Mon, 20 May 2024 09:07:40 +0100 Subject: [PATCH] feat (OONI Run v2): Add support for updated badge (#727) ## Proposed Changes - Introduce `AvailableUpdatesViewModel` to store cancelled updates across the application instance. - Show the `updated` badge based on the availability of the descriptor in `AvailableUpdatesViewModel` |.|.| |-|-| | ![Screenshot_20240508_090940](https://github.com/ooni/probe-android/assets/17911892/fffe9487-5836-4742-a571-d8530ed3d647) | ![Screenshot_20240508_090952](https://github.com/ooni/probe-android/assets/17911892/c4b990ed-8cdc-4635-a637-6349a71e9356) | --- app/build.gradle | 1 + .../ooniprobe/activity/MainActivity.java | 65 ++++---- .../ooniprobe/activity/OverviewActivity.java | 21 +++ .../adddescriptor/AddDescriptorActivity.kt | 4 +- .../AvailableUpdatesViewModel.kt | 23 +++ .../ReviewDescriptorUpdatesActivity.kt | 155 ++++++++++-------- .../ooniprobe/adapters/DashboardAdapter.kt | 13 +- .../ooniprobe/fragment/DashboardFragment.kt | 9 + .../fragment/dashboard/DashboardViewModel.kt | 39 ++++- .../ooniprobe/model/database/Result.java | 2 +- .../model/database/TestDescriptor.kt | 7 +- app/src/main/res/drawable/warning_amber.xml | 9 + app/src/main/res/layout/activity_overview.xml | 10 ++ app/src/main/res/layout/item_testsuite.xml | 8 + app/src/main/res/layout/updated_tag.xml | 30 ++++ 15 files changed, 282 insertions(+), 114 deletions(-) create mode 100644 app/src/main/java/org/openobservatory/ooniprobe/activity/reviewdescriptorupdates/AvailableUpdatesViewModel.kt create mode 100644 app/src/main/res/drawable/warning_amber.xml create mode 100644 app/src/main/res/layout/updated_tag.xml diff --git a/app/build.gradle b/app/build.gradle index e0080c707..b58f03244 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -51,6 +51,7 @@ android { stable { dimension 'testing' buildConfigField 'String', 'BASE_SOFTWARE_NAME', '"ooniprobe-android"' + buildConfigField 'String', 'OONI_API_BASE_URL', '"https://api.dev.ooni.io"' } dev { dimension 'testing' diff --git a/app/src/main/java/org/openobservatory/ooniprobe/activity/MainActivity.java b/app/src/main/java/org/openobservatory/ooniprobe/activity/MainActivity.java index 025f442b1..acc0c6735 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/activity/MainActivity.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/MainActivity.java @@ -32,6 +32,7 @@ import com.google.android.material.snackbar.Snackbar; import org.openobservatory.ooniprobe.R; +import org.openobservatory.ooniprobe.activity.reviewdescriptorupdates.AvailableUpdatesViewModel; import org.openobservatory.ooniprobe.activity.reviewdescriptorupdates.ReviewDescriptorUpdatesActivity; import org.openobservatory.ooniprobe.common.Application; import org.openobservatory.ooniprobe.common.NotificationUtility; @@ -75,6 +76,9 @@ public class MainActivity extends ReviewUpdatesAbstractActivity implements Confi @Inject TestDescriptorManager descriptorManager; + @Inject + AvailableUpdatesViewModel updatesViewModel; + private ActivityResultLauncher requestPermissionLauncher; public static Intent newIntent(Context context, int resItem) { @@ -213,7 +217,7 @@ public void fetchManualUpdate() { */ private void onManualUpdatesFetchComplete(WorkInfo workInfo) { if (workInfo != null) { - if (workInfo.getProgress().getInt(PROGRESS,-1) >= 0) { + if (workInfo.getProgress().getInt(PROGRESS, -1) >= 0) { binding.reviewUpdateNotificationFragment.setVisibility(View.VISIBLE); } switch (workInfo.getState()) { @@ -224,29 +228,32 @@ private void onManualUpdatesFetchComplete(WorkInfo workInfo) { return; } getSupportFragmentManager() - .beginTransaction() - .add( - R.id.review_update_notification_fragment, - OONIRunDynamicProgressBar.newInstance(ProgressType.REVIEW_LINK, new OnActionListener() { - @Override - public void onActionButtonCLicked() { - - getReviewUpdatesLauncher().launch( - ReviewDescriptorUpdatesActivity.newIntent( - MainActivity.this, - descriptor - ) - ); - removeProgressFragment(R.id.review_update_notification_fragment); - } - - @Override - public void onCloseButtonClicked() { - removeProgressFragment(R.id.review_update_notification_fragment); - } - }), - OONIRunDynamicProgressBar.getTAG() + "_review_update_success_notification" - ).commit(); + .beginTransaction() + .add( + R.id.review_update_notification_fragment, + OONIRunDynamicProgressBar.newInstance(ProgressType.REVIEW_LINK, new OnActionListener() { + @Override + public void onActionButtonCLicked() { + + getReviewUpdatesLauncher().launch( + ReviewDescriptorUpdatesActivity.newIntent( + MainActivity.this, + descriptor, + true + ) + ); + removeProgressFragment(R.id.review_update_notification_fragment); + } + + @Override + public void onCloseButtonClicked() { + updatesViewModel.setDescriptorsWith(descriptor); + //TODO: Add a method to reload the list of descriptors. + removeProgressFragment(R.id.review_update_notification_fragment); + } + }), + OONIRunDynamicProgressBar.getTAG() + "_review_update_success_notification" + ).commit(); } case ENQUEUED -> getSupportFragmentManager() @@ -321,11 +328,11 @@ protected void onNewIntent(Intent intent) { binding.bottomNavigation.setSelectedItemId(intent.getIntExtra(RES_ITEM, R.id.dashboard)); } else if (intent.getExtras().containsKey(NOTIFICATION_DIALOG)) { new ConfirmDialogFragment.Builder() - .withTitle(intent.getExtras().getString("title")) - .withMessage(intent.getExtras().getString("message")) - .withNegativeButton("") - .withPositiveButton(getString(R.string.Modal_OK)) - .build().show(getSupportFragmentManager(), null); + .withTitle(intent.getExtras().getString("title")) + .withMessage(intent.getExtras().getString("message")) + .withNegativeButton("") + .withPositiveButton(getString(R.string.Modal_OK)) + .build().show(getSupportFragmentManager(), null); } } } 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 81663fa64..9806ce26e 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/activity/OverviewActivity.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/OverviewActivity.java @@ -35,6 +35,7 @@ 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.AvailableUpdatesViewModel; import org.openobservatory.ooniprobe.activity.reviewdescriptorupdates.ReviewDescriptorUpdatesActivity; import org.openobservatory.ooniprobe.common.AbstractDescriptor; import org.openobservatory.ooniprobe.common.OONITests; @@ -67,6 +68,10 @@ public class OverviewActivity extends ReviewUpdatesAbstractActivity implements C @Inject OverviewViewModel viewModel; + + @Inject + AvailableUpdatesViewModel updatesViewModel; + OverviewTestsExpandableListViewAdapter adapter; private AbstractDescriptor descriptor; @@ -112,6 +117,22 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { if (Boolean.TRUE.equals(testDescriptor.isExpired())) { binding.expiredTag.getRoot().setVisibility(View.VISIBLE); } + + InstalledDescriptor installedDescriptor = ((InstalledDescriptor) descriptor); + if (installedDescriptor.isUpdateAvailable()) { + binding.updatedTag.getRoot().setVisibility(View.VISIBLE); + + + binding.reviewUpdates.setVisibility(View.VISIBLE); + binding.reviewUpdates.setOnClickListener(view -> getReviewUpdatesLauncher().launch( + ReviewDescriptorUpdatesActivity.newIntent( + OverviewActivity.this, + updatesViewModel.getUpdatedDescriptor(testDescriptor.getRunId()) + ) + )); + + + } } else { markwon.setMarkdown(binding.desc, descriptor.getDescription()); } 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 2159b9d1d..5f424377a 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 @@ -89,7 +89,9 @@ class AddDescriptorActivity : AbstractActivity() { StringUtils.camelToSnake( iconName ), "drawable", imageView.context.packageName - ) + ).let { + if (it == 0) R.drawable.ooni_empty_state else it + } ).apply { color?.let { imageView.setColorFilter(it) diff --git a/app/src/main/java/org/openobservatory/ooniprobe/activity/reviewdescriptorupdates/AvailableUpdatesViewModel.kt b/app/src/main/java/org/openobservatory/ooniprobe/activity/reviewdescriptorupdates/AvailableUpdatesViewModel.kt new file mode 100644 index 000000000..7468c8c2d --- /dev/null +++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/reviewdescriptorupdates/AvailableUpdatesViewModel.kt @@ -0,0 +1,23 @@ +package org.openobservatory.ooniprobe.activity.reviewdescriptorupdates + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.google.gson.Gson +import org.openobservatory.ooniprobe.model.database.ITestDescriptor +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AvailableUpdatesViewModel @Inject() constructor(var gson: Gson) : ViewModel() { + var descriptors: MutableLiveData> = MutableLiveData() + var descriptorString: MutableLiveData = MutableLiveData() + + fun setDescriptorsWith(descriptorJson: String) { + descriptorString.value = descriptorJson + descriptors.value = gson.fromJson(descriptorJson, Array::class.java).toList() + } + + fun getUpdatedDescriptor(runId: Long): String { + return gson.toJson(arrayOf(descriptors.value?.find { it.runId == runId })) + } +} \ 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 b3cdfb738..6cebefeda 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 @@ -19,7 +19,6 @@ import androidx.fragment.app.FragmentActivity import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import com.google.android.material.checkbox.MaterialCheckBox -import com.google.android.material.snackbar.Snackbar import com.google.gson.Gson import com.google.gson.internal.LinkedTreeMap import org.openobservatory.engine.BaseNettest @@ -45,6 +44,7 @@ class ReviewDescriptorUpdatesActivity : AbstractActivity() { companion object { private const val DESCRIPTORS = "descriptors" + private const val PERSISTENT = "persistent" @JvmField var RESULT_MESSAGE = "result" @@ -58,10 +58,18 @@ class ReviewDescriptorUpdatesActivity : AbstractActivity() { @JvmStatic fun newIntent(context: Context, descriptors: String?): Intent { return Intent(context, ReviewDescriptorUpdatesActivity::class.java).putExtra( - DESCRIPTORS, - descriptors + DESCRIPTORS, + descriptors ) } + + @JvmStatic + fun newIntent(context: Context, descriptors: String?, persistent: Boolean): Intent { + return Intent(context, ReviewDescriptorUpdatesActivity::class.java).apply { + putExtra(DESCRIPTORS, descriptors) + putExtra(PERSISTENT, persistent) + } + } } @Inject @@ -70,6 +78,9 @@ class ReviewDescriptorUpdatesActivity : AbstractActivity() { @Inject lateinit var gson: Gson + @Inject + lateinit var updatesViewModel: AvailableUpdatesViewModel + private lateinit var reviewUpdatesPagingAdapter: ReviewUpdatesPagingAdapter override fun onCreate(savedInstanceState: Bundle?) { @@ -89,8 +100,8 @@ class ReviewDescriptorUpdatesActivity : AbstractActivity() { * Because [TestDescriptor.nettests] is of type [Any], the gson library converts it to a [LinkedTreeMap]. */ val descriptors: List = - gson.fromJson(descriptorJson, Array::class.java) - .map { it.toTestDescriptor() } + gson.fromJson(descriptorJson, Array::class.java) + .map { it.toTestDescriptor() } // Disable swipe behavior of viewpager binding.viewpager.isUserInputEnabled = false @@ -104,31 +115,31 @@ class ReviewDescriptorUpdatesActivity : AbstractActivity() { * When the user clicks on the last update, the activity is finished. */ val bottomBarOnMenuItemClickListener: Toolbar.OnMenuItemClickListener = - Toolbar.OnMenuItemClickListener { item -> - when (item.itemId) { - R.id.update_descriptor -> { - descriptorManager.updateFromNetwork(descriptors[binding.viewpager.currentItem]) - /** - * **[currPos]** is the current position of the viewpager. - * If the current position is not the last position, the viewpager is swiped to the next page. - * If the current position is the last position, the last update is saved in the shared preferences and the activity is finished. - */ - val currPos: Int = binding.viewpager.currentItem - if ((currPos + 1) != binding.viewpager.adapter?.itemCount) { - binding.viewpager.currentItem = currPos + 1 - } else { - setResult( - RESULT_OK, - Intent().putExtra(RESULT_MESSAGE, "Link(s) updated") - ) - finish() + Toolbar.OnMenuItemClickListener { item -> + when (item.itemId) { + R.id.update_descriptor -> { + descriptorManager.updateFromNetwork(descriptors[binding.viewpager.currentItem]) + /** + * **[currPos]** is the current position of the viewpager. + * If the current position is not the last position, the viewpager is swiped to the next page. + * If the current position is the last position, the last update is saved in the shared preferences and the activity is finished. + */ + val currPos: Int = binding.viewpager.currentItem + if ((currPos + 1) != binding.viewpager.adapter?.itemCount) { + binding.viewpager.currentItem = currPos + 1 + } else { + setResult( + RESULT_OK, + Intent().putExtra(RESULT_MESSAGE, "Link(s) updated") + ) + finish() + } + true } - true - } - else -> false + else -> false + } } - } binding.bottomBar.setOnMenuItemClickListener(bottomBarOnMenuItemClickListener) /** @@ -138,16 +149,16 @@ class ReviewDescriptorUpdatesActivity : AbstractActivity() { binding.viewpager.registerOnPageChangeCallback(object : OnPageChangeCallback() { override fun onPageSelected(position: Int) { binding.bottomBar.menu.findItem(R.id.update_descriptor) - ?.let { - val countString = - "(${position + 1} of ${binding.viewpager.adapter?.itemCount})" - supportActionBar?.title = "Link Update $countString" - it.title = if ((position + 1) != binding.viewpager.adapter?.itemCount) { - "UPDATE $countString" - } else { - "UPDATE AND FINISH $countString" + ?.let { + val countString = + "(${position + 1} of ${binding.viewpager.adapter?.itemCount})" + supportActionBar?.title = "Link Update $countString" + it.title = if ((position + 1) != binding.viewpager.adapter?.itemCount) { + "UPDATE $countString" + } else { + "UPDATE AND FINISH $countString" + } } - } } }) @@ -166,13 +177,21 @@ class ReviewDescriptorUpdatesActivity : AbstractActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.close_button -> { - finish() + if (intent.getBooleanExtra(PERSISTENT, false)) { + intent.getStringExtra(DESCRIPTORS)?.let { updatesViewModel.setDescriptorsWith(it) } + } + onSupportNavigateUp() true } else -> super.onOptionsItemSelected(item) } } + + override fun onSupportNavigateUp(): Boolean { + onBackPressed() + return true + } } /** @@ -181,8 +200,8 @@ class ReviewDescriptorUpdatesActivity : AbstractActivity() { * @param descriptors is the list of descriptors to display. */ class ReviewUpdatesPagingAdapter( - fragmentActivity: FragmentActivity, - private val descriptors: List + fragmentActivity: FragmentActivity, + private val descriptors: List ) : FragmentStateAdapter(fragmentActivity) { override fun getItemCount(): Int = descriptors.size @@ -213,14 +232,14 @@ class DescriptorUpdateFragment : Fragment() { title.text = absDescriptor.title author.text = "Created by ${descriptor.author} on ${ SimpleDateFormat( - "MMM dd, yyyy", - Locale.getDefault() + "MMM dd, yyyy", + Locale.getDefault() ).format(descriptor.dateCreated) }" description.text = absDescriptor.description // Use markdown icon.setImageResource(absDescriptor.getDisplayIcon(context)) val adapter = - ReviewDescriptorExpandableListAdapter(nettests = absDescriptor.nettests) + ReviewDescriptorExpandableListAdapter(nettests = absDescriptor.nettests) expandableListView.setAdapter(adapter) // Expand all groups for (i in 0 until adapter.groupCount) { @@ -234,9 +253,9 @@ class DescriptorUpdateFragment : Fragment() { private lateinit var binding: FragmentDescriptorUpdateBinding override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? ): View { binding = FragmentDescriptorUpdateBinding.inflate(inflater, container, false) return binding.root @@ -245,11 +264,11 @@ class DescriptorUpdateFragment : Fragment() { 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 - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getSerializable(DESCRIPTOR, TestDescriptor::class.java)!! + } else { + getSerializable(DESCRIPTOR) as TestDescriptor + } bindData(requireContext(), descriptor, binding) } } @@ -261,7 +280,7 @@ class DescriptorUpdateFragment : Fragment() { * @param nettests is the list of nettests to display. */ class ReviewDescriptorExpandableListAdapter( - val nettests: List, + val nettests: List, ) : BaseExpandableListAdapter() { /** @@ -274,7 +293,7 @@ class ReviewDescriptorExpandableListAdapter( * @return Number of children in the group. */ override fun getChildrenCount(groupPosition: Int): Int = - nettests[groupPosition].inputs?.size ?: 0 + nettests[groupPosition].inputs?.size ?: 0 /** * @param groupPosition Position of the group in the list. @@ -288,7 +307,7 @@ class ReviewDescriptorExpandableListAdapter( * @return string item at position. */ override fun getChild(groupPosition: Int, childPosition: Int): String? = - nettests[groupPosition].inputs?.get(childPosition) + nettests[groupPosition].inputs?.get(childPosition) /** * @param groupPosition Position of the group in the list. @@ -316,22 +335,22 @@ class ReviewDescriptorExpandableListAdapter( * @return View of the group. */ override fun getGroupView( - groupPosition: Int, - isExpanded: Boolean, - convertView: View?, - parent: ViewGroup, + groupPosition: Int, + isExpanded: Boolean, + convertView: View?, + parent: ViewGroup, ): View { val view = convertView ?: LayoutInflater.from(parent.context) - .inflate(R.layout.nettest_group_list_item, parent, false) + .inflate(R.layout.nettest_group_list_item, parent, false) val groupItem = getGroup(groupPosition) val groupIndicator = view.findViewById(R.id.group_indicator) val abstractNettest = AbstractTest.getTestByName(groupItem.name) view.findViewById(R.id.group_name).text = - when (abstractNettest.labelResId == R.string.Test_Experimental_Fullname) { - true -> groupItem.name - false -> parent.context.resources.getText(abstractNettest.labelResId) - } + when (abstractNettest.labelResId == R.string.Test_Experimental_Fullname) { + true -> groupItem.name + false -> parent.context.resources.getText(abstractNettest.labelResId) + } val groupCheckBox = view.findViewById(R.id.groupCheckBox) groupCheckBox.visibility = View.GONE @@ -357,14 +376,14 @@ class ReviewDescriptorExpandableListAdapter( * @return View object. */ override fun getChildView( - groupPosition: Int, - childPosition: Int, - isLastChild: Boolean, - convertView: View?, - parent: ViewGroup + groupPosition: Int, + childPosition: Int, + isLastChild: Boolean, + convertView: View?, + parent: ViewGroup ): View { val view = convertView ?: LayoutInflater.from(parent.context) - .inflate(R.layout.nettest_child_list_item, parent, false) + .inflate(R.layout.nettest_child_list_item, parent, false) view.findViewById(R.id.text).apply { text = getChild(groupPosition, childPosition) diff --git a/app/src/main/java/org/openobservatory/ooniprobe/adapters/DashboardAdapter.kt b/app/src/main/java/org/openobservatory/ooniprobe/adapters/DashboardAdapter.kt index 97f286759..b640ddc42 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/adapters/DashboardAdapter.kt +++ b/app/src/main/java/org/openobservatory/ooniprobe/adapters/DashboardAdapter.kt @@ -9,7 +9,6 @@ import org.openobservatory.ooniprobe.common.PreferenceManager import org.openobservatory.ooniprobe.databinding.ItemSeperatorBinding import org.openobservatory.ooniprobe.databinding.ItemTestsuiteBinding import org.openobservatory.ooniprobe.model.database.InstalledDescriptor -import java.util.Date class DashboardAdapter( private val items: List, @@ -46,9 +45,9 @@ class DashboardAdapter( val item = items[position] when (holder.itemViewType) { VIEW_TYPE_TITLE -> { - val separator = holder as CardGroupTitleViewHolder - separator.binding.root.text = item as String - } + val separator = holder as CardGroupTitleViewHolder + separator.binding.root.text = item as String + } VIEW_TYPE_CARD -> { val cardHolder = holder as CardViewHolder @@ -57,10 +56,14 @@ class DashboardAdapter( title.setText(item.title) desc.setText(item.shortDescription) icon.setImageResource(item.getDisplayIcon(holder.itemView.context)).also { - if (item is InstalledDescriptor){ + if (item is InstalledDescriptor) { icon.setColorFilter(item.color) if (item.descriptor?.isExpired == true) { expiredTag.root.visibility = View.VISIBLE + } else if (item.tags?.isNotEmpty() == true && item.tags?.get(0) == "updated") { + updatedTag.root.visibility = View.VISIBLE + } else { + expiredTag.root.visibility = View.GONE } holder.setIsRecyclable(false) } diff --git a/app/src/main/java/org/openobservatory/ooniprobe/fragment/DashboardFragment.kt b/app/src/main/java/org/openobservatory/ooniprobe/fragment/DashboardFragment.kt index 426600f89..063e6c000 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/fragment/DashboardFragment.kt +++ b/app/src/main/java/org/openobservatory/ooniprobe/fragment/DashboardFragment.kt @@ -14,6 +14,7 @@ import org.openobservatory.ooniprobe.R import org.openobservatory.ooniprobe.activity.AbstractActivity import org.openobservatory.ooniprobe.activity.MainActivity import org.openobservatory.ooniprobe.activity.OverviewActivity +import org.openobservatory.ooniprobe.activity.reviewdescriptorupdates.AvailableUpdatesViewModel import org.openobservatory.ooniprobe.activity.runtests.RunTestsActivity import org.openobservatory.ooniprobe.adapters.DashboardAdapter import org.openobservatory.ooniprobe.common.AbstractDescriptor @@ -36,9 +37,13 @@ class DashboardFragment : Fragment(), View.OnClickListener { lateinit var viewModel: DashboardViewModel private var descriptors: ArrayList> = ArrayList() + @Inject lateinit var testStateRepository: TestStateRepository + @Inject + lateinit var updatesViewModel: AvailableUpdatesViewModel + private lateinit var binding: FragmentDashboardBinding private lateinit var adapter: DashboardAdapter @@ -91,6 +96,10 @@ class DashboardFragment : Fragment(), View.OnClickListener { (requireActivity() as MainActivity).fetchManualUpdate() binding.swipeRefresh.isRefreshing = false } + + updatesViewModel.descriptors.observe(viewLifecycleOwner) { descriptors -> + descriptors.let { viewModel.updateDescriptorWith(it) } + } } override fun onResume() { diff --git a/app/src/main/java/org/openobservatory/ooniprobe/fragment/dashboard/DashboardViewModel.kt b/app/src/main/java/org/openobservatory/ooniprobe/fragment/dashboard/DashboardViewModel.kt index 873b6403c..ee8c890e4 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/fragment/dashboard/DashboardViewModel.kt +++ b/app/src/main/java/org/openobservatory/ooniprobe/fragment/dashboard/DashboardViewModel.kt @@ -9,13 +9,16 @@ import org.openobservatory.engine.BaseNettest import org.openobservatory.ooniprobe.common.AbstractDescriptor import org.openobservatory.ooniprobe.common.PreferenceManager import org.openobservatory.ooniprobe.common.TestDescriptorManager +import org.openobservatory.ooniprobe.model.database.ITestDescriptor import org.openobservatory.ooniprobe.model.database.InstalledDescriptor +import org.openobservatory.ooniprobe.model.database.TestDescriptor import javax.inject.Inject class DashboardViewModel @Inject constructor( - private val preferenceManager: PreferenceManager, - private val descriptorManager: TestDescriptorManager + private val preferenceManager: PreferenceManager, + private val descriptorManager: TestDescriptorManager ) : ViewModel(), DefaultLifecycleObserver { + private var pendingUpdates: MutableLiveData> = MutableLiveData() private var ooniRunDescriptors: List = emptyList() private val oonTestsTitle: String = "OONI Tests" private val oonRunLinksTitle: String = "OONI RUN Links" @@ -25,14 +28,24 @@ class DashboardViewModel @Inject constructor( init { ooniRunDescriptors = descriptorManager.getRunV2Descriptors().map { - InstalledDescriptor(it) + InstalledDescriptor(it, getTags(it)) + } + } + + private fun getTags(descriptor: TestDescriptor): List { + pendingUpdates.value.let { updates -> + return if (updates?.any { it.runId == descriptor.runId } == true) { + listOf("updated") + } else { + emptyList() + } } } override fun onResume(owner: LifecycleOwner) { super.onResume(owner) ooniRunDescriptors = descriptorManager.getRunV2Descriptors().map { - InstalledDescriptor(it) + InstalledDescriptor(it, getTags(it)) } fetchItemList() } @@ -51,12 +64,12 @@ class DashboardViewModel @Inject constructor( private fun fetchItemList() { val groupedItems = items.value!!.groupBy { - return@groupBy if (oonTests.contains(it)) { - oonTestsTitle - } else { - "" - } + return@groupBy if (oonTests.contains(it)) { + oonTestsTitle + } else { + "" } + } val groupedItemList = mutableListOf() groupedItems.forEach { (status, itemList) -> @@ -69,4 +82,12 @@ class DashboardViewModel @Inject constructor( } this.groupedItemList.value = groupedItemList } + + fun updateDescriptorWith(descriptors: List) { + pendingUpdates.value = descriptors + ooniRunDescriptors = descriptorManager.getRunV2Descriptors().map { + InstalledDescriptor(it, getTags(it)) + } + fetchItemList() + } } diff --git a/app/src/main/java/org/openobservatory/ooniprobe/model/database/Result.java b/app/src/main/java/org/openobservatory/ooniprobe/model/database/Result.java index 898cf761a..4be7f79b1 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/model/database/Result.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/model/database/Result.java @@ -239,7 +239,7 @@ public Optional> getDescriptor(Context context) * We return an {@link InstalledDescriptor} object which implements {@link AbstractDescriptor}. */ if (descriptor != null) { - return Optional.of(new InstalledDescriptor(descriptor)); + return Optional.of(new InstalledDescriptor(descriptor, null)); } /** * If the descriptor does not exist, then this is an OONI Provided test or an OONI Run v1 measurement result. 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 985c3f671..afb9f274f 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 @@ -118,7 +118,8 @@ fun TestDescriptor.shouldUpdate(updatedDescriptor: TestDescriptor): Boolean { private const val DESCRIPTOR_TEST_NAME = "ooni_run" class InstalledDescriptor( - var testDescriptor: TestDescriptor + var testDescriptor: TestDescriptor, + var tags: List? = null ) : AbstractDescriptor( name = DESCRIPTOR_TEST_NAME, title = testDescriptor.localizedName(), @@ -146,6 +147,10 @@ class InstalledDescriptor( return R.string.TestResults_NotAvailable } + fun isUpdateAvailable(): Boolean { + return tags?.contains("updated") ?: false + } + override fun toRunTestsGroupItem(preferenceManager: PreferenceManager): GroupItem { return GroupItem( selected = false, diff --git a/app/src/main/res/drawable/warning_amber.xml b/app/src/main/res/drawable/warning_amber.xml new file mode 100644 index 000000000..463d9bc0e --- /dev/null +++ b/app/src/main/res/drawable/warning_amber.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/activity_overview.xml b/app/src/main/res/layout/activity_overview.xml index cbd842cd0..1daa57c49 100644 --- a/app/src/main/res/layout/activity_overview.xml +++ b/app/src/main/res/layout/activity_overview.xml @@ -32,6 +32,16 @@ android:paddingBottom="16dp" app:layout_collapseMode="parallax"> + + + + + + + + + + + + + + \ No newline at end of file