diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3a9d6e328..903df2dee 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,38 +1,38 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -57,144 +57,144 @@
android:host="runv2"
android:scheme="ooni" />
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:parentActivityName=".activity.MainActivity"
+ android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar.App.NoActionBar" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/org/openobservatory/ooniprobe/activity/MainActivity.java b/app/src/main/java/org/openobservatory/ooniprobe/activity/MainActivity.java
index 5ee1801f4..35fc697b8 100644
--- a/app/src/main/java/org/openobservatory/ooniprobe/activity/MainActivity.java
+++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/MainActivity.java
@@ -14,6 +14,7 @@
import android.provider.Settings;
import android.view.View;
+
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
@@ -23,6 +24,7 @@
import com.google.android.material.snackbar.Snackbar;
+import org.openobservatory.engine.OONIRunDescriptor;
import org.openobservatory.ooniprobe.R;
import org.openobservatory.ooniprobe.activity.add_descriptor.AddDescriptorActivity;
import org.openobservatory.ooniprobe.common.*;
@@ -40,6 +42,7 @@
import javax.inject.Inject;
+import kotlin.Unit;
import localhost.toolkit.app.fragment.ConfirmDialogFragment;
public class MainActivity extends AbstractActivity implements ConfirmDialogFragment.OnConfirmedListener {
@@ -187,66 +190,34 @@ private void requestNotificationPermission() {
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
+ // Check if we are starting the activity from a link [Intent.ACTION_VIEW].
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
Uri uri = intent.getData();
- if (uri == null) return;
+ // If the intent does not contain a link, do nothing.
+ if (uri == null) {
+ return;
+ }
- String host = uri.getHost();
- long runId = 0;
+ long runId = getRunId(uri);
- try {
- if ("runv2".equals(host)) {
- runId = Long.parseLong(uri.getPathSegments().get(0));
- } else if ("run.test.ooni.org".equals(host)) {
- runId = Long.parseLong(uri.getPathSegments().get(1));
- }
- } catch (Exception e) {
- e.printStackTrace();
+ // If the intent contains a link, but the `link_id` is zero, or the link is not supported, do nothing.
+ if (runId == 0) {
+ return;
}
- if (runId == 0) return;
+
TaskExecutor executor = new TaskExecutor();
- binding.dynamicProgressFragment.setVisibility(View.VISIBLE);
- getSupportFragmentManager().beginTransaction()
- .add(
- R.id.dynamic_progress_fragment,
- DynamicProgressFragment.newInstance(ProgressType.ADD_LINK, new OnActionListener() {
- @Override
- public void onActionButtonCLicked() {
- executor.cancelTask();
- removeProgressFragment();
- }
-
- @Override
- public void onIconButtonClicked() {
- removeProgressFragment();
- }
- }),
- DynamicProgressFragment.getTAG()
- ).commit();
- long finalRunId = runId;
- executor.executeTask(
- () -> {
- try {
- return descriptorManager.fetchDescriptorFromRunId(finalRunId, this);
- } catch (Exception exception) {
- exception.printStackTrace();
- ThirdPartyServices.logException(exception);
- return null;
- }
- },
- descriptorResponse -> {
- if (descriptorResponse != null) {
- startActivity(AddDescriptorActivity.newIntent(this, descriptorResponse));
- } else {
- // TODO(aanorbel): Provide a better error message.
- Snackbar.make(binding.getRoot(), R.string.Modal_Error, Snackbar.LENGTH_LONG)
- .setAnchorView(binding.bottomNavigation) // NOTE:To avoid the `snackbar` from covering the bottom navigation.
- .show();
- }
- removeProgressFragment();
- return null;
- });
+ displayAddLinkProgressFragment(executor);
+
+ executor.executeTask(() -> {
+ try {
+ return descriptorManager.fetchDescriptorFromRunId(runId, this);
+ } catch (Exception exception) {
+ exception.printStackTrace();
+ ThirdPartyServices.logException(exception);
+ return null;
+ }
+ }, this::fetchDescriptorComplete);
} else {
if (intent.getExtras() != null) {
if (intent.getExtras().containsKey(RES_ITEM))
@@ -263,6 +234,112 @@ else if (intent.getExtras().containsKey(NOTIFICATION_DIALOG)) {
}
}
+ /**
+ * The task to fetch the descriptor from the link is completed.
+ *
+ * This method is called when the `fetchDescriptorFromRunId` task is completed.
+ * The `descriptorResponse` is the result of the task.
+ * If the task is successful, the `descriptorResponse` is the descriptor.
+ * Otherwise, the `descriptorResponse` is null.
+ *
+ * If the `descriptorResponse` is not null, start the `AddDescriptorActivity`.
+ * Otherwise, show an error message.
+ *
+ * @param descriptorResponse The result of the task.
+ * @return null.
+ */
+ private Unit fetchDescriptorComplete(OONIRunDescriptor descriptorResponse) {
+ if (descriptorResponse != null) {
+ startActivity(AddDescriptorActivity.newIntent(this, descriptorResponse));
+ } else {
+ // TODO(aanorbel): Provide a better error message.
+ Snackbar.make(binding.getRoot(), R.string.Modal_Error, Snackbar.LENGTH_LONG)
+ .setAnchorView(binding.bottomNavigation) // NOTE:To avoid the `snackbar` from covering the bottom navigation.
+ .show();
+ }
+ removeProgressFragment();
+ return Unit.INSTANCE;
+
+ }
+
+ /**
+ * Display the progress fragment.
+ *
+ * The progress fragment is used to display the progress of the task.
+ * e.g. Fetching the descriptor from the link.
+ *
+ * @param executor The executor that will be used to execute the task.
+ */
+ private void displayAddLinkProgressFragment(TaskExecutor executor) {
+ binding.dynamicProgressFragment.setVisibility(View.VISIBLE);
+ getSupportFragmentManager().beginTransaction()
+ .add(
+ R.id.dynamic_progress_fragment,
+ DynamicProgressFragment.newInstance(ProgressType.ADD_LINK, new OnActionListener() {
+ @Override
+ public void onActionButtonCLicked() {
+ executor.cancelTask();
+ removeProgressFragment();
+ }
+
+ @Override
+ public void onCloseButtonClicked() {
+ removeProgressFragment();
+ }
+ }),
+ DynamicProgressFragment.getTAG()
+ ).commit();
+ }
+
+ /**
+ * Get the run id from the intent.
+ * The run id can be in two different formats.
+ *
+ * 1. ooni://runv2/
+ * 2. https://run.test.ooni.org/v2/
+ * The run id is the `link_id` in the link.
+ * If the intent contains a link, but the `link_id` is not a number, return zero.
+ * If the intent contains a link, but it is not a supported link, return zero.
+ *
+ * @param uri The intent data.
+ * @return The run id if the intent contains a link with a valid `link_id`.
+ */
+ private long getRunId(Uri uri) {
+ String host = uri.getHost();
+ long runId = 0;
+
+ try {
+ if ("runv2".equals(host)) {
+ /**
+ * The run id is the first segment of the path.
+ * Launched when `Open Link in OONI Probe` is clicked.
+ * e.g. ooni://runv2/
+ */
+ runId = Long.parseLong(uri.getPathSegments().get(0));
+ } else if ("run.test.ooni.org".equals(host)) {
+ /**
+ * The run id is the second segment of the path.
+ * Launched when the system recognizes this app can open this link
+ * and launches the app when a link is clicked.
+ * e.g. https://run.test.ooni.org/v2/
+ */
+ runId = Long.parseLong(uri.getPathSegments().get(1));
+ } else {
+ // If the intent contains a link, but it is not a supported link, return zero.
+ return 0;
+ }
+ } catch (Exception e) {
+ // If the intent contains a link, but the `link_id` is not a number.
+ e.printStackTrace();
+ }
+ return runId;
+ }
+
+ /**
+ * Remove the progress fragment.
+ *
+ * This method is called when the task is completed.
+ */
private void removeProgressFragment() {
Fragment fragment = getSupportFragmentManager().findFragmentByTag(DynamicProgressFragment.getTAG());
if (fragment != null && fragment.isAdded()) {
diff --git a/app/src/main/java/org/openobservatory/ooniprobe/activity/add_descriptor/AddDescriptorActivity.kt b/app/src/main/java/org/openobservatory/ooniprobe/activity/add_descriptor/AddDescriptorActivity.kt
index 983e1ec2a..f9182c690 100644
--- a/app/src/main/java/org/openobservatory/ooniprobe/activity/add_descriptor/AddDescriptorActivity.kt
+++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/add_descriptor/AddDescriptorActivity.kt
@@ -28,10 +28,20 @@ import org.openobservatory.ooniprobe.common.TestDescriptorManager
import org.openobservatory.ooniprobe.databinding.ActivityAddDescriptorBinding
import javax.inject.Inject
+/**
+ * This activity is used to add a new descriptor to the application. The activity shows the tests that are included in the descriptor.
+ * The user can select which tests to include, and if the descriptor should be automatically updated.
+ */
class AddDescriptorActivity : AbstractActivity() {
companion object {
private const val DESCRIPTOR = "descriptor"
+ /**
+ * This method is used to create an intent to start this activity.
+ * @param context is the context of the activity that calls this method
+ * @param descriptor is the descriptor to add
+ * @return an intent to start this activity
+ */
@JvmStatic
fun newIntent(context: Context, descriptor: OONIRunDescriptor): Intent {
return Intent(context, AddDescriptorActivity::class.java).putExtra(
@@ -144,11 +154,13 @@ class AddDescriptorActivity : AbstractActivity() {
binding.testsCheckbox.checkedState = state;
}
+ // This observer is used to change the state of the "Select All" button when a checkbox is clicked.
binding.testsCheckbox.addOnCheckedStateChangedListener { checkBox, state ->
viewModel.setSelectedAllBtnStatus(state)
adapter.notifyDataSetChanged()
}
+ // This observer is used to finish the activity when the descriptor is added.
viewModel.finishActivity.observe(this) { shouldFinish ->
if (shouldFinish) {
finish()
diff --git a/app/src/main/java/org/openobservatory/ooniprobe/activity/add_descriptor/AddDescriptorViewModel.kt b/app/src/main/java/org/openobservatory/ooniprobe/activity/add_descriptor/AddDescriptorViewModel.kt
index adc13852d..9be6d5109 100644
--- a/app/src/main/java/org/openobservatory/ooniprobe/activity/add_descriptor/AddDescriptorViewModel.kt
+++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/add_descriptor/AddDescriptorViewModel.kt
@@ -11,6 +11,14 @@ import org.openobservatory.ooniprobe.common.LocaleUtils
import org.openobservatory.ooniprobe.common.TestDescriptorManager
import javax.inject.Inject
+/**
+ * ViewModel for the AddDescriptorActivity. This class is responsible for preparing and managing the data for the AddDescriptorActivity.
+ * It handles the communication of the Activity with the rest of the application (e.g. calling business logic classes).
+ *
+ * @property descriptorManager Instance of TestDescriptorManager which is responsible for managing the test descriptors.
+ * @property selectedAllBtnStatus LiveData holding the state of the "Select All" button in the UI.
+ * @property descriptor LiveData holding the OONIRunDescriptor object that the user is currently interacting with in the UI.
+ */
class AddDescriptorViewModel constructor(
var descriptorManager: TestDescriptorManager
) : ViewModel() {
@@ -19,22 +27,43 @@ class AddDescriptorViewModel constructor(
MutableLiveData(MaterialCheckBox.STATE_CHECKED)
var descriptor: MutableLiveData = MutableLiveData()
val finishActivity: MutableLiveData = MutableLiveData()
+
+ /**
+ * This method is called when the activity is created.
+ * It sets the descriptor value of this ViewModel.
+ * @param descriptor is the new descriptor
+ */
fun onDescriptorChanged(descriptor: OONIRunDescriptor) {
this.descriptor.value = descriptor
}
+ /**
+ * This method is used to get the name of the descriptor.
+ * Used by the UI during data binding.
+ * @return the name of the descriptor.
+ */
fun getName(): String {
return descriptor.value?.let { descriptor ->
descriptor.nameIntl[LocaleUtils.sLocale.language] ?: descriptor.name
} ?: ""
}
+ /**
+ * This method is used to get the name of the descriptor.
+ * Used by the UI during data binding.
+ * @return the name of the descriptor.
+ */
fun getDescription(): String {
return descriptor.value?.let { descriptor ->
descriptor.descriptionIntl[LocaleUtils.sLocale.language] ?: descriptor.description
} ?: ""
}
+ /**
+ * This method is used to get the short description of the descriptor.
+ * Used by the UI during data binding.
+ * @return the short description of the descriptor.
+ */
fun getShortDescription(): String {
return descriptor.value?.let { descriptor ->
descriptor.shortDescriptionIntl[LocaleUtils.sLocale.language]
@@ -42,10 +71,21 @@ class AddDescriptorViewModel constructor(
} ?: ""
}
+ /**
+ * This method is used to set the state of the "Select All" button in the UI.
+ * @param selectedStatus is the new state of the "Select All" button.
+ */
fun setSelectedAllBtnStatus(@CheckedState selectedStatus: Int) {
selectedAllBtnStatus.postValue(selectedStatus)
}
+
+ /**
+ * This method is called when the "Add Link" button is clicked.
+ * It adds the descriptor to the descriptor manager and signals that the activity should finish.
+ * @param selectedNettest is the list of selected nettests.
+ * @param automatedUpdates is a boolean indicating whether automated updates should be enabled.
+ */
fun onAddButtonClicked(selectedNettest: List, automatedUpdates: Boolean) {
descriptor.value?.let { descriptor ->
descriptorManager.addDescriptor(
@@ -59,7 +99,10 @@ class AddDescriptorViewModel constructor(
} ?: throw IllegalStateException("Descriptor is null")
}
+ /**
+ * This method is used to signal that the activity should finish.
+ */
fun finishActivity() {
finishActivity.value = true
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/openobservatory/ooniprobe/activity/add_descriptor/adapter/AddDescriptorExpandableListAdapter.kt b/app/src/main/java/org/openobservatory/ooniprobe/activity/add_descriptor/adapter/AddDescriptorExpandableListAdapter.kt
index 2ded231c4..671da232c 100644
--- a/app/src/main/java/org/openobservatory/ooniprobe/activity/add_descriptor/adapter/AddDescriptorExpandableListAdapter.kt
+++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/add_descriptor/adapter/AddDescriptorExpandableListAdapter.kt
@@ -15,12 +15,22 @@ import org.openobservatory.ooniprobe.R
import org.openobservatory.ooniprobe.activity.add_descriptor.AddDescriptorViewModel
import org.openobservatory.ooniprobe.test.test.AbstractTest
+
+/**
+ * An extension of [OONIRunNettest] class
+ * used to track the selected state of nettests in the [ExpandableListView].
+ */
class GroupedItem(
override var name: String,
override var inputs: List?,
var selected: Boolean = false
) : OONIRunNettest(name = name, inputs = inputs)
+/**
+ * Adapter class for the [ExpandableListView] in [AddDescriptorActivity].
+ * @param nettests List of GroupedItem objects.
+ * @param viewModel AddDescriptorViewModel object.
+ */
class AddDescriptorExpandableListAdapter(
val nettests: List,
val viewModel: AddDescriptorViewModel
diff --git a/app/src/main/java/org/openobservatory/ooniprobe/common/TaskExecutor.kt b/app/src/main/java/org/openobservatory/ooniprobe/common/TaskExecutor.kt
index b0ed4a66a..9469d6555 100644
--- a/app/src/main/java/org/openobservatory/ooniprobe/common/TaskExecutor.kt
+++ b/app/src/main/java/org/openobservatory/ooniprobe/common/TaskExecutor.kt
@@ -6,21 +6,51 @@ import java.util.concurrent.Callable
import java.util.concurrent.Executors
import java.util.concurrent.Future
+/**
+ * ProgressTask is an abstract class that represents a task that reports progress.
+ * @param P The type of the progress token.
+ * @param R The type of the result.
+ */
abstract class ProgressTask {
abstract fun runTask(progressToken: OnTaskProgressUpdate
): R
}
+/**
+ * Callable is an alias for the java.util.concurrent.Callable interface.
+ * @param R The type of the result.
+ */
typealias Task = Callable
+/**
+ * OnTaskProgressUpdate is a typealias for a callback that is invoked when a task reports progress.
+ * @param P The type of the progress token.
+ */
typealias OnTaskProgressUpdate = (P) -> Unit
+/**
+ * OnTaskComplete is a typealias for a callback that is invoked when a task is completed.
+ * @param R The type of the result.
+ */
typealias OnTaskComplete = (R) -> Unit
+/**
+ * TaskExecutor is a utility class that provides methods to execute tasks in a separate thread and post results on the main thread.
+ * It uses a single thread executor to run tasks and a Handler to post results on the main thread.
+ *
+ * @property executor The executor service that runs tasks in a separate thread.
+ * @property handler The handler that posts results on the main thread.
+ * @property future The future that represents the result of a task.
+ */
class TaskExecutor {
private val executor = Executors.newSingleThreadExecutor()
private val handler = Handler(Looper.getMainLooper())
private var future: Future<*>? = null
+ /**
+ * Executes a task in a separate thread and posts the result on the main thread.
+ * @param task The task to be executed.
+ * @param onComplete The callback to be invoked when the task is completed.
+ */
fun executeTask(task: Task, onComplete: OnTaskComplete) {
future = executor.submit {
val result = task.call()
@@ -30,6 +60,12 @@ class TaskExecutor {
}
}
+ /**
+ * Executes a task that reports progress in a separate thread and posts the result and progress updates on the main thread.
+ * @param progressTask The task to be executed.
+ * @param onProgress The callback to be invoked when the task reports progress.
+ * @param onComplete The callback to be invoked when the task is completed.
+ */
fun executeProgressTask(
progressTask: ProgressTask
,
onProgress: OnTaskProgressUpdate
,
@@ -50,6 +86,9 @@ class TaskExecutor {
}
}
+ /**
+ * Cancels the currently running task.
+ */
fun cancelTask() {
future?.cancel(true)
}
diff --git a/app/src/main/java/org/openobservatory/ooniprobe/common/TestDescriptorManager.kt b/app/src/main/java/org/openobservatory/ooniprobe/common/TestDescriptorManager.kt
index f5470b267..3b3e85038 100644
--- a/app/src/main/java/org/openobservatory/ooniprobe/common/TestDescriptorManager.kt
+++ b/app/src/main/java/org/openobservatory/ooniprobe/common/TestDescriptorManager.kt
@@ -11,6 +11,9 @@ import org.openobservatory.ooniprobe.test.suite.DynamicTestSuite
import javax.inject.Inject
import javax.inject.Singleton
+/**
+ * This class is responsible for managing the test descriptors
+ */
@Singleton
class TestDescriptorManager @Inject constructor(private val context: Context) {
private val descriptors: List> = ooniDescriptors(context)
@@ -27,6 +30,11 @@ class TestDescriptorManager @Inject constructor(private val context: Context) {
return getDescriptorByName(name)?.getTest(context)
}
+ /**
+ * Fetches the descriptor from the ooni server using the run id.
+ * @param runId the run id of the descriptor to fetch
+ * @param context the context to use for the request
+ */
fun fetchDescriptorFromRunId(runId: Long, context: Context): OONIRunDescriptor {
val session = EngineProvider.get().newSession(
EngineProvider.get().getDefaultSessionConfig(
diff --git a/app/src/main/java/org/openobservatory/ooniprobe/common/views/CustomExpandableListView.kt b/app/src/main/java/org/openobservatory/ooniprobe/common/views/CustomExpandableListView.kt
index d080d861f..3029bb0e0 100644
--- a/app/src/main/java/org/openobservatory/ooniprobe/common/views/CustomExpandableListView.kt
+++ b/app/src/main/java/org/openobservatory/ooniprobe/common/views/CustomExpandableListView.kt
@@ -4,6 +4,10 @@ import android.content.Context
import android.util.AttributeSet
import android.widget.ExpandableListView
+/**
+ * This class is needed to allow the ExpandableListView to be placed inside a ScrollView.
+ * Without this, the ExpandableListView will not expand to its full size and will not be scrollable.
+ */
class CustomExpandableListView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ExpandableListView(context, attrs, defStyleAttr) {
diff --git a/app/src/main/java/org/openobservatory/ooniprobe/fragment/dynamic_progress/DynamicProgressFragment.kt b/app/src/main/java/org/openobservatory/ooniprobe/fragment/dynamic_progress/DynamicProgressFragment.kt
index 2accf1d2b..0a0a26a80 100644
--- a/app/src/main/java/org/openobservatory/ooniprobe/fragment/dynamic_progress/DynamicProgressFragment.kt
+++ b/app/src/main/java/org/openobservatory/ooniprobe/fragment/dynamic_progress/DynamicProgressFragment.kt
@@ -36,7 +36,7 @@ class DynamicProgressFragment : Fragment() {
onActionListener?.onActionButtonCLicked()
}
binding.iconButton.setOnClickListener {
- onActionListener?.onIconButtonClicked()
+ onActionListener?.onCloseButtonClicked()
}
when (progressType) {
ProgressType.ADD_LINK -> {
@@ -115,5 +115,5 @@ interface OnActionListener {
/**
* Called when the icon button is clicked.
*/
- fun onIconButtonClicked()
+ fun onCloseButtonClicked()
}
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_dynamic_progress.xml b/app/src/main/res/layout/fragment_dynamic_progress.xml
index 5ca458afd..9a1f1247d 100644
--- a/app/src/main/res/layout/fragment_dynamic_progress.xml
+++ b/app/src/main/res/layout/fragment_dynamic_progress.xml
@@ -3,7 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="@color/color_gray9"
+ android:background="@color/progress_background"
tools:context=".fragment.dynamic_progress.DynamicProgressFragment">