From d096681aa3f0037ee41d4eb35377a4fc890a46d0 Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Tue, 13 Feb 2024 15:16:20 +0100 Subject: [PATCH] feat: Update the entry point for ooni run v2 to use load using a different screen (#657) ## Proposed Changes - Add `OoniRunV2Activity` to handle all incoming OONI Run v2 `Intents` - Update `AndroidManifest.xml` to launch `OoniRunV2Activity` when OONI Run v2 `Intents` are received. - Clean `MainActivity` - Override `onBackPressed` and `finish` in `AddDescriptorActivity` to ensure activity always launches `MainActivity` when closed |.| Possible Areas of Improvement | |-|-| | ![Screenshot_20240125_205311](https://github.com/ooni/probe-android/assets/17911892/595a9100-9fd6-4baf-b9d0-f0f024402fa7)| -Improve on error messages.
- Merge with `AddDescriptorActivity` to make a single activity with 2 fragments for each of the views (requires a bit of core rewrite). | --------- Co-authored-by: Simone Basso --- app/src/main/AndroidManifest.xml | 6 + .../ooniprobe/activity/MainActivity.java | 210 ++---------------- .../adddescriptor/AddDescriptorActivity.kt | 11 + .../activity/oonirun/OoniRunV2Activity.kt | 187 ++++++++++++++++ .../ooniprobe/di/ActivityComponent.java | 2 + .../main/res/layout/activity_ooni_run_v2.xml | 58 +++++ 6 files changed, 282 insertions(+), 192 deletions(-) create mode 100644 app/src/main/java/org/openobservatory/ooniprobe/activity/oonirun/OoniRunV2Activity.kt create mode 100644 app/src/main/res/layout/activity_ooni_run_v2.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 324cba992..05c043dd8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -33,6 +33,12 @@ + + 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 4cc5a3d53..2d418afe5 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/activity/MainActivity.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/MainActivity.java @@ -13,38 +13,30 @@ import android.os.PowerManager; import android.provider.Settings; -import android.view.View; - import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatDelegate; import androidx.core.content.ContextCompat; -import androidx.fragment.app.Fragment; import com.google.android.material.snackbar.Snackbar; -import com.google.common.base.Optional; -import org.openobservatory.engine.OONIRunDescriptor; import org.openobservatory.ooniprobe.R; -import org.openobservatory.ooniprobe.activity.adddescriptor.AddDescriptorActivity; -import org.openobservatory.ooniprobe.common.*; +import org.openobservatory.ooniprobe.common.Application; +import org.openobservatory.ooniprobe.common.NotificationUtility; +import org.openobservatory.ooniprobe.common.PreferenceManager; +import org.openobservatory.ooniprobe.common.ThirdPartyServices; import org.openobservatory.ooniprobe.common.service.ServiceUtil; import org.openobservatory.ooniprobe.databinding.ActivityMainBinding; import org.openobservatory.ooniprobe.domain.UpdatesNotificationManager; import org.openobservatory.ooniprobe.fragment.DashboardFragment; import org.openobservatory.ooniprobe.fragment.PreferenceGlobalFragment; import org.openobservatory.ooniprobe.fragment.ResultListFragment; -import org.openobservatory.ooniprobe.fragment.dynamicprogressbar.OONIRunDynamicProgressBar; -import org.openobservatory.ooniprobe.fragment.dynamicprogressbar.OnActionListener; -import org.openobservatory.ooniprobe.fragment.dynamicprogressbar.ProgressType; -import org.openobservatory.ooniprobe.model.database.TestDescriptor; import java.io.Serializable; import javax.inject.Inject; -import kotlin.Unit; import localhost.toolkit.app.fragment.ConfirmDialogFragment; public class MainActivity extends AbstractActivity implements ConfirmDialogFragment.OnConfirmedListener { @@ -62,9 +54,6 @@ public class MainActivity extends AbstractActivity implements ConfirmDialogFragm @Inject PreferenceManager preferenceManager; - @Inject - TestDescriptorManager descriptorManager; - private ActivityResultLauncher requestPermissionLauncher; public static Intent newIntent(Context context, int resItem) { @@ -148,7 +137,6 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { } } requestNotificationPermission(); - onNewIntent(getIntent()); } private void requestNotificationPermission() { @@ -193,185 +181,23 @@ private void requestNotificationPermission() { protected void onNewIntent(Intent intent) { super.onNewIntent(intent); /** - * Check if we are starting the activity from a link [Intent.ACTION_VIEW]. - * This is invoked when a v2 link is opened. - * @see {@link org.openobservatory.ooniprobe.activity.OoniRunActivity#newIntent}. for v1 links. + * Check if we are starting the activity with an intent extra. + * This is invoked when we are starting the activity from a notification or + * when the activity is launched from the onboarding fragment + * @see {@link org.openobservatory.ooniprobe.fragment.onboarding.Onboarding3Fragment#masterClick}. */ - if (Intent.ACTION_VIEW.equals(intent.getAction())) { - Uri uri = intent.getData(); - // If the intent does not contain a link, do nothing. - if (uri == null) { - return; - } - - Optional possibleRunId = getRunId(uri); - - // If the intent contains a link, but it is not a supported link or has a non-numerical `link_id`. - if (!possibleRunId.isPresent()) { - return; - } - - // If the intent contains a link, but the `link_id` is zero. - if (possibleRunId.get() == 0) { - return; - } - - TaskExecutor executor = new TaskExecutor(); - - displayAddLinkProgressFragment(executor); - - executor.executeTask(() -> { - try { - return descriptorManager.fetchDescriptorFromRunId(possibleRunId.get(), this); - } catch (Exception exception) { - exception.printStackTrace(); - ThirdPartyServices.logException(exception); - return null; - } - }, this::fetchDescriptorComplete); - } else { - /** - * Check if we are starting the activity with an intent extra. - * This is invoked when we are starting the activity from a notification or - * when the activity is launched from the onboarding fragment - * @see {@link org.openobservatory.ooniprobe.fragment.onboarding.Onboarding3Fragment#masterClick}. - */ - if (intent.getExtras() != null) { - if (intent.getExtras().containsKey(RES_ITEM)) { - binding.bottomNavigation.setSelectedItemId(intent.getIntExtra(RES_ITEM, R.id.dashboard)); - } else if (intent.getExtras().containsKey(NOTIFICATION_DIALOG)) { - new ConfirmDialogFragment.Builder() - .withTitle(intent.getExtras().getString("title")) - .withMessage(intent.getExtras().getString("message")) - .withNegativeButton("") - .withPositiveButton(getString(R.string.Modal_OK)) - .build().show(getSupportFragmentManager(), null); - } - } - } - } - - /** - * 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(TestDescriptor 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, - OONIRunDynamicProgressBar.newInstance(ProgressType.ADD_LINK, new OnActionListener() { - @Override - public void onActionButtonCLicked() { - executor.cancelTask(); - removeProgressFragment(); - } - - @Override - public void onCloseButtonClicked() { - removeProgressFragment(); - } - }), - OONIRunDynamicProgressBar.getTAG() - ).commit(); - } - - /** - * Extracts the run id from the provided Uri. - * The run id can be in two different formats: - *

- * 1. ooni://runv2/link_id - * 2. https://run.test.ooni.org/v2/link_id - *

- * The run id is the `link_id` in the link. - * If the Uri contains a link, but the `link_id` is not a number, an empty Optional is returned. - * If the Uri contains a link, but it is not a supported link, an empty Optional is returned. - *

- * - * @param uri The Uri data. - * @return An Optional containing the run id if the Uri contains a link with a valid `link_id`, or an empty Optional otherwise. - */ - private Optional getRunId(Uri uri) { - String host = uri.getHost(); - - 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/link_id - */ - return Optional.of( - 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/link_id - */ - return Optional.of( - Long.parseLong( - uri.getPathSegments().get(1) - ) - ); - } else { - // If the intent contains a link, but it is not a supported link. - return Optional.absent(); + if (intent.getExtras() != null) { + if (intent.getExtras().containsKey(RES_ITEM)) { + binding.bottomNavigation.setSelectedItemId(intent.getIntExtra(RES_ITEM, R.id.dashboard)); + } else if (intent.getExtras().containsKey(NOTIFICATION_DIALOG)) { + new ConfirmDialogFragment.Builder() + .withTitle(intent.getExtras().getString("title")) + .withMessage(intent.getExtras().getString("message")) + .withNegativeButton("") + .withPositiveButton(getString(R.string.Modal_OK)) + .build().show(getSupportFragmentManager(), null); } - } catch (Exception e) { - // If the intent contains a link, but the `link_id` is not a number. - e.printStackTrace(); - return Optional.absent(); - } - } - - /** - * Remove the progress fragment. - *

- * This method is called when the task is completed. - */ - private void removeProgressFragment() { - Fragment fragment = getSupportFragmentManager().findFragmentByTag(OONIRunDynamicProgressBar.getTAG()); - if (fragment != null && fragment.isAdded()) { - getSupportFragmentManager().beginTransaction().remove(fragment).commit(); } - binding.dynamicProgressFragment.setVisibility(View.GONE); } @Override diff --git a/app/src/main/java/org/openobservatory/ooniprobe/activity/adddescriptor/AddDescriptorActivity.kt b/app/src/main/java/org/openobservatory/ooniprobe/activity/adddescriptor/AddDescriptorActivity.kt index fd313b7c0..5b2a0a8d8 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/activity/adddescriptor/AddDescriptorActivity.kt +++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/adddescriptor/AddDescriptorActivity.kt @@ -21,6 +21,7 @@ import org.openobservatory.engine.OONIRunDescriptor import org.openobservatory.engine.OONIRunNettest import org.openobservatory.ooniprobe.R import org.openobservatory.ooniprobe.activity.AbstractActivity +import org.openobservatory.ooniprobe.activity.MainActivity import org.openobservatory.ooniprobe.activity.adddescriptor.adapter.AddDescriptorExpandableListAdapter import org.openobservatory.ooniprobe.activity.adddescriptor.adapter.GroupedItem import org.openobservatory.ooniprobe.common.PreferenceManager @@ -198,4 +199,14 @@ class AddDescriptorActivity : AbstractActivity() { else -> super.onOptionsItemSelected(item) } } + + override fun onBackPressed() { + startActivity(MainActivity.newIntent(applicationContext, R.id.dashboard)) + super.onBackPressed() + } + + override fun finish() { + startActivity(MainActivity.newIntent(applicationContext, R.id.dashboard)) + super.finish() + } } \ No newline at end of file diff --git a/app/src/main/java/org/openobservatory/ooniprobe/activity/oonirun/OoniRunV2Activity.kt b/app/src/main/java/org/openobservatory/ooniprobe/activity/oonirun/OoniRunV2Activity.kt new file mode 100644 index 000000000..5b4dc61bc --- /dev/null +++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/oonirun/OoniRunV2Activity.kt @@ -0,0 +1,187 @@ +package org.openobservatory.ooniprobe.activity.oonirun + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.widget.Toast +import org.openobservatory.ooniprobe.R +import org.openobservatory.ooniprobe.activity.AbstractActivity +import org.openobservatory.ooniprobe.activity.adddescriptor.AddDescriptorActivity +import org.openobservatory.ooniprobe.common.TaskExecutor +import org.openobservatory.ooniprobe.common.TestDescriptorManager +import org.openobservatory.ooniprobe.common.ThirdPartyServices +import org.openobservatory.ooniprobe.databinding.ActivityOoniRunV2Binding +import org.openobservatory.ooniprobe.model.database.TestDescriptor +import javax.inject.Inject + +/** + * Activity to handle a v2 link. + * + * A v2 link has the following format: + * + * 1. ooni://runv2/link_id + * 2. https://run.test.ooni.org/v2/link_id + * + * The activity is started when the user clicks on `Open Link in OONI Probe` or + * when the system recognizes this app can open this link and launches the app when a link is clicked. + * + * It fetches the descriptor from the link and starts the `AddDescriptorActivity`. + * If the link is invalid, it shows an error message and ends the activity. + * + * @see {@link org.openobservatory.ooniprobe.activity.OoniRunActivity} for v1 links. + */ +class OoniRunV2Activity : AbstractActivity() { + lateinit var binding: ActivityOoniRunV2Binding + + @Inject + lateinit var descriptorManager: TestDescriptorManager + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + activityComponent.inject(this) + binding = ActivityOoniRunV2Binding.inflate(layoutInflater) + setContentView(binding.root) + onNewIntent(intent) + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + /** + * Check if we are starting the activity from a link [Intent.ACTION_VIEW]. + * This is invoked when a v2 link is opened. + * @see {@link org.openobservatory.ooniprobe.activity.OoniRunActivity.newIntent} for v1 links. + */ + if (Intent.ACTION_VIEW == intent.action) { + manageIntent(intent) + } else { + // If the intent action is not `Intent.ACTION_VIEW`, end activity. + Toast.makeText(this, getString(R.string.Modal_Error), Toast.LENGTH_LONG).show() + finish() + } + } + + /** + * Parses the intent data to extract the link. + * If the intent does not contain a link, show an error message and end the activity. + * If the intent contains a link, but it is not a supported link or has a non-numerical `link_id`, + * show an error message and end the activity. + * If the intent contains a link, but the `link_id` is zero, + * show an error message and end the activity. + * If the intent contains a link with a valid `link_id`, + * fetch the descriptor from the link and start the `AddDescriptorActivity`. + * + * @param intent The intent data. + */ + private fun manageIntent(intent: Intent) { + // If the intent does not contain a link, do nothing. + val uri = intent.data ?: finishWithError().run { return } + // If the intent contains a link, but it is not a supported link or has a non-numerical `link_id`. + val possibleRunId: Long = getRunId(uri) ?: finishWithError().run { return } + + // If the intent contains a link, but the `link_id` is zero. + if (possibleRunId == 0L) { + finishWithError().run { return } + } + val executor = TaskExecutor() + binding.cancelButton.setOnClickListener { + executor.cancelTask() + finishWithError(message = getString(R.string.Modal_Cancel)) + } + executor.executeTask({ + try { + return@executeTask descriptorManager.fetchDescriptorFromRunId( + possibleRunId, + this + ) + } catch (exception: Exception) { + exception.printStackTrace() + ThirdPartyServices.logException(exception) + return@executeTask null + } + }) { descriptorResponse: TestDescriptor? -> + fetchDescriptorComplete( + descriptorResponse + ) + } + } + + /** + * Shows an error message and ends the activity. + * + * @param message The error message to be shown. + */ + private fun finishWithError(message: String = getString(R.string.Modal_Error)) { + Toast.makeText(this, message, Toast.LENGTH_LONG).show() + finish() + } + + /** + * 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 fun fetchDescriptorComplete(descriptorResponse: TestDescriptor?) { + descriptorResponse?.let { + startActivity(AddDescriptorActivity.newIntent(this, descriptorResponse)) + } ?: run { + Toast.makeText(this, getString(R.string.Modal_Error), Toast.LENGTH_LONG).show() + } + } + + /** + * Extracts the run id from the provided Uri. + * The run id can be in two different formats: + * + * 1. ooni://runv2/link_id + * 2. https://run.test.ooni.org/v2/link_id + * + * The run id is the `link_id` in the link. + * If the Uri contains a link, but the `link_id` is not a number, null is returned. + * If the Uri contains a link, but it is not a supported link, null is returned. + * + * @param uri The Uri data. + * @return The run id if the Uri contains a link with a valid `link_id`, or null otherwise. + */ + private fun getRunId(uri: Uri): Long? { + val host = uri.host + try { + when (host) { + "runv2" -> { + /* + * The run id is the first segment of the path. + * Launched when `Open Link in OONI Probe` is clicked. + * e.g. ooni://runv2/link_id + */ + return uri.pathSegments[0].toLong() + } + + "run.test.ooni.org" -> { + /* + * 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/link_id + */ + return uri.pathSegments[1].toLong() + } + + else -> return null + } + } catch (e: Exception) { + // If the intent contains a link, but the `link_id` is not a number. + e.printStackTrace() + return null + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/openobservatory/ooniprobe/di/ActivityComponent.java b/app/src/main/java/org/openobservatory/ooniprobe/di/ActivityComponent.java index de437ddab..4ed40d976 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/di/ActivityComponent.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/di/ActivityComponent.java @@ -10,6 +10,7 @@ import org.openobservatory.ooniprobe.activity.OverviewActivity; import org.openobservatory.ooniprobe.activity.ProxyActivity; import org.openobservatory.ooniprobe.activity.ResultDetailActivity; +import org.openobservatory.ooniprobe.activity.oonirun.OoniRunV2Activity; import org.openobservatory.ooniprobe.activity.runtests.RunTestsActivity; import org.openobservatory.ooniprobe.activity.RunningActivity; import org.openobservatory.ooniprobe.activity.TextActivity; @@ -20,6 +21,7 @@ @PerActivity @Subcomponent() public interface ActivityComponent { + void inject(OoniRunV2Activity activity); void inject(AddDescriptorActivity activity); void inject(CustomWebsiteActivity activity); void inject(MainActivity activity); diff --git a/app/src/main/res/layout/activity_ooni_run_v2.xml b/app/src/main/res/layout/activity_ooni_run_v2.xml new file mode 100644 index 000000000..0dfc63c1d --- /dev/null +++ b/app/src/main/res/layout/activity_ooni_run_v2.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + +