diff --git a/app/src/main/java/org/openobservatory/ooniprobe/activity/CustomWebsiteActivity.java b/app/src/main/java/org/openobservatory/ooniprobe/activity/CustomWebsiteActivity.java deleted file mode 100644 index 20cd47558..000000000 --- a/app/src/main/java/org/openobservatory/ooniprobe/activity/CustomWebsiteActivity.java +++ /dev/null @@ -1,103 +0,0 @@ -package org.openobservatory.ooniprobe.activity; - -import android.content.DialogInterface; -import android.os.Bundle; -import android.util.Patterns; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.ImageButton; - -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.LinearLayoutManager; - -import localhost.toolkit.app.fragment.ConfirmDialogFragment; - -import org.openobservatory.ooniprobe.R; -import org.openobservatory.ooniprobe.adapters.CustomWebsiteRecyclerViewAdapter; -import org.openobservatory.ooniprobe.common.PreferenceManager; -import org.openobservatory.ooniprobe.databinding.ActivityCustomwebsiteBinding; -import org.openobservatory.ooniprobe.model.database.Url; -import org.openobservatory.ooniprobe.test.suite.WebsitesSuite; - -import javax.inject.Inject; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class CustomWebsiteActivity extends AbstractActivity implements ConfirmDialogFragment.OnConfirmedListener { - @Inject - PreferenceManager preferenceManager; - private CustomWebsiteRecyclerViewAdapter adapter; - private ActivityCustomwebsiteBinding binding; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - getActivityComponent().inject(this); - binding = ActivityCustomwebsiteBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - setSupportActionBar(binding.toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - binding.bottomBar.setOnMenuItemClickListener(item -> { - List items = adapter.getItems(); - ArrayList urls = new ArrayList<>(items.size()); - for (String value : items) { - String sanitizedUrl = value.replaceAll("\\r\\n|\\r|\\n", " "); - //https://support.microsoft.com/en-us/help/208427/maximum-url-length-is-2-083-characters-in-internet-explorer - if (Patterns.WEB_URL.matcher(sanitizedUrl).matches() && sanitizedUrl.length() < 2084) - urls.add(Url.checkExistingUrl(sanitizedUrl).toString()); - } - WebsitesSuite suite = new WebsitesSuite(); - suite.getTestList(preferenceManager)[0].setInputs(urls); - - RunningActivity.runAsForegroundService(CustomWebsiteActivity.this, suite.asArray(), this::finish,preferenceManager); - return true; - }); - binding.add.setOnClickListener(v -> add()); - LinearLayoutManager layoutManager = new LinearLayoutManager(this); - binding.urlContainer.setLayoutManager(layoutManager); - adapter = new CustomWebsiteRecyclerViewAdapter(input -> { - binding.bottomBar.setTitle(getString(R.string.OONIRun_URLs, Integer.toString(adapter.getItemCount()))); - }); - binding.urlContainer.setAdapter(adapter); - add(); - } - - @Override - public void onBackPressed() { - String base = getString(R.string.http); - boolean edited = adapter.getItemCount() > 0 && !adapter.getItems().get(0).equals(base); - - if (edited) - new ConfirmDialogFragment.Builder().withMessage(getString(R.string.Modal_CustomURL_NotSaved)).build().show(getSupportFragmentManager(), null); - else super.onBackPressed(); - } - - @Override - public boolean onSupportNavigateUp() { - onBackPressed(); - return true; - } - - void add() { - adapter.addAll(Collections.singletonList(getString(R.string.http))); - binding.bottomBar.setTitle(getString(R.string.OONIRun_URLs, Integer.toString(adapter.getItemCount()))); - adapter.notifyDataSetChanged(); - this.scrollToBottom(); - } - - void scrollToBottom() { - binding.urlContainer.scrollToPosition(adapter.getItemCount() - 1); - binding.urlsList.post(() -> binding.urlsList.fullScroll(View.FOCUS_DOWN)); - } - - @Override - public void onConfirmation(Serializable serializable, int i) { - if (i == DialogInterface.BUTTON_POSITIVE) super.onBackPressed(); - } -} diff --git a/app/src/main/java/org/openobservatory/ooniprobe/activity/CustomWebsiteActivity.kt b/app/src/main/java/org/openobservatory/ooniprobe/activity/CustomWebsiteActivity.kt new file mode 100644 index 000000000..eb1ec4a5e --- /dev/null +++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/CustomWebsiteActivity.kt @@ -0,0 +1,106 @@ +package org.openobservatory.ooniprobe.activity + +import android.content.DialogInterface +import android.os.Bundle +import android.os.Parcelable +import android.util.Patterns +import android.view.MenuItem +import android.view.View +import androidx.recyclerview.widget.LinearLayoutManager +import org.openobservatory.ooniprobe.fragment.ConfirmDialogFragment +import org.openobservatory.ooniprobe.R +import org.openobservatory.ooniprobe.adapters.CustomWebsiteRecyclerViewAdapter +import org.openobservatory.ooniprobe.adapters.ItemRemovedListener +import org.openobservatory.ooniprobe.common.PreferenceManager +import org.openobservatory.ooniprobe.databinding.ActivityCustomwebsiteBinding +import org.openobservatory.ooniprobe.model.database.Url +import org.openobservatory.ooniprobe.test.suite.WebsitesSuite +import java.io.Serializable +import javax.inject.Inject + +class CustomWebsiteActivity : AbstractActivity(), ConfirmDialogFragment.OnClickListener { + @Inject + lateinit var preferenceManager: PreferenceManager + private lateinit var adapter: CustomWebsiteRecyclerViewAdapter + private lateinit var binding: ActivityCustomwebsiteBinding + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + activityComponent.inject(this) + binding = ActivityCustomwebsiteBinding.inflate( + layoutInflater + ) + setContentView(binding.root) + setSupportActionBar(binding.toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + val layoutManager = LinearLayoutManager(this) + binding.urlContainer.layoutManager = layoutManager + adapter = CustomWebsiteRecyclerViewAdapter(object : ItemRemovedListener { + override fun onItemRemoved(position: Int) { + binding.bottomBar.title = getString( + R.string.OONIRun_URLs, adapter.itemCount.toString() + ) + } + }) + binding.bottomBar.setOnMenuItemClickListener { item: MenuItem? -> + val items = adapter.getItems() + val urls = ArrayList(items.size) + for (value in items) { + val sanitizedUrl = value.replace("\\r\\n|\\r|\\n".toRegex(), " ") + //https://support.microsoft.com/en-us/help/208427/maximum-url-length-is-2-083-characters-in-internet-explorer + if (Patterns.WEB_URL.matcher(sanitizedUrl) + .matches() && sanitizedUrl.length < 2084 + ) urls.add( + Url.checkExistingUrl(sanitizedUrl).toString() + ) + } + val suite = WebsitesSuite() + suite.getTestList(preferenceManager)[0].inputs = urls + RunningActivity.runAsForegroundService( + this@CustomWebsiteActivity, suite.asArray(), { finish() }, preferenceManager + ) + true + } + binding.add.setOnClickListener { add() } + + binding.urlContainer.adapter = adapter + add() + } + + override fun onBackPressed() { + val base = getString(R.string.http) + val edited = adapter.itemCount > 0 && adapter.getItems()[0] != base + if (edited) { + ConfirmDialogFragment( + title = "Are you sure?", + message = "Your URLs will not be saved when you leave this screen. Are you sure you want to do that?", + ).show(supportFragmentManager, null) + } else { + super.onBackPressed() + } + } + + override fun onSupportNavigateUp(): Boolean { + onBackPressed() + return true + } + + fun add() { + adapter.addAll(listOf(getString(R.string.http))) + binding.bottomBar.title = getString( + R.string.OONIRun_URLs, adapter.itemCount.toString() + ) + adapter.notifyDataSetChanged() + scrollToBottom() + } + + private fun scrollToBottom() { + binding.urlContainer.scrollToPosition(adapter.itemCount - 1) + binding.urlsList.post { binding.urlsList.fullScroll(View.FOCUS_DOWN) } + } + + override fun onConfirmDialogClick( + serializable: Serializable?, parcelable: Parcelable?, buttonClicked: Int + ) { + if (buttonClicked == DialogInterface.BUTTON_POSITIVE) super.onBackPressed() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/openobservatory/ooniprobe/fragment/ConfirmDialogFragment.kt b/app/src/main/java/org/openobservatory/ooniprobe/fragment/ConfirmDialogFragment.kt new file mode 100644 index 000000000..115986a6c --- /dev/null +++ b/app/src/main/java/org/openobservatory/ooniprobe/fragment/ConfirmDialogFragment.kt @@ -0,0 +1,104 @@ +package org.openobservatory.ooniprobe.fragment + +import android.content.DialogInterface +import android.os.Bundle +import android.os.Parcelable +import androidx.annotation.IntDef +import androidx.core.text.HtmlCompat +import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import java.io.Serializable + +class ConfirmDialogFragment( + serializable: Serializable? = null, + parcelable: Parcelable? = null, + title: String? = null, + message: String? = null, + positiveButton: String? = null, + negativeButton: String? = null, + neutralButton: String? = null +) : DialogFragment(), DialogInterface.OnClickListener { + companion object { + private const val MESSAGE = "MESSAGE" + private const val TITLE = "TITLE" + private const val POSITIVE_BUTTON = "POSITIVE_BUTTON" + private const val NEGATIVE_BUTTON = "NEGATIVE_BUTTON" + private const val NEUTRAL_BUTTON = "NEUTRAL_BUTTON" + private const val SERIALIZABLE = "SERIALIZABLE" + private const val PARCELABLE = "PARCELABLE" + } + + private val listener: OnClickListener + get() = parentFragment as? OnClickListener ?: requireActivity() as OnClickListener + + init { + Bundle().apply { + title?.let { putString(TITLE, it) } + message?.let { putString(MESSAGE, it) } + positiveButton?.let { putString(POSITIVE_BUTTON, it) } + negativeButton?.let { putString(NEGATIVE_BUTTON, it) } + neutralButton?.let { putString(NEUTRAL_BUTTON, it) } + serializable?.let { putSerializable(SERIALIZABLE, it) } + parcelable?.let { putParcelable(PARCELABLE, it) } + }.let { + if (!it.isEmpty) arguments = it + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + isCancelable = false + } + + override fun onCreateDialog(savedInstanceState: Bundle?) = + MaterialAlertDialogBuilder(requireContext()).apply { + setTitle(requireArguments().getString(TITLE)) + if (requireArguments().containsKey(MESSAGE)) setMessage( + HtmlCompat.fromHtml( + requireArguments().getString(MESSAGE)!!, + HtmlCompat.FROM_HTML_MODE_COMPACT + ) + ) + setPositiveButton( + requireArguments().getString( + POSITIVE_BUTTON, + getString(android.R.string.ok) + ), this@ConfirmDialogFragment + ) + setNegativeButton( + requireArguments().getString( + NEGATIVE_BUTTON, + getString(android.R.string.cancel) + ), this@ConfirmDialogFragment + ) + if (requireArguments().containsKey(NEUTRAL_BUTTON)) setNeutralButton( + requireArguments().getString( + NEUTRAL_BUTTON + ), this@ConfirmDialogFragment + ) + }.create() + + override fun onClick(dialog: DialogInterface, which: Int) { + listener.onConfirmDialogClick( + requireArguments().getSerializable(SERIALIZABLE), + requireArguments().getParcelable(PARCELABLE), + which + ) + } + + interface OnClickListener { + fun onConfirmDialogClick( + serializable: Serializable?, + parcelable: Parcelable?, + @ConfirmDialogButton buttonClicked: Int + ) + } +} + +@Retention(AnnotationRetention.SOURCE) +@IntDef( + DialogInterface.BUTTON_POSITIVE, + DialogInterface.BUTTON_NEGATIVE, + DialogInterface.BUTTON_NEUTRAL +) +internal annotation class ConfirmDialogButton \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index d6e812188..1fc5b1ae2 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -23,6 +23,7 @@ @color/color_blue6 @color/color_blue4 @font/firasans + @color/color_white