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 9d1b79018..296232913 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/activity/OverviewActivity.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/OverviewActivity.java @@ -12,6 +12,7 @@ import org.openobservatory.ooniprobe.R; import org.openobservatory.ooniprobe.common.PreferenceManager; +import org.openobservatory.ooniprobe.common.ReadMorePlugin; import org.openobservatory.ooniprobe.databinding.ActivityOverviewBinding; import org.openobservatory.ooniprobe.model.database.Result; import org.openobservatory.ooniprobe.test.suite.AbstractSuite; @@ -50,11 +51,11 @@ public static Intent newIntent(Context context, AbstractSuite testSuite) { setTitle(testSuite.getTitle()); binding.icon.setImageResource(testSuite.getIcon()); binding.customUrl.setVisibility(testSuite.getName().equals(WebsitesSuite.NAME) ? View.VISIBLE : View.GONE); - if(testSuite.isTestEmpty(preferenceManager)){ - binding.run.setAlpha(0.5F); - binding.run.setEnabled(false); - } - Markwon markwon = Markwon.builder(this).build(); + Markwon markwon = Markwon.builder(this) + .usePlugin(new ReadMorePlugin( + getString(R.string.OONIRun_ReadMore), + getString(R.string.OONIRun_ReadLess)) + ).build(); if (testSuite.getName().equals(ExperimentalSuite.NAME)) { String experimentalLinks = "\n\n* [STUN Reachability](https://github.com/ooni/spec/blob/master/nettests/ts-025-stun-reachability.md)" + @@ -63,11 +64,13 @@ public static Intent newIntent(Context context, AbstractSuite testSuite) { "\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)); markwon.setMarkdown(binding.desc, getString(testSuite.getDesc1(), experimentalLinks)); - if (TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL) - binding.desc.setTextDirection(View.TEXT_DIRECTION_RTL); + if (TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL) { + binding.desc.setTextDirection(View.TEXT_DIRECTION_RTL); + } } - else - markwon.setMarkdown(binding.desc, getString(testSuite.getDesc1())); + else { + markwon.setMarkdown(binding.desc, getString(testSuite.getDesc1())); + } Result lastResult = Result.getLastResult(testSuite.getName()); if (lastResult == null) binding.lastTime.setText(R.string.Dashboard_Overview_LastRun_Never); @@ -78,7 +81,6 @@ public static Intent newIntent(Context context, AbstractSuite testSuite) { } private void setUpOnCLickListeners() { - binding.run.setOnClickListener(view -> onRunClick()); binding.customUrl.setOnClickListener(view -> customUrlClick()); } @@ -95,12 +97,6 @@ public boolean onSupportNavigateUp() { return true; } - void onRunClick() { - if(!testSuite.isTestEmpty(preferenceManager)){ - RunningActivity.runAsForegroundService(this, testSuite.asArray(), this::bindTestService, preferenceManager); - } - } - void customUrlClick() { startActivity(new Intent(this, CustomWebsiteActivity.class)); } diff --git a/app/src/main/java/org/openobservatory/ooniprobe/common/ReadMorePlugin.kt b/app/src/main/java/org/openobservatory/ooniprobe/common/ReadMorePlugin.kt new file mode 100644 index 000000000..e71dfefb2 --- /dev/null +++ b/app/src/main/java/org/openobservatory/ooniprobe/common/ReadMorePlugin.kt @@ -0,0 +1,126 @@ +package org.openobservatory.ooniprobe.common + +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.ClickableSpan +import android.text.style.ReplacementSpan +import android.view.View +import android.widget.TextView +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() { + private val maxLength = 150 + + override fun afterSetText(textView: TextView) { + val text = textView.text + if (text.length < maxLength) { + // everything is OK, no need to ellipsize) + return + } + val breakAt = breakTextAt(text, 0, maxLength) + val cs = createCollapsedString(text, 0, breakAt) + textView.text = cs + } + + private fun createCollapsedString(text: CharSequence, start: Int, end: Int): CharSequence { + val builder = SpannableStringBuilder(text, start, end) + val spans = builder.getSpans( + 0, builder.length, + ReplacementSpan::class.java + ) + if (spans != null) { + for (span in spans) { + builder.removeSpan(span) + } + } + + + trim(builder) + val fullText = createFullText(text, builder) + builder.append(" ...") + val length = builder.length + builder.append("\n\n") + builder.append(labelMore) + builder.setSpan(object : ClickableSpan() { + override fun onClick(widget: View) { + (widget as TextView).text = fullText + } + }, length, builder.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + return builder + } + + private fun createFullText(text: CharSequence, collapsedText: CharSequence): CharSequence { + val fullText: CharSequence + 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 + } + + companion object { + private fun trim(builder: SpannableStringBuilder) { + + // NB! tables use `\u00a0` (non breaking space) which is not reported as white-space + var c: Char + run { + var i = 0 + val length = builder.length + while (i < length) { + c = builder[i] + if (!Character.isWhitespace(c) && c != '\u00a0') { + if (i > 0) { + builder.replace(0, i, "") + } + break + } + i++ + } + } + for (i in builder.length - 1 downTo 0) { + c = builder[i] + if (!Character.isWhitespace(c) && c != '\u00a0') { + if (i < builder.length - 1) { + builder.replace(i, builder.length, "") + } + break + } + } + } + + // depending on your locale these can be different + // There is a BreakIterator in Android, but it is not reliable, still theoretically + // it should work better than hand-written and hardcoded rules + private fun breakTextAt(text: CharSequence, start: Int, max: Int): Int { + var last = start + + // no need to check for _start_ (anyway will be ignored) + for (i in start + max - 1 downTo start + 1) { + val c = text[i] + if (Character.isWhitespace(c) || c == '.' || c == ',' || c == '!' || c == '?') { + // include this special character + last = i - 1 + break + } + } + return if (last <= start) { + // when used in subSequence last index is exclusive, + // so given max=150 would result in 0-149 subSequence + start + max + } else last + } + } +} diff --git a/app/src/main/res/layout/activity_overview.xml b/app/src/main/res/layout/activity_overview.xml index e4c1b1665..ea5263718 100644 --- a/app/src/main/res/layout/activity_overview.xml +++ b/app/src/main/res/layout/activity_overview.xml @@ -1,127 +1,125 @@ - + tools:context=".activity.OverviewActivity"> + android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> - - - - - - - - - - - - + app:contentScrim="?attr/colorPrimary" + app:titleEnabled="false" + app:layout_scrollFlags="scroll|snap|exitUntilCollapsed"> - - + + + + - - + + + + + + + + + + + + + + + + +