Skip to content

Commit

Permalink
[New Designs] Update Chose websites view (#630)
Browse files Browse the repository at this point in the history
Fixes ooni/probe#2592

## Proposed Changes

  - Updated `CustomWebsiteActivity` to use new theme.
  - Change List item builder to use  `RecyclerView`.


Tasks

- [x] Implement designs on light theme.
- [x] Convert list item builder to  use  `RecyclerView`.
- [x] Fix dark theme
- [x] Implement modal properly

| Light | Dark|
|-|-|

|![Screenshot_20231104_110235](https://github.com/ooni/probe-android/assets/17911892/b4c8dca7-2c6a-4d55-9df4-f194a67b98a6))|
![Screenshot_20231104_110254](https://github.com/ooni/probe-android/assets/17911892/60ade191-40bb-4b0b-a5fa-aa93540bd112)
|

|![Screenshot_20231104_110336](https://github.com/ooni/probe-android/assets/17911892/2df8c97a-23ff-4771-970b-59044de00386)|
![Screenshot_20231104_110407](https://github.com/ooni/probe-android/assets/17911892/291af536-72e3-4774-a5ca-8301334543a0)
|
|
![Screenshot_20231104_110429](https://github.com/ooni/probe-android/assets/17911892/a7423372-5acd-45b8-aa0a-ae2ef4e4b62c)
|
![Screenshot_20231104_110441](https://github.com/ooni/probe-android/assets/17911892/15047e19-9c3e-4cbe-a1c1-ceaa8783da8a)
|

---------

Co-authored-by: Simone Basso <bassosimone@gmail.com>
  • Loading branch information
aanorbel and bassosimone authored Dec 5, 2023
1 parent 5aac551 commit 706eef1
Show file tree
Hide file tree
Showing 15 changed files with 488 additions and 153 deletions.
4 changes: 2 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,11 @@
</intent-filter>
</activity>
<activity
android:name=".activity.CustomWebsiteActivity"
android:name=".activity.customwebsites.CustomWebsiteActivity"
android:label="@string/Settings_Websites_CustomURL_Title"
android:exported="false"
android:parentActivityName=".activity.PreferenceActivity"
android:screenOrientation="userPortrait">
android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar.App.NoActionBar">
<intent-filter>
<action android:name="${applicationId}.activity.CustomWebsiteActivity" />

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import androidx.core.view.ViewCompat;

import org.openobservatory.ooniprobe.R;
import org.openobservatory.ooniprobe.activity.customwebsites.CustomWebsiteActivity;
import org.openobservatory.ooniprobe.common.PreferenceManager;
import org.openobservatory.ooniprobe.common.ReadMorePlugin;
import org.openobservatory.ooniprobe.databinding.ActivityOverviewBinding;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package org.openobservatory.ooniprobe.activity.customwebsites

import android.content.DialogInterface
import android.os.Bundle
import android.os.Parcelable
import android.util.Patterns
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.activity.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import org.openobservatory.ooniprobe.R
import org.openobservatory.ooniprobe.activity.AbstractActivity
import org.openobservatory.ooniprobe.activity.RunningActivity
import org.openobservatory.ooniprobe.activity.customwebsites.adapter.CustomWebsiteRecyclerViewAdapter
import org.openobservatory.ooniprobe.activity.customwebsites.adapter.ItemChangedListener
import org.openobservatory.ooniprobe.common.PreferenceManager
import org.openobservatory.ooniprobe.databinding.ActivityCustomwebsiteBinding
import org.openobservatory.ooniprobe.fragment.ConfirmDialogFragment
import org.openobservatory.ooniprobe.model.database.Url
import org.openobservatory.ooniprobe.test.suite.WebsitesSuite
import java.io.Serializable
import javax.inject.Inject

/** This activity allows a user to test a custom website. */
class CustomWebsiteActivity : AbstractActivity(), ConfirmDialogFragment.OnClickListener {
@Inject
lateinit var preferenceManager: PreferenceManager

val viewModel: CustomWebsiteViewModel by viewModels()

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(
onItemChangedListener = object : ItemChangedListener {
override fun onItemRemoved(position: Int) {
binding.bottomBar.title = getString(
R.string.OONIRun_URLs, adapter.itemCount.toString()
)
viewModel.onItemRemoved(position)
}

override fun onItemUpdated(position: Int, item: String) {
viewModel.updateUrlAt(position, item)
}
},
)
viewModel.urls.observe(this) { urls ->
binding.bottomBar.title = getString(
R.string.OONIRun_URLs, urls.size.toString()
)
}

binding.bottomBar.setOnMenuItemClickListener { item: MenuItem? -> runTests() }
binding.add.setOnClickListener { add() }

binding.urlContainer.adapter = adapter
if (viewModel.urls.value == null) {
add()
}
}

override fun onResume() {
super.onResume()
viewModel.urls.value?.let { urls ->
adapter.submitList(urls)
binding.urlContainer.post { adapter.notifyDataSetChanged() }
}
}

/**
* This function will run the tests if the list of urls is not empty.
* If the list is empty, it will not run the tests.
* This function will also sanitize the url and remove any new lines.
* It will also check if the url is valid and not too long.
* If the url is not valid or too long, it will not be added to the tests.
*/
private fun runTests(): Boolean {
val items = viewModel.urls.value ?: listOf()
if (items.isEmpty()) {
return false
}
val urls = ArrayList<String>(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
)
return true
}

/**
* This function will show a dialog if the user has edited the list of urls.
* If the user has edited the list of urls, it will show a dialog asking if the user wants to save the changes.
* If the user has not edited the list of urls, it will just call super.onBackPressed()
*/
override fun onBackPressed() {
val base = getString(R.string.http)
val edited = adapter.itemCount > 0 && viewModel.urls.value?.get(0) != base
if (edited) {
ConfirmDialogFragment(
title = getString(R.string.Modal_CustomURL_Title_NotSaved),
message = getString(R.string.Modal_CustomURL_NotSaved),
).show(supportFragmentManager, null)
} else {
super.onBackPressed()
}
}

override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return true
}

override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater: MenuInflater = menuInflater
inflater.inflate(R.menu.close, menu)
return true
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.close_button -> {
onSupportNavigateUp()
true
}

else -> super.onOptionsItemSelected(item)
}
}

/**
* This function will add a new url to the list of urls.
* It will also scroll to the bottom of the list.
*/
fun add() {
viewModel.addUrl(getString(R.string.http))
binding.urlContainer.layoutManager?.scrollToPosition(adapter.itemCount - 1)
}

/**
* This function will be called when the user clicks on a button in the dialog.
* If the user clicks on the positive button, it will call super.onBackPressed()
*/
override fun onConfirmDialogClick(
serializable: Serializable?, parcelable: Parcelable?, buttonClicked: Int
) {
if (buttonClicked == DialogInterface.BUTTON_POSITIVE) super.onBackPressed()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.openobservatory.ooniprobe.activity.customwebsites

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

/**
* This class is used to store the data for the CustomWebsiteActivity.
* The data is stored in a ViewModel so that it can survive configuration changes (like rotation).
* This class shound not be injected to the activity using a DI framework.
* Dagger forces the recreation of the [ViewModel] on configuration
* as oposed to using `by viewModels()` which remembers the last state.
*/
class CustomWebsiteViewModel : ViewModel() {

val urls = MutableLiveData<MutableList<String>>()

/**
* This function will add a new url to the list of urls.
* If the list is null, it will create a new list.
*/
fun addUrl(url: String) {
val currentUrls = urls.value ?: ArrayList()
currentUrls.add(url)
urls.value = currentUrls
}

/**
* This function will remove a url from the list of urls.
* If the list is null, it will not do anything.
*/
fun onItemRemoved(position: Int) {
val currentList = urls.value ?: mutableListOf()
if (position < currentList.size) {
currentList.removeAt(position)
urls.value = currentList
}
}

/**
* This function will update the url at the given position.
* If the list is null, it will not do anything.
*/
fun updateUrlAt(position: Int, newUrl: String) {
val currentList = urls.value ?: mutableListOf()
if (position < currentList.size) {
currentList[position] = newUrl
urls.value = currentList
}
}

}
Loading

0 comments on commit 706eef1

Please sign in to comment.