diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 03676c9e1..1b0eacd1a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -115,7 +115,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/java/org/openobservatory/ooniprobe/common/ReadMorePlugin.kt b/app/src/main/java/org/openobservatory/ooniprobe/common/ReadMorePlugin.kt index a792cfb4d..9cef24f39 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/common/ReadMorePlugin.kt +++ b/app/src/main/java/org/openobservatory/ooniprobe/common/ReadMorePlugin.kt @@ -13,7 +13,8 @@ import io.noties.markwon.AbstractMarkwonPlugin * Read more plugin based on text length. * @see ReadMorePluginSample */ -class ReadMorePlugin(private val labelMore:String, private val labelLess:String) : AbstractMarkwonPlugin() { +class ReadMorePlugin(private val labelMore: String, private val labelLess: String) : + AbstractMarkwonPlugin() { private val maxLength = 150 override fun afterSetText(textView: TextView) { @@ -29,34 +30,23 @@ class ReadMorePlugin(private val labelMore:String, private val labelLess:String) private fun createCollapsedString(text: CharSequence, start: Int, end: Int): CharSequence { val builder = SpannableStringBuilder(text, start, end) - - // NB! each table row is represented as a space character and new-line (so length=2) no - // matter how many characters are inside table cells - - // we can _clean_ this builder, for example remove all dynamic content (like images and tables, - // but keep them in full/expanded version) - if (true) { - // it is an implementation detail but _mostly_ dynamic content is implemented as - // ReplacementSpans - val spans = builder.getSpans( - 0, builder.length, - ReplacementSpan::class.java - ) - if (spans != null) { - for (span in spans) { - builder.removeSpan(span) - } + val spans = builder.getSpans( + 0, builder.length, + ReplacementSpan::class.java + ) + if (spans != null) { + for (span in spans) { + builder.removeSpan(span) } - - // NB! if there will be a table in _preview_ (collapsed) then each row will be represented as a - // space and new-line - trim(builder) } + + + trim(builder) val fullText = createFullText(text, builder) builder.append(" ...") val length = builder.length - builder.append("\n\n") - builder.append(labelMore) + builder.append("\n\n") + builder.append(labelMore) builder.setSpan(object : ClickableSpan() { override fun onClick(widget: View) { (widget as TextView).text = fullText @@ -70,22 +60,18 @@ class ReadMorePlugin(private val labelMore:String, private val labelLess:String) // for example it can be kept as-is and have no `collapse` functionality (once expanded cannot collapse) // or can contain collapse feature val fullText: CharSequence - if (true) { - // for example, let's allow collapsing - val builder = SpannableStringBuilder(text) - builder.append(' ') - val length = builder.length - builder.append("\n\n") - builder.append(labelLess) - builder.setSpan(object : ClickableSpan() { - override fun onClick(widget: View) { - (widget as TextView).text = collapsedText - } - }, length, builder.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - fullText = builder - } else { - fullText = text - } + val builder = SpannableStringBuilder(text) + builder.append(' ') + val length = builder.length + builder.append("\n\n") + builder.append(labelLess) + builder.setSpan(object : ClickableSpan() { + override fun onClick(widget: View) { + (widget as TextView).text = collapsedText + } + }, length, builder.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + fullText = builder + return fullText } 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"> - + - + - + - + - + - - + - - - + + - + + + - + -