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">