From 5aac551ab2a81d0a2de82b0c38fb59db9dc726dc Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Tue, 5 Dec 2023 18:14:26 +0100 Subject: [PATCH] [New Designs] Update Dashboard view to match new design (#631) Fixes https://github.com/ooni/probe/issues/2588 ## Proposed Changes - Update XML designs - Convert `ProgressFragment` to kotlin |.|.| |-|-| | ![Screenshot_20231031_172528](https://github.com/ooni/probe-android/assets/17911892/5b848e77-adf6-473c-889f-f8f119938aea) | ![Screenshot_20231031_172516](https://github.com/ooni/probe-android/assets/17911892/fe5f4d99-adc3-42b2-9571-737ae9db2b8f) | |![Screenshot_20231031_172436](https://github.com/ooni/probe-android/assets/17911892/1f30dbae-d8ae-43e1-b615-b34e295213bd) | ![Screenshot_20231031_172418](https://github.com/ooni/probe-android/assets/17911892/ea459e62-088e-4d37-886a-53347df94ca3)| --- .github/workflows/archive.yml | 2 +- app/build.gradle | 6 +- .../ooniprobe/activity/MainActivity.java | 21 +- .../ooniprobe/activity/OverviewActivity.java | 1 + .../ooniprobe/activity/RunningActivity.java | 2 +- .../ooniprobe/adapters/DashboardAdapter.kt | 111 ++++++++++ .../ooniprobe/fragment/DashboardFragment.java | 140 ------------ .../ooniprobe/fragment/DashboardFragment.kt | 120 +++++++++++ .../ooniprobe/fragment/ProgressFragment.java | 164 -------------- .../ooniprobe/fragment/ProgressFragment.kt | 202 ++++++++++++++++++ .../fragment/dashboard/DashboardViewModel.kt | 43 ++++ .../ooniprobe/item/SeperatorItem.java | 1 + .../ooniprobe/item/TestsuiteItem.java | 1 + .../test/suite/ExperimentalSuite.java | 1 + .../ooniprobe/test/test/RiseupVPN.java | 22 +- .../main/res/drawable/bottom_nav_color.xml | 5 + app/src/main/res/drawable/card_boarder.xml | 8 + app/src/main/res/drawable/outline_timer.xml | 5 + app/src/main/res/layout/activity_main.xml | 6 +- .../main/res/layout/fragment_dashboard.xml | 37 ++-- app/src/main/res/layout/fragment_progress.xml | 98 ++++----- app/src/main/res/layout/item_seperator.xml | 5 +- app/src/main/res/layout/item_testsuite.xml | 117 +++++----- app/src/main/res/values-night/colors.xml | 9 +- app/src/main/res/values/colors.xml | 9 +- app/src/main/res/values/styles.xml | 2 +- .../test/suite/ExperimentalSuiteTest.java | 16 +- gradle/libs.versions.toml | 4 +- 28 files changed, 710 insertions(+), 448 deletions(-) create mode 100644 app/src/main/java/org/openobservatory/ooniprobe/adapters/DashboardAdapter.kt delete mode 100644 app/src/main/java/org/openobservatory/ooniprobe/fragment/DashboardFragment.java create mode 100644 app/src/main/java/org/openobservatory/ooniprobe/fragment/DashboardFragment.kt delete mode 100644 app/src/main/java/org/openobservatory/ooniprobe/fragment/ProgressFragment.java create mode 100644 app/src/main/java/org/openobservatory/ooniprobe/fragment/ProgressFragment.kt create mode 100644 app/src/main/java/org/openobservatory/ooniprobe/fragment/dashboard/DashboardViewModel.kt create mode 100644 app/src/main/res/drawable/bottom_nav_color.xml create mode 100644 app/src/main/res/drawable/card_boarder.xml create mode 100644 app/src/main/res/drawable/outline_timer.xml diff --git a/.github/workflows/archive.yml b/.github/workflows/archive.yml index 40344f610..b04392ac3 100644 --- a/.github/workflows/archive.yml +++ b/.github/workflows/archive.yml @@ -16,4 +16,4 @@ jobs: uses: actions/upload-artifact@v3 with: name: dev-apk - path: app/build/outputs/apk/devFull/release \ No newline at end of file + path: app/build/outputs/apk/devFull/release diff --git a/app/build.gradle b/app/build.gradle index e61731ad2..e7a084919 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId 'org.openobservatory.ooniprobe' minSdk libs.versions.minSdk.get().toInteger() targetSdk libs.versions.targetSdk.get().toInteger() - versionName '3.8.4' - versionCode 106 + versionName '3.8.5' + versionCode 107 testInstrumentationRunner "org.openobservatory.ooniprobe.TestAndroidJUnitRunner" buildConfigField 'String', 'OONI_API_BASE_URL', '"https://api.ooni.io/"' buildConfigField 'String', 'NOTIFICATION_SERVER', '"https://countly.ooni.io"' @@ -149,7 +149,7 @@ dependencies { // Unit Testing testImplementation project(':shared-test') testImplementation libs.junit4 - testImplementation libs.androidx.core + testImplementation libs.androidx.test.core testImplementation libs.androidx.runner testImplementation libs.androidx.rules testImplementation libs.mockito.core 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 933bfbf88..e0f79d624 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/activity/MainActivity.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/MainActivity.java @@ -41,6 +41,7 @@ public class MainActivity extends AbstractActivity implements ConfirmDialogFragment.OnConfirmedListener { private static final String RES_ITEM = "resItem"; + private static final String RES_SNACKBAR_MESSAGE = "resSnackbarMessage"; public static final String NOTIFICATION_DIALOG = "notification"; public static final String AUTOTEST_DIALOG = "automatic_testing"; public static final String BATTERY_DIALOG = "battery_optimization"; @@ -59,7 +60,14 @@ public static Intent newIntent(Context context, int resItem) { return new Intent(context, MainActivity.class).putExtra(RES_ITEM, resItem).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); } - @Override + public static Intent newIntent(Context context, int resItem, String message) { + return new Intent(context, MainActivity.class) + .putExtra(RES_ITEM, resItem) + .putExtra(RES_SNACKBAR_MESSAGE, message) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + } + + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); getActivityComponent().inject(this); @@ -85,7 +93,18 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { return false; } }); + /* TODO(aanorbel): Fix change in state(theme change from notification) changes the selected item. + The proper fix would be to track the selected item as well as other properties in a `ViewModel`. */ binding.bottomNavigation.setSelectedItemId(getIntent().getIntExtra(RES_ITEM, R.id.dashboard)); + /* Check if we are restoring the activity from a saved state first. + * If we have a message to show, show it as a snackbar. + * This is used to show the message from test completion. + */ + if (savedInstanceState == null && getIntent().hasExtra(RES_SNACKBAR_MESSAGE)) { + Snackbar.make(binding.getRoot(), getIntent().getStringExtra(RES_SNACKBAR_MESSAGE), Snackbar.LENGTH_SHORT) + .setAnchorView(binding.bottomNavigation) + .show(); + } if (notificationManager.shouldShowAutoTest()) { new ConfirmDialogFragment.Builder() .withTitle(getString(R.string.Modal_Autorun_Modal_Title)) 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 296232913..92b952b5b 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/activity/OverviewActivity.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/OverviewActivity.java @@ -60,6 +60,7 @@ public static Intent newIntent(Context context, AbstractSuite testSuite) { String experimentalLinks = "\n\n* [STUN Reachability](https://github.com/ooni/spec/blob/master/nettests/ts-025-stun-reachability.md)" + "\n\n* [DNS Check](https://github.com/ooni/spec/blob/master/nettests/ts-028-dnscheck.md)" + + "\n\n* [RiseupVPN](https://ooni.org/nettest/riseupvpn/)" + "\n\n* [ECH Check](https://github.com/ooni/spec/blob/master/nettests/ts-039-echcheck.md)" + "\n\n* [Tor Snowflake](https://ooni.org/nettest/tor-snowflake/) "+ String.format(" ( %s )",getString(R.string.Settings_TestOptions_LongRunningTest))+ "\n\n* [Vanilla Tor](https://github.com/ooni/spec/blob/master/nettests/ts-016-vanilla-tor.md) " + String.format(" ( %s )",getString(R.string.Settings_TestOptions_LongRunningTest)); diff --git a/app/src/main/java/org/openobservatory/ooniprobe/activity/RunningActivity.java b/app/src/main/java/org/openobservatory/ooniprobe/activity/RunningActivity.java index c65b13df1..4c82c2834 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/activity/RunningActivity.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/RunningActivity.java @@ -216,7 +216,7 @@ protected void onDestroy() { } private void testEnded(Context context) { - startActivity(MainActivity.newIntent(context, R.id.testResults)); + startActivity(MainActivity.newIntent(context, R.id.testResults,"Probe Run complete")); finish(); } diff --git a/app/src/main/java/org/openobservatory/ooniprobe/adapters/DashboardAdapter.kt b/app/src/main/java/org/openobservatory/ooniprobe/adapters/DashboardAdapter.kt new file mode 100644 index 000000000..93d0d6679 --- /dev/null +++ b/app/src/main/java/org/openobservatory/ooniprobe/adapters/DashboardAdapter.kt @@ -0,0 +1,111 @@ +package org.openobservatory.ooniprobe.adapters + +import android.content.res.Resources +import android.graphics.PorterDuff +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.cardview.widget.CardView +import androidx.recyclerview.widget.RecyclerView +import org.openobservatory.ooniprobe.R +import org.openobservatory.ooniprobe.common.PreferenceManager +import org.openobservatory.ooniprobe.databinding.ItemSeperatorBinding +import org.openobservatory.ooniprobe.databinding.ItemTestsuiteBinding +import org.openobservatory.ooniprobe.test.suite.AbstractSuite + +class DashboardAdapter( + private val items: List, + private val onClickListener: View.OnClickListener, + private val preferenceManager: PreferenceManager, +) : RecyclerView.Adapter() { + + companion object { + private const val VIEW_TYPE_TITLE = 0 + private const val VIEW_TYPE_CARD = 1 + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + VIEW_TYPE_TITLE -> { + CardGroupTitleViewHolder( + ItemSeperatorBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + ) + } + + else -> { + CardViewHolder( + ItemTestsuiteBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + ) + } + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val item = items[position] + when (holder.itemViewType) { + VIEW_TYPE_TITLE -> { + val separator = holder as CardGroupTitleViewHolder + separator.binding.root.text = item as String + } + + VIEW_TYPE_CARD -> { + val cardHolder = holder as CardViewHolder + if (item is AbstractSuite) { + cardHolder.binding.apply { + title.setText(item.title) + desc.setText(item.cardDesc) + icon.setImageResource(item.icon) + } + holder.itemView.tag = item + if (item.isTestEmpty(preferenceManager)) { + holder.setIsRecyclable(false) + holder.itemView.apply { + elevation = 0f + isClickable = false + } + val resources: Resources = holder.itemView.context.resources + (holder.itemView as CardView).setCardBackgroundColor(resources.getColor(R.color.disabled_test_background)) + holder.binding.apply { + title.setTextColor(resources.getColor(R.color.disabled_test_text)) + desc.setTextColor(resources.getColor(R.color.disabled_test_text)) + icon.setColorFilter( + resources.getColor(R.color.disabled_test_text), + PorterDuff.Mode.SRC_IN + ) + } + } else { + holder.itemView.setOnClickListener(onClickListener) + } + } + } + } + } + + override fun getItemCount(): Int { + return items.size + } + + override fun getItemViewType(position: Int): Int { + return when (items[position]) { + is String -> VIEW_TYPE_TITLE + else -> VIEW_TYPE_CARD + } + } + + /** + * ViewHolder for a dashboard item group. + * @param binding + */ + class CardGroupTitleViewHolder(var binding: ItemSeperatorBinding) : + RecyclerView.ViewHolder(binding.root) + + /** + * ViewHolder for dashboard item. + * @param binding + */ + class CardViewHolder(var binding: ItemTestsuiteBinding) : RecyclerView.ViewHolder(binding.root) +} diff --git a/app/src/main/java/org/openobservatory/ooniprobe/fragment/DashboardFragment.java b/app/src/main/java/org/openobservatory/ooniprobe/fragment/DashboardFragment.java deleted file mode 100644 index 7eee1ca50..000000000 --- a/app/src/main/java/org/openobservatory/ooniprobe/fragment/DashboardFragment.java +++ /dev/null @@ -1,140 +0,0 @@ -package org.openobservatory.ooniprobe.fragment; - -import android.os.Bundle; -import android.text.format.DateUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.app.ActivityCompat; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.LinearLayoutManager; - -import org.openobservatory.ooniprobe.R; -import org.openobservatory.ooniprobe.activity.AbstractActivity; -import org.openobservatory.ooniprobe.activity.OverviewActivity; -import org.openobservatory.ooniprobe.activity.RunningActivity; -import org.openobservatory.ooniprobe.common.Application; -import org.openobservatory.ooniprobe.common.PreferenceManager; -import org.openobservatory.ooniprobe.common.ReachabilityManager; -import org.openobservatory.ooniprobe.common.ThirdPartyServices; -import org.openobservatory.ooniprobe.databinding.FragmentDashboardBinding; -import org.openobservatory.ooniprobe.item.SeperatorItem; -import org.openobservatory.ooniprobe.item.TestsuiteItem; -import org.openobservatory.ooniprobe.model.database.Result; -import org.openobservatory.ooniprobe.test.TestAsyncTask; -import org.openobservatory.ooniprobe.test.suite.AbstractSuite; - -import java.util.ArrayList; - -import javax.inject.Inject; - -import localhost.toolkit.widget.recyclerview.HeterogeneousRecyclerAdapter; -import localhost.toolkit.widget.recyclerview.HeterogeneousRecyclerItem; - -public class DashboardFragment extends Fragment implements View.OnClickListener { - - @Inject - PreferenceManager preferenceManager; - - private ArrayList items; - - private ArrayList testSuites; - - private HeterogeneousRecyclerAdapter adapter; - - private FragmentDashboardBinding binding; - - @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - binding = FragmentDashboardBinding.inflate(inflater,container,false); - ((Application) getActivity().getApplication()).getFragmentComponent().inject(this); - ((AppCompatActivity) getActivity()).setSupportActionBar(binding.toolbar); - ((AppCompatActivity) getActivity()).getSupportActionBar().setTitle(null); - items = new ArrayList<>(); - testSuites = new ArrayList<>(); - adapter = new HeterogeneousRecyclerAdapter<>(getActivity(), items); - binding.recycler.setAdapter(adapter); - binding.recycler.setLayoutManager(new LinearLayoutManager(getActivity())); - binding.runAll.setOnClickListener(v1 -> runAll()); - binding.vpn.setOnClickListener(view -> ((Application) getActivity().getApplication()).openVPNSettings()); - return binding.getRoot(); - } - - @Override public void onResume() { - super.onResume(); - items.clear(); - testSuites.clear(); - testSuites.addAll(TestAsyncTask.getSuites()); - - ArrayList emptySuites = new ArrayList<>(); - for (AbstractSuite testSuite : testSuites){ - if(testSuite.getTestList(preferenceManager).length > 0){ - items.add(new TestsuiteItem(testSuite, this, preferenceManager)); - } else { - emptySuites.add(testSuite); - } - } - - if(!emptySuites.isEmpty()){ - items.add(new SeperatorItem()); - - for(AbstractSuite emptyTest: emptySuites) - items.add(new TestsuiteItem(emptyTest, this, preferenceManager)); - } - - - - setLastTest(); - adapter.notifyTypesChanged(); - if (ReachabilityManager.isVPNinUse(this.getContext()) - && preferenceManager.isWarnVPNInUse()) - binding.vpn.setVisibility(View.VISIBLE); - else - binding.vpn.setVisibility(View.GONE); - } - - private void setLastTest() { - Result lastResult = Result.getLastResult(); - if (lastResult == null) - binding.lastTested.setText(getString(R.string.Dashboard_Overview_LatestTest) - + " " + - getString(R.string.Dashboard_Overview_LastRun_Never)); - else - binding.lastTested.setText(getString(R.string.Dashboard_Overview_LatestTest) - + " " + - DateUtils.getRelativeTimeSpanString(lastResult.start_time.getTime())); - } - - public void runAll() { - RunningActivity.runAsForegroundService((AbstractActivity) getActivity(), testSuites, this::onTestServiceStartedListener, preferenceManager); - } - - private void onTestServiceStartedListener() { - try { - ((AbstractActivity) getActivity()).bindTestService(); - } catch (Exception e) { - e.printStackTrace(); - ThirdPartyServices.logException(e); - } - } - - @Override public void onClick(View v) { - AbstractSuite testSuite = (AbstractSuite) v.getTag(); - switch (v.getId()) { - case R.id.run: - RunningActivity.runAsForegroundService( - (AbstractActivity) getActivity(), - testSuite.asArray(), - this::onTestServiceStartedListener, - preferenceManager - ); - break; - default: - ActivityCompat.startActivity(getActivity(), OverviewActivity.newIntent(getActivity(), testSuite), null); - break; - } - } -} diff --git a/app/src/main/java/org/openobservatory/ooniprobe/fragment/DashboardFragment.kt b/app/src/main/java/org/openobservatory/ooniprobe/fragment/DashboardFragment.kt new file mode 100644 index 000000000..a973b1cee --- /dev/null +++ b/app/src/main/java/org/openobservatory/ooniprobe/fragment/DashboardFragment.kt @@ -0,0 +1,120 @@ +package org.openobservatory.ooniprobe.fragment + +import android.os.Bundle +import android.text.format.DateUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import org.openobservatory.ooniprobe.R +import org.openobservatory.ooniprobe.activity.AbstractActivity +import org.openobservatory.ooniprobe.activity.OverviewActivity +import org.openobservatory.ooniprobe.activity.RunningActivity +import org.openobservatory.ooniprobe.adapters.DashboardAdapter +import org.openobservatory.ooniprobe.common.Application +import org.openobservatory.ooniprobe.common.PreferenceManager +import org.openobservatory.ooniprobe.common.ReachabilityManager +import org.openobservatory.ooniprobe.common.ThirdPartyServices +import org.openobservatory.ooniprobe.databinding.FragmentDashboardBinding +import org.openobservatory.ooniprobe.fragment.dashboard.DashboardViewModel +import org.openobservatory.ooniprobe.model.database.Result +import org.openobservatory.ooniprobe.test.suite.AbstractSuite +import javax.inject.Inject + +class DashboardFragment : Fragment(), View.OnClickListener { + @Inject + lateinit var preferenceManager: PreferenceManager + + @Inject + lateinit var viewModel: DashboardViewModel + private var testSuites: ArrayList = ArrayList() + private lateinit var binding: FragmentDashboardBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentDashboardBinding.inflate(inflater, container, false) + (requireActivity().application as Application).fragmentComponent.inject(this) + (requireActivity() as AppCompatActivity).apply { + setSupportActionBar(binding.toolbar) + supportActionBar?.title = null + } + binding.apply { + runAll.setOnClickListener { _: View? -> runAll() } + vpn.setOnClickListener { _: View? -> (requireActivity().application as Application).openVPNSettings() } + } + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.getGroupedItemList().observe(viewLifecycleOwner) { items -> + binding.recycler.layoutManager = LinearLayoutManager(requireContext()) + binding.recycler.adapter = DashboardAdapter(items, this, preferenceManager) + } + + viewModel.items.observe(viewLifecycleOwner) { items -> + testSuites.apply { + clear() + addAll(items) + } + } + } + + override fun onResume() { + super.onResume() + setLastTest() + if (ReachabilityManager.isVPNinUse(this.context) + && preferenceManager.isWarnVPNInUse + ) binding.vpn.visibility = View.VISIBLE else binding.vpn.visibility = View.GONE + } + + private fun setLastTest() { + val lastResult = Result.getLastResult() + if (lastResult == null) { + (getString(R.string.Dashboard_Overview_LatestTest) + " " + getString(R.string.Dashboard_Overview_LastRun_Never)) + .also { binding.lastTested.text = it } + } else { + (getString(R.string.Dashboard_Overview_LatestTest) + " " + DateUtils.getRelativeTimeSpanString(lastResult.start_time.time)) + .also { binding.lastTested.text = it } + } + } + + private fun runAll() { + RunningActivity.runAsForegroundService( + activity as AbstractActivity?, + testSuites, + { onTestServiceStartedListener() }, + preferenceManager + ) + } + + private fun onTestServiceStartedListener() = try { + (requireActivity() as AbstractActivity).bindTestService() + } catch (e: Exception) { + e.printStackTrace() + ThirdPartyServices.logException(e) + } + + override fun onClick(v: View) { + val testSuite = v.tag as AbstractSuite + when (v.id) { + R.id.run -> RunningActivity.runAsForegroundService( + activity as AbstractActivity?, + testSuite.asArray(), { onTestServiceStartedListener() }, + preferenceManager + ) + + else -> ActivityCompat.startActivity( + requireActivity(), + OverviewActivity.newIntent(activity, testSuite), + null + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/openobservatory/ooniprobe/fragment/ProgressFragment.java b/app/src/main/java/org/openobservatory/ooniprobe/fragment/ProgressFragment.java deleted file mode 100644 index b37840428..000000000 --- a/app/src/main/java/org/openobservatory/ooniprobe/fragment/ProgressFragment.java +++ /dev/null @@ -1,164 +0,0 @@ -package org.openobservatory.ooniprobe.fragment; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.core.app.ActivityCompat; -import androidx.fragment.app.Fragment; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import org.openobservatory.ooniprobe.R; -import org.openobservatory.ooniprobe.activity.RunningActivity; -import org.openobservatory.ooniprobe.common.Application; -import org.openobservatory.ooniprobe.common.PreferenceManager; -import org.openobservatory.ooniprobe.common.TestProgressRepository; -import org.openobservatory.ooniprobe.common.service.RunTestService; -import org.openobservatory.ooniprobe.databinding.FragmentProgressBinding; -import org.openobservatory.ooniprobe.receiver.TestRunBroadRequestReceiver; - -import javax.inject.Inject; - -/** - * Monitors and displays progress of {@link RunTestService}. - */ -public class ProgressFragment extends Fragment { - private TestRunBroadRequestReceiver receiver; - - private FragmentProgressBinding biding; - - @Inject - PreferenceManager preferenceManager; - @Inject - TestProgressRepository testProgressRepository; - - public ProgressFragment() { - // Required empty public constructor - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - biding = FragmentProgressBinding.inflate(inflater, container, false); - ((Application) getActivity().getApplication()).getFragmentComponent().inject(this); - biding.getRoot().setOnClickListener(v -> { - Intent intent = new Intent(getContext(), RunningActivity.class); - ActivityCompat.startActivity(getActivity(), intent, null); - }); - testProgressRepository.getProgress().observe(getViewLifecycleOwner(),progressValue -> { - if (progressValue!=null) { - biding.progress.setProgress(progressValue); - } - }); - return biding.getRoot(); - } - - @Override - public void onResume() { - super.onResume(); - IntentFilter filter = new IntentFilter("org.openobservatory.ooniprobe.activity.RunningActivity"); - receiver = new TestRunBroadRequestReceiver(preferenceManager, new TestRunnerEventListener(),testProgressRepository); - LocalBroadcastManager.getInstance(getActivity()).registerReceiver(receiver, filter); - //Bind the RunTestService - this.bindTestService(); - } - - public void bindTestService() { - Activity activity = getActivity(); - if (activity!=null && ((Application)activity.getApplication()).isTestRunning()) { - Intent intent = new Intent(getActivity(), RunTestService.class); - getActivity().bindService(intent, receiver, Context.BIND_AUTO_CREATE); - biding.progressLayout.setVisibility(View.VISIBLE); - } - else - biding.progressLayout.setVisibility(View.GONE); - } - - private void updateUI(RunTestService service){ - Activity activity = getActivity(); - if (activity!=null && ((Application)activity.getApplication()).isTestRunning()){ - - Integer progressLevel = testProgressRepository.getProgress().getValue(); - if (progressLevel != null) { - biding.progress.setProgress(progressLevel); - } else { - biding.progress.setIndeterminate(true); - } - if (service != null && service.task != null){ - if (service.task.currentSuite != null) - biding.progress.setMax(service.task.getMax(preferenceManager)); - if (service.task.currentTest != null) - biding.name.setText(getString(service.task.currentTest.getLabelResId())); - } - } - } - - @Override - public void onPause() { - super.onPause(); - if (receiver.isBound()) { - getActivity().unbindService(receiver); - receiver.setBound(false); - } - LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(receiver); - } - - @Override - public void onDestroy() { - super.onDestroy(); - LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(receiver); - } - - private class TestRunnerEventListener implements TestRunBroadRequestReceiver.EventListener { - @Override - public void onStart(RunTestService service) { - updateUI(service); - } - - @Override - public void onRun(String value) { - biding.name.setText(value); - } - - @Override - public void onProgress(int state, double eta) { - if (biding.progress.isIndeterminate()) - updateUI(receiver.service); - biding.progress.setIndeterminate(false); - biding.progress.setProgress(state); - } - - @Override - public void onLog(String value) { - /* nothing */ - } - - @Override - public void onError(String value) { - /* nothing */ - } - - @Override - public void onUrl() { - biding.progress.setIndeterminate(false); - } - - @Override - public void onInterrupt() { - biding.running.setText(getString(R.string.Dashboard_Running_Stopping_Title)); - } - - @Override - public void onEnd(Context context) { - biding.progressLayout.setVisibility(View.GONE); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/openobservatory/ooniprobe/fragment/ProgressFragment.kt b/app/src/main/java/org/openobservatory/ooniprobe/fragment/ProgressFragment.kt new file mode 100644 index 000000000..86132021a --- /dev/null +++ b/app/src/main/java/org/openobservatory/ooniprobe/fragment/ProgressFragment.kt @@ -0,0 +1,202 @@ +package org.openobservatory.ooniprobe.fragment + +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Bundle +import android.text.SpannableStringBuilder +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.core.text.bold +import androidx.fragment.app.Fragment +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import org.openobservatory.ooniprobe.R +import org.openobservatory.ooniprobe.activity.RunningActivity +import org.openobservatory.ooniprobe.common.Application +import org.openobservatory.ooniprobe.common.PreferenceManager +import org.openobservatory.ooniprobe.common.TestProgressRepository +import org.openobservatory.ooniprobe.common.service.RunTestService +import org.openobservatory.ooniprobe.databinding.FragmentProgressBinding +import org.openobservatory.ooniprobe.receiver.TestRunBroadRequestReceiver +import org.openobservatory.ooniprobe.test.suite.ExperimentalSuite +import javax.inject.Inject + +/** + * Monitors and displays progress of [RunTestService]. + */ +class ProgressFragment : Fragment() { + private lateinit var receiver: TestRunBroadRequestReceiver + private lateinit var biding: FragmentProgressBinding + + @Inject + lateinit var preferenceManager: PreferenceManager + + @Inject + lateinit var testProgressRepository: TestProgressRepository + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View { + biding = FragmentProgressBinding.inflate(inflater, container, false) + (requireActivity().application as Application).fragmentComponent.inject(this) + biding.root.setOnClickListener { _: View? -> + val intent = Intent(context, RunningActivity::class.java) + ActivityCompat.startActivity(requireContext(), intent, null) + } + testProgressRepository.progress.observe(viewLifecycleOwner) { progressValue: Int? -> + if (progressValue != null) { + biding.progress.progress = progressValue + } + } + return biding.root + } + + override fun onResume() { + super.onResume() + val filter = IntentFilter("org.openobservatory.ooniprobe.activity.RunningActivity") + receiver = TestRunBroadRequestReceiver( + preferenceManager, TestRunnerEventListener(), testProgressRepository + ) + // NOTE: Simple update to ContextCompat#registerReceiver not possible at the moment. + LocalBroadcastManager.getInstance(requireContext()).registerReceiver(receiver, filter) + bindTestService() + } + + fun bindTestService() { + if ((requireActivity().application as Application).isTestRunning) { + requireContext().bindService( + Intent(requireContext(), RunTestService::class.java), + receiver, + Context.BIND_AUTO_CREATE + ) + biding.progressLayout.visibility = View.VISIBLE + } else { + biding.progressLayout.visibility = View.GONE + } + } + + private fun updateUI(service: RunTestService?) { + if ((requireActivity().application as Application).isTestRunning) { + val progressLevel = testProgressRepository.progress.value + when { + progressLevel != null -> { + biding.progress.progress = progressLevel + } + + else -> { + biding.progress.isIndeterminate = true + } + } + biding.testImage.setImageDrawable(null) + biding.testImage.visibility = View.GONE + + service?.task?.let { task -> + task.currentSuite?.let { + biding.progress.max = service.task.getMax(preferenceManager) + } + task.currentTest?.let { currentTest -> + when { + /** + * If the test has no icon, use the suite icon. + * Currently not used since the most suites have a larger than desired icon. + */ + /*currentTest.iconResId == 0 -> { + biding.testImage.apply { + visibility = View.VISIBLE + setImageDrawable( + ContextCompat.getDrawable( + requireContext(), + task.currentSuite.icon + ) + ) + } + }*/ + /** + * If the test has an icon, use it. + */ + currentTest.iconResId != 0 -> { + biding.testImage.apply { + visibility = View.VISIBLE + setImageDrawable( + ContextCompat.getDrawable( + requireContext(), + task.currentTest.iconResId + ) + ) + } + } + + else -> { + biding.testImage.visibility = View.GONE + } + } + biding.name.text = when (task.currentSuite is ExperimentalSuite) { + true -> SpannableStringBuilder().bold { append(currentTest.name) } + false -> SpannableStringBuilder().bold { append(getString(currentTest.labelResId)) } + }.append(" ") + .append( + getString(R.string.Dashboard_Running_Running) + .replace(":", "").toLowerCase() + ) + + } + } + } + } + + override fun onPause() { + super.onPause() + if (receiver.isBound) { + requireContext().unbindService(receiver) + receiver.isBound = false + } + LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(receiver) + } + + override fun onDestroy() { + super.onDestroy() + LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(receiver) + } + + private inner class TestRunnerEventListener : TestRunBroadRequestReceiver.EventListener { + override fun onStart(service: RunTestService) = updateUI(service) + + override fun onRun(value: String) { + biding.name.text = value + } + + override fun onProgress(state: Int, eta: Double) { + updateUI(receiver.service) + + biding.progress.apply { + isIndeterminate = false + progress = state + } + } + + override fun onLog(value: String) { + /* nothing */ + } + + override fun onError(value: String) { + /* nothing */ + } + + override fun onUrl() { + biding.progress.isIndeterminate = false + } + + override fun onInterrupt() { + biding.testImage.setImageDrawable(null) + biding.testImage.visibility = View.GONE + biding.name.text = getString(R.string.Dashboard_Running_Stopping_Title) + } + + override fun onEnd(context: Context) { + biding.progressLayout.visibility = View.GONE + } + } +} 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 new file mode 100644 index 000000000..892990cc6 --- /dev/null +++ b/app/src/main/java/org/openobservatory/ooniprobe/fragment/dashboard/DashboardViewModel.kt @@ -0,0 +1,43 @@ +package org.openobservatory.ooniprobe.fragment.dashboard + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.openobservatory.ooniprobe.common.PreferenceManager +import org.openobservatory.ooniprobe.test.TestAsyncTask +import org.openobservatory.ooniprobe.test.suite.AbstractSuite +import javax.inject.Inject + +class DashboardViewModel @Inject constructor(private val preferenceManager: PreferenceManager) : ViewModel() { + private val oonTestsTitle: String = "OONI Tests" + private val oonTests = TestAsyncTask.getSuites() + private val groupedItemList = MutableLiveData>() + val items = MutableLiveData>(oonTests) + + fun getGroupedItemList(): LiveData> { + if (groupedItemList.value == null) { + fetchItemList() + } + return groupedItemList + } + + private fun fetchItemList() { + + val groupedItems = items.value!!.sortedBy { it.getTestList(preferenceManager).isEmpty() } + .groupBy { + return@groupBy if (oonTests.contains(it)) { + oonTestsTitle + } else { + "" + } + } + + val groupedItemList = mutableListOf() + groupedItems.forEach { (status, itemList) -> + groupedItemList.add(status) + groupedItemList.addAll(itemList) + } + + this.groupedItemList.value = groupedItemList + } +} diff --git a/app/src/main/java/org/openobservatory/ooniprobe/item/SeperatorItem.java b/app/src/main/java/org/openobservatory/ooniprobe/item/SeperatorItem.java index 70c971b22..c303f2b07 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/item/SeperatorItem.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/item/SeperatorItem.java @@ -9,6 +9,7 @@ import localhost.toolkit.widget.recyclerview.HeterogeneousRecyclerItem; +@Deprecated public class SeperatorItem extends HeterogeneousRecyclerItem { public SeperatorItem() { diff --git a/app/src/main/java/org/openobservatory/ooniprobe/item/TestsuiteItem.java b/app/src/main/java/org/openobservatory/ooniprobe/item/TestsuiteItem.java index 9eb8bf60b..a334a751b 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/item/TestsuiteItem.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/item/TestsuiteItem.java @@ -13,6 +13,7 @@ import org.openobservatory.ooniprobe.databinding.ItemTestsuiteBinding; import org.openobservatory.ooniprobe.test.suite.AbstractSuite; +@Deprecated public class TestsuiteItem extends HeterogeneousRecyclerItem { private final View.OnClickListener onClickListener; private final PreferenceManager preferenceManager; diff --git a/app/src/main/java/org/openobservatory/ooniprobe/test/suite/ExperimentalSuite.java b/app/src/main/java/org/openobservatory/ooniprobe/test/suite/ExperimentalSuite.java index 6f887bfe6..9d7086651 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/test/suite/ExperimentalSuite.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/test/suite/ExperimentalSuite.java @@ -42,6 +42,7 @@ public AbstractTest[] getTestList(@Nullable PreferenceManager pm) { if (pm == null || pm.isExperimentalOn()){ list.add(new Experimental("stunreachability")); list.add(new Experimental("dnscheck")); + list.add(new Experimental("riseupvpn")); list.add(new Experimental("echcheck")); if ((pm == null || pm.isLongRunningTestsInForeground()) || getAutoRun()){ list.add(new Experimental("torsf")); diff --git a/app/src/main/java/org/openobservatory/ooniprobe/test/test/RiseupVPN.java b/app/src/main/java/org/openobservatory/ooniprobe/test/test/RiseupVPN.java index 52601f065..2339afbbe 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/test/test/RiseupVPN.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/test/test/RiseupVPN.java @@ -17,19 +17,29 @@ import org.openobservatory.ooniprobe.model.jsonresult.JsonResult; import org.openobservatory.ooniprobe.model.settings.Settings; +/** + * Represents the RiseupVPN test. + * + * @deprecated This test has been demoted to experimental in Chore: Moved riseup vpn to experimental suite and correct tests. + * This test has been moved to experimental because it causes too many false positive. + */ public class RiseupVPN extends AbstractTest { public static final String NAME = "riseupvpn"; public RiseupVPN() { - super(NAME, R.string.Test_RiseupVPN_Fullname, R.drawable.test_riseupvpn, R.string.urlTestRvpn, 15); + // NOTE: this test has been demoted to experimental (see https://github.com/ooni/probe-android/pull/632) + // and such the icon resource `R.drawable.test_riseupvpn` is not displayed anymore. + super(NAME, R.string.Test_Experimental_Fullname, 0, R.string.urlTestRvpn, 15); } - @Override public void run(Context c, PreferenceManager pm, AppLogger logger, Gson gson, Result result, int index, AbstractTest.TestCallback testCallback) { + @Override + public void run(Context c, PreferenceManager pm, AppLogger logger, Gson gson, Result result, int index, AbstractTest.TestCallback testCallback) { Settings settings = new Settings(c, pm, isAutoRun()); - run(c, pm,logger, gson, settings, result, index, testCallback); + run(c, pm, logger, gson, settings, result, index, testCallback); } - @Override public void onEntry(Context c, PreferenceManager pm, @NonNull JsonResult json, Measurement measurement) { + @Override + public void onEntry(Context c, PreferenceManager pm, @NonNull JsonResult json, Measurement measurement) { super.onEntry(c, pm, json, measurement); //When json.test_keys.transport_status is null the test is failed so the result of is_anomaly doesn't matter. if (json.test_keys == null || json.test_keys.transport_status == null) { @@ -37,8 +47,8 @@ public RiseupVPN() { return; } boolean isTransportBlocked = false; - isTransportBlocked = MapUtility.getOrDefaultCompat(json.test_keys.transport_status, "openvpn", "ok").equals(BLOCKED) || - MapUtility.getOrDefaultCompat(json.test_keys.transport_status, "obfs4", "ok").equals(BLOCKED); + isTransportBlocked = MapUtility.getOrDefaultCompat(json.test_keys.transport_status, "openvpn", "ok").equals(BLOCKED) || + MapUtility.getOrDefaultCompat(json.test_keys.transport_status, "obfs4", "ok").equals(BLOCKED); measurement.is_anomaly = !json.test_keys.ca_cert_status || json.test_keys.api_failure != null || isTransportBlocked; } diff --git a/app/src/main/res/drawable/bottom_nav_color.xml b/app/src/main/res/drawable/bottom_nav_color.xml new file mode 100644 index 000000000..d92c80dac --- /dev/null +++ b/app/src/main/res/drawable/bottom_nav_color.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/card_boarder.xml b/app/src/main/res/drawable/card_boarder.xml new file mode 100644 index 000000000..6d1888227 --- /dev/null +++ b/app/src/main/res/drawable/card_boarder.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/outline_timer.xml b/app/src/main/res/drawable/outline_timer.xml new file mode 100644 index 000000000..c99a68557 --- /dev/null +++ b/app/src/main/res/drawable/outline_timer.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 5ee00e79f..e2e9030ae 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -22,7 +22,9 @@ android:layout_width="match_parent" app:itemRippleColor="?attr/colorOnSurface" android:layout_height="wrap_content" - android:background="?android:attr/colorBackground" + android:background="@color/bottom_navigation_background" + app:itemTextColor="@drawable/bottom_nav_color" + app:itemIconTint="@drawable/bottom_nav_color" app:elevation="8dp" app:menu="@menu/bottom_navigation"/> - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_dashboard.xml b/app/src/main/res/layout/fragment_dashboard.xml index f93195356..d90d20ad8 100644 --- a/app/src/main/res/layout/fragment_dashboard.xml +++ b/app/src/main/res/layout/fragment_dashboard.xml @@ -2,16 +2,19 @@ + android:orientation="vertical" + tools:context=".fragment.DashboardFragment"> @@ -21,6 +24,8 @@ android:scaleType="centerInside" android:layout_marginLeft="15dp" android:layout_marginStart="15dp" + android:layout_gravity="center_horizontal" + app:tint="@color/color_gray6" android:src="@drawable/ooniprobe_logo" /> @@ -37,29 +42,29 @@ + android:background="@color/color_white"/>