From 997840c4f21ee92cc71d15d37eb8f6c79e831e77 Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Fri, 24 Nov 2023 15:08:01 +0100 Subject: [PATCH 1/2] [New Designs] Update OONI Run v1 UI to v2 (#626) Fixes https://github.com/ooni/probe/issues/2596 ## Proposed Changes - Update designs for Run v1 components to match new designs - Add `SrtingListRecyclerViewAdapter` to display List items - Convert `OoniRunActivity` to Kotlin | Light | Dark| |--|--| |.|.| |![web_connectivity_light](https://github.com/ooni/probe-android/assets/17911892/6f4b5896-c85a-4cd9-8fb2-0f6eb693f38e)|![web_connectivity_dark](https://github.com/ooni/probe-android/assets/17911892/6a30f612-1b42-4e43-b8e5-0f4a16edf86b)| |![web_connectivity_light_long_list](https://github.com/ooni/probe-android/assets/17911892/9d871137-fd3b-4b23-9c3a-700d30d43f6b)| ![web_connectivity_dark_long_list](https://github.com/ooni/probe-android/assets/17911892/82c0d8fa-c0b6-4ed1-83ef-607c4bf952c4)| |![web_connectivity_light_short_list](https://github.com/ooni/probe-android/assets/17911892/42e96ad5-318b-4220-beae-d5d8932da985)|![web_connectivity_dark_short_list](https://github.com/ooni/probe-android/assets/17911892/7aafa54c-5498-40d2-8cbb-f00173e001ee)| |![http_header_field_manipulation_light](https://github.com/ooni/probe-android/assets/17911892/c79e07be-9044-4cf9-9272-b4be0a6f6d4f)|![http_header_field_manipulation_dark](https://github.com/ooni/probe-android/assets/17911892/d4cb96ed-c6a5-4e55-9a9b-7f47059fde3f)| |![invalid_light](https://github.com/ooni/probe-android/assets/17911892/05516448-7094-484d-9ccb-f118f489e546)|![invalid_dark](https://github.com/ooni/probe-android/assets/17911892/bc5351fa-770f-43d8-aa65-5265ba4c1cad)| |![outdated_light](https://github.com/ooni/probe-android/assets/17911892/e475971e-23c0-4f37-9ee1-598ce91bb611)|![outdated_dark](https://github.com/ooni/probe-android/assets/17911892/a94e5849-3eeb-4ad1-a393-4591d5d0f824)| |![whatsapp_light](https://github.com/ooni/probe-android/assets/17911892/3f9b5760-a259-4838-a28c-1d8db1e8ceed)|![whatsapp_dark](https://github.com/ooni/probe-android/assets/17911892/2313ca90-b5ab-4bb1-ac13-7cc763bc06eb)? --- app/src/main/AndroidManifest.xml | 2 +- .../ooniprobe/activity/OoniRunActivity.java | 173 ---------------- .../ooniprobe/activity/OoniRunActivity.kt | 188 ++++++++++++++++++ .../adapters/StringListRecyclerViewAdapter.kt | 59 ++++++ app/src/main/res/layout/activity_oonirun.xml | 147 +++++++------- app/src/main/res/layout/item_run_v1.xml | 11 + app/src/main/res/values/dimens.xml | 5 + app/src/main/res/values/styles.xml | 1 + 8 files changed, 341 insertions(+), 245 deletions(-) create mode 100644 app/src/main/java/org/openobservatory/ooniprobe/activity/OoniRunActivity.kt create mode 100644 app/src/main/java/org/openobservatory/ooniprobe/adapters/StringListRecyclerViewAdapter.kt create mode 100644 app/src/main/res/layout/item_run_v1.xml create mode 100644 app/src/main/res/values/dimens.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1b717a3e5..a3e217c42 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -108,7 +108,7 @@ items; - private HeterogeneousRecyclerAdapter adapter; - - @Inject - PreferenceManager preferenceManager; - - @Inject - VersionCompare versionCompare; - - @Inject - GetTestSuite getSuite; - - @Inject - Gson gson; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - getActivityComponent().inject(this); - binding = ActivityOonirunBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - setSupportActionBar(binding.toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setDisplayShowHomeEnabled(true); - LinearLayoutManager layoutManager = new LinearLayoutManager(this); - binding.recycler.setLayoutManager(layoutManager); - binding.recycler.addItemDecoration(new DividerItemDecoration(this, layoutManager.getOrientation())); - items = new ArrayList<>(); - adapter = new HeterogeneousRecyclerAdapter<>(this, items); - binding.recycler.setAdapter(adapter); - manageIntent(getIntent()); - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - manageIntent(intent); - } - - private void manageIntent(Intent intent) { - if (isTestRunning()) { - Toast.makeText(this, getString(R.string.OONIRun_TestRunningError), Toast.LENGTH_LONG).show(); - finish(); - } - else if (Intent.ACTION_VIEW.equals(intent.getAction())) { - Uri uri = intent.getData(); - String mv = uri == null ? null : uri.getQueryParameter("mv"); - String tn = uri == null ? null : uri.getQueryParameter("tn"); - String ta = uri == null ? null : uri.getQueryParameter("ta"); - loadScreen(mv, tn, ta); - } - else if (Intent.ACTION_SEND.equals(intent.getAction())) { - String url = intent.getStringExtra(Intent.EXTRA_TEXT); - if (url != null && Patterns.WEB_URL.matcher(url).matches()) { - List urls = Collections.singletonList(url); - AbstractSuite suite = getSuite.get("web_connectivity", urls); - if (suite != null) { - loadSuite(suite, urls); - } else { - loadInvalidAttributes(); - } - } else { - loadInvalidAttributes(); - } - } - } - - private void loadScreen(String mv, String tn, String ta){ - String[] split = BuildConfig.VERSION_NAME.split("-"); - String version_name = split[0]; - if (mv != null && tn != null) { - if (versionCompare.compare(version_name, mv) >= 0) { - try { - Attribute attribute = gson.fromJson(ta, Attribute.class); - List urls = (attribute!=null && attribute.urls != null) ? attribute.urls : null; - AbstractSuite suite = getSuite.get(tn, urls); - if (suite != null) { - loadSuite(suite, urls); - } else { - loadInvalidAttributes(); - } - } catch (Exception e) { - loadInvalidAttributes(); - } - } else { - loadOutOfDate(); - } - } else { - loadInvalidAttributes(); - } - } - - private void loadOutOfDate() { - binding.title.setText(R.string.OONIRun_OONIProbeOutOfDate); - binding.desc.setText(R.string.OONIRun_OONIProbeNewerVersion); - binding.run.setText(R.string.OONIRun_Update); - binding.icon.setImageResource(R.drawable.update); - binding.iconBig.setImageResource(R.drawable.update); - binding.iconBig.setVisibility(View.VISIBLE); - binding.run.setOnClickListener(v -> { - startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + getPackageName()))); - finish(); - }); - } - - private void loadSuite(AbstractSuite suite, List urls) { - binding.icon.setImageResource(suite.getIcon()); - binding.title.setText(suite.getTestList(preferenceManager)[0].getLabelResId()); - binding.desc.setText(getString(R.string.OONIRun_YouAreAboutToRun)); - if (urls != null) { - for (String url : urls) { - if (URLUtil.isValidUrl(url)) - items.add(new TextItem(url)); - } - adapter.notifyTypesChanged(); - binding.iconBig.setVisibility(View.GONE); - } else { - binding.iconBig.setImageResource(suite.getIcon()); - binding.iconBig.setVisibility(View.VISIBLE); - } - binding.run.setOnClickListener(v -> { - - RunningActivity.runAsForegroundService(OoniRunActivity.this, suite.asArray(),this::finish, preferenceManager); - - }); - } - - private void loadInvalidAttributes() { - binding.title.setText(R.string.OONIRun_InvalidParameter); - binding.desc.setText(R.string.OONIRun_InvalidParameter_Msg); - binding.run.setText(R.string.OONIRun_Close); - binding.icon.setImageResource(R.drawable.question_mark); - binding.iconBig.setImageResource(R.drawable.question_mark); - binding.iconBig.setVisibility(View.VISIBLE); - binding.run.setOnClickListener(v -> finish()); - } -} diff --git a/app/src/main/java/org/openobservatory/ooniprobe/activity/OoniRunActivity.kt b/app/src/main/java/org/openobservatory/ooniprobe/activity/OoniRunActivity.kt new file mode 100644 index 000000000..8f0fba769 --- /dev/null +++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/OoniRunActivity.kt @@ -0,0 +1,188 @@ +package org.openobservatory.ooniprobe.activity + +import android.content.Intent +import android.content.res.ColorStateList +import android.graphics.Color +import android.net.Uri +import android.os.Bundle +import android.util.Patterns +import android.webkit.URLUtil +import android.widget.Toast +import androidx.core.graphics.ColorUtils +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.gson.Gson +import org.openobservatory.ooniprobe.BuildConfig +import org.openobservatory.ooniprobe.R +import org.openobservatory.ooniprobe.adapters.StringListRecyclerViewAdapter +import org.openobservatory.ooniprobe.common.PreferenceManager +import org.openobservatory.ooniprobe.databinding.ActivityOonirunBinding +import org.openobservatory.ooniprobe.domain.GetTestSuite +import org.openobservatory.ooniprobe.domain.VersionCompare +import org.openobservatory.ooniprobe.domain.models.Attribute +import org.openobservatory.ooniprobe.test.suite.AbstractSuite +import javax.inject.Inject + +class OoniRunActivity : AbstractActivity() { + lateinit var binding: ActivityOonirunBinding + + @Inject + lateinit var preferenceManager: PreferenceManager + + @Inject + lateinit var versionCompare: VersionCompare + + @Inject + lateinit var getSuite: GetTestSuite + + @Inject + lateinit var gson: Gson + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + activityComponent.inject(this) + binding = ActivityOonirunBinding.inflate( + layoutInflater + ) + setContentView(binding.root) + setSupportActionBar(binding.toolbar) + supportActionBar?.apply { + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + manageIntent(intent) + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + manageIntent(intent) + } + + private fun manageIntent(intent: Intent) { + if (isTestRunning) { + Toast.makeText(this, getString(R.string.OONIRun_TestRunningError), Toast.LENGTH_LONG) + .show() + finish() + } else if (Intent.ACTION_VIEW == intent.action) { + val uri = intent.data + val mv = uri?.getQueryParameter("mv") + val tn = uri?.getQueryParameter("tn") + val ta = uri?.getQueryParameter("ta") + loadScreen(mv, tn, ta) + } else if (Intent.ACTION_SEND == intent.action) { + val url = intent.getStringExtra(Intent.EXTRA_TEXT) + if (url != null && Patterns.WEB_URL.matcher(url).matches()) { + val urls = listOf(url) + val suite = getSuite["web_connectivity", urls] + if (suite != null) { + loadSuite(suite, urls) + } else { + loadInvalidAttributes() + } + } else { + loadInvalidAttributes() + } + } + } + + private fun loadScreen(mv: String?, tn: String?, ta: String?) { + val split = BuildConfig.VERSION_NAME.split("-".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + val versionName = split.first() + if (mv != null && tn != null) { + if (versionCompare.compare(versionName, mv) >= 0) { + try { + val attribute = gson.fromJson(ta, Attribute::class.java) + val urls = attribute?.urls + val suite = getSuite[tn, urls] + if (suite != null) { + loadSuite(suite, urls) + } else { + loadInvalidAttributes() + } + } catch (e: Exception) { + loadInvalidAttributes() + } + } else { + loadOutOfDate() + } + } else { + loadInvalidAttributes() + } + } + + private fun loadOutOfDate() { + setThemeColor(resources.getColor(R.color.color_gray4)) + setTextColor(Color.BLACK) + binding.title.setText(R.string.OONIRun_OONIProbeOutOfDate) + binding.desc.setText(R.string.OONIRun_OONIProbeNewerVersion) + binding.icon.setImageResource(R.drawable.update) + binding.run.apply { + setText(R.string.OONIRun_Update) + setOnClickListener { + startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse("https://play.google.com/store/apps/details?id=$packageName") + ) + ) + finish() + } + } + } + + private fun loadSuite(suite: AbstractSuite, urls: List?) { + val items = ArrayList() + binding.icon.setImageResource(suite.icon) + binding.title.setText(suite.getTestList(preferenceManager).first().labelResId) + binding.desc.text = getString(R.string.OONIRun_YouAreAboutToRun) + urls?.let { urls -> + urls.filterTo(items) { URLUtil.isValidUrl(it) } + binding.recycler.apply { + layoutManager = LinearLayoutManager(this@OoniRunActivity) + adapter = StringListRecyclerViewAdapter(items) + } + } + setThemeColor(resources.getColor(suite.color)) + binding.run.setOnClickListener { + RunningActivity.runAsForegroundService( + this@OoniRunActivity, suite.asArray(), { finish() }, preferenceManager + ) + } + } + + private fun setThemeColor(color: Int) { + val window = window + window.statusBarColor = color + binding.appbarLayout.setBackgroundColor(color) + when { + ColorUtils.calculateLuminance(color) > 0.5 -> { + setTextColor(Color.WHITE) + } + + else -> { + setTextColor(Color.WHITE) + } + } + } + + private fun setTextColor(color: Int) { + binding.title.setTextColor(color) + binding.icon.setColorFilter(color) + binding.desc.setTextColor(color) + binding.run.apply { + setTextColor(color) + strokeColor = ColorStateList.valueOf(color) + } + } + + private fun loadInvalidAttributes() { + setThemeColor(resources.getColor(R.color.color_gray4)) + setTextColor(Color.BLACK) + binding.title.setText(R.string.OONIRun_InvalidParameter) + binding.desc.setText(R.string.OONIRun_InvalidParameter_Msg) + binding.icon.setImageResource(R.drawable.question_mark) + binding.run.apply { + setText(R.string.OONIRun_Close) + setOnClickListener { finish() } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/openobservatory/ooniprobe/adapters/StringListRecyclerViewAdapter.kt b/app/src/main/java/org/openobservatory/ooniprobe/adapters/StringListRecyclerViewAdapter.kt new file mode 100644 index 000000000..41c8d16de --- /dev/null +++ b/app/src/main/java/org/openobservatory/ooniprobe/adapters/StringListRecyclerViewAdapter.kt @@ -0,0 +1,59 @@ +package org.openobservatory.ooniprobe.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.Toast +import androidx.recyclerview.widget.RecyclerView +import org.openobservatory.ooniprobe.databinding.ItemRunV1Binding + +/** + * Adapter for displaying a List. + */ +class StringListRecyclerViewAdapter(private val items: List) : + RecyclerView.Adapter() { + + /** + * Creates new views (invoked by the layout manager). + */ + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + ItemRunV1Binding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + ) + } + + /** + * Replaces the contents of a view (invoked by the layout manager). + */ + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.binding.textView.apply { + text = items[position] + /** + * INFO(aanorbel): The item view is set to single line in the layout file. + * This is to prevent long URLs from displaying on multiple lines. + * The code below will show the full URL in a toast when the user clicks on it. + */ + setOnClickListener { + Toast.makeText( + holder.binding.textView.context, + items[position], + Toast.LENGTH_SHORT, + ).show() + } + } + } + + + /** + * Returns the total number of items in the data set held by the adapter. + */ + override fun getItemCount(): Int { + return items.size + } + + /** + * Provides a reference to the views for each data item. + */ + class ViewHolder(val binding: ItemRunV1Binding) : RecyclerView.ViewHolder(binding.root) +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_oonirun.xml b/app/src/main/res/layout/activity_oonirun.xml index fee9299a2..8155c229f 100644 --- a/app/src/main/res/layout/activity_oonirun.xml +++ b/app/src/main/res/layout/activity_oonirun.xml @@ -1,84 +1,89 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".activity.OoniRunActivity"> - + - + - + - + - + - - + - - - + + - + + + - + -