commit dbc1811b57d0f5c4c08294ec2fe3e838407d7f4f parent cbc07d746f9216716d594a9be041b14480bd36ea Author: clairehurst <clairehurst@torproject.org> Date: Mon, 11 Dec 2023 17:42:52 -0700 [android] Implement Android-native Connection Assist UI Diffstat:
30 files changed, 1548 insertions(+), 41 deletions(-)
diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/SearchUseCases.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/SearchUseCases.kt @@ -76,12 +76,7 @@ class SearchUseCases( flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(), additionalHeaders: Map<String, String>? = null, ) { - var securityLevel: Int - try { - securityLevel = settings?.torSecurityLevel ?: 0 - } catch (e: UnsupportedSettingException) { - securityLevel = 0 - } + val securityLevel : Int = settings!!.torSecurityLevel val searchUrl = searchEngine?.let { searchEngine.buildSearchUrl(searchTerms, securityLevel) } ?: store.state.search.selectedOrDefaultSearchEngine?.buildSearchUrl(searchTerms, securityLevel) @@ -172,12 +167,7 @@ class SearchUseCases( flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(), additionalHeaders: Map<String, String>? = null, ) { - var securityLevel: Int - try { - securityLevel = settings?.torSecurityLevel ?: 0 - } catch (e: UnsupportedSettingException) { - securityLevel = 0 - } + val securityLevel : Int = settings!!.torSecurityLevel val searchUrl = searchEngine?.let { searchEngine.buildSearchUrl(searchTerms, securityLevel) } ?: store.state.search.selectedOrDefaultSearchEngine?.buildSearchUrl(searchTerms, securityLevel) diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/NotificationsDelegate.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/NotificationsDelegate.kt @@ -33,6 +33,15 @@ class NotificationsDelegate( var isRequestingPermission: Boolean = false private set + /** + * Defaults to true, normal behavior is to destroy the app when OnDestroy is called with isFinishing set to true + * + * A value of false indicates that the notification was just swiped away and the app should not shut down on it's behalf + * + * Workaround to make swiping the notification away not shutdown the app + */ + var shouldShutDownWithOnDestroyWhenIsFinishing: Boolean = true + @VisibleForTesting internal var permissionRequestsCount: Int = 0 diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt @@ -47,4 +47,5 @@ enum class BrowserDirection( FromMenuDialogFragment(R.id.menuDialogFragment), FromWebCompatReporterFragment(R.id.webCompatReporterFragment), FromGleanDebugToolsFragment(R.id.gleanDebugToolsFragment), + FromTorConnectionAssistFragment(R.id.torConnectionAssistFragment), } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -28,6 +28,7 @@ import android.view.ViewConfiguration import android.view.ViewGroup import android.view.WindowManager.LayoutParams.FLAG_SECURE import androidx.activity.BackEventCompat +import androidx.activity.viewModels import androidx.annotation.CallSuper import androidx.annotation.IdRes import androidx.annotation.VisibleForTesting @@ -85,6 +86,7 @@ import mozilla.components.support.utils.BrowsersCache import mozilla.components.support.utils.BuildManufacturerChecker import mozilla.components.support.utils.SafeIntent import mozilla.components.support.utils.TorUtils +import mozilla.components.support.utils.ext.getParcelableExtraCompat import mozilla.components.support.utils.toSafeIntent import mozilla.components.support.webextensions.WebExtensionPopupObserver import mozilla.telemetry.glean.private.NoExtras @@ -171,13 +173,23 @@ import org.mozilla.fenix.tabstray.TabsTrayFragment import org.mozilla.fenix.theme.DefaultThemeManager import org.mozilla.fenix.theme.StatusBarColorManager import org.mozilla.fenix.theme.ThemeManager -import org.mozilla.fenix.tor.TorEvents +import org.mozilla.fenix.tor.TorConnectionAssistFragmentDirections import org.mozilla.fenix.utils.AccessibilityUtils.announcePrivateModeForAccessibility import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.changeAppLauncherIcon import java.util.Locale import mozilla.components.ui.icons.R as iconsR +import mozilla.components.browser.engine.gecko.GeckoEngine +import org.mozilla.fenix.compose.core.Action +import org.mozilla.fenix.compose.snackbar.SnackbarState +import org.mozilla.fenix.compose.snackbar.Snackbar +import org.mozilla.fenix.tor.UrlQuickLoadViewModel +import org.mozilla.geckoview.TorAndroidIntegration +import org.mozilla.geckoview.TorAndroidIntegration.BootstrapStateChangeListener +import org.mozilla.geckoview.TorConnectStage +import kotlin.system.exitProcess + /** * The main activity of the application. The application is primarily a single Activity (this one) * with fragments switching out to display different views. The most important views shown here are the: @@ -185,7 +197,7 @@ import mozilla.components.ui.icons.R as iconsR * - browser screen */ @SuppressWarnings("TooManyFunctions", "LargeClass", "LongMethod") -open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { +open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity, TorAndroidIntegration.BootstrapStateChangeListener { @VisibleForTesting internal lateinit var binding: ActivityHomeBinding lateinit var themeManager: ThemeManager @@ -367,6 +379,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { private var dialog: RedirectDialogFragment? = null + private val urlQuickLoadViewModel: UrlQuickLoadViewModel by viewModels() + @Suppress("CognitiveComplexMethod", "CyclomaticComplexMethod") final override fun onCreate(savedInstanceState: Bundle?) { // DO NOT MOVE ANYTHING ABOVE THIS getProfilerTime CALL. @@ -597,6 +611,14 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { onBackPressedCallback = onBackPressedCallback, ) + if (settings().useHtmlConnectionUi) { + val engine = components.core.engine + if (engine is GeckoEngine) { + val torIntegration = engine.getTorIntegrationController() + torIntegration.registerBootstrapStateChangeListener(this) + } + } + StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE. } @@ -809,7 +831,9 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { override fun onProvideAssistContent(outContent: AssistContent?) { super.onProvideAssistContent(outContent) val currentTabUrl = components.core.store.state.selectedTab?.content?.url - outContent?.webUri = currentTabUrl?.let { it.toUri() } + if (components.core.store.state.selectedTab?.content?.private == false) { + outContent?.webUri = currentTabUrl?.let { it.toUri() } + } } @CallSuper @@ -844,6 +868,16 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { stopMediaSession() } + if (applicationContext.components.notificationsDelegate.shouldShutDownWithOnDestroyWhenIsFinishing) { + if (isFinishing) { + shutDown() + } + } else { + // We only want to not shut down when the notification is swiped away, + // if we do not reset this value + applicationContext.components.notificationsDelegate.shouldShutDownWithOnDestroyWhenIsFinishing = true + } + components.core.engine.profiler?.addMarker( MarkersActivityLifecycleCallbacks.MARKER_NAME, startTimeProfiler, @@ -908,15 +942,21 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { onNewIntentInternal(intent) } else { // Wait until Tor is connected to handle intents from external apps for links, search, etc. - components.torController.registerTorListener(object : TorEvents { - override fun onTorConnected() { - components.torController.unregisterTorListener(this) - onNewIntentInternal(intent) + val torIntegration = (components.core.engine as GeckoEngine).getTorIntegrationController() + torIntegration.registerBootstrapStateChangeListener( + object : BootstrapStateChangeListener { + + override fun onBootstrapStageChange(stage: TorConnectStage) { + if (stage.isBootstrapped) { + torIntegration.unregisterBootstrapStateChangeListener(this) + onNewIntentInternal(intent) + } + } + + override fun onBootstrapProgress(progress: Double, hasWarnings: Boolean) {} } - override fun onTorConnecting() { /* no-op */ } - override fun onTorStopped() { /* no-op */ } - override fun onTorStatusUpdate(entry: String?, status: String?, progress: Double?) { /* no-op */ } - }) + ) + return } } @@ -1285,6 +1325,30 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { historyMetadata: HistoryMetadataKey? = null, additionalHeaders: Map<String, String>? = null, ) { + if (!components.torController.isBootstrapped && !searchTermOrURL.startsWith("about:")) { + Snackbar.make( + snackBarParentView = binding.root, + snackbarState = SnackbarState( + message = getString(R.string.connection_assist_connect_to_tor_before_opening_links), + duration = SnackbarState.Duration.Preset.Long, + action = Action( + label = getString(R.string.connection_assist_connect_to_tor_before_opening_links_confirmation), + onClick = { + urlQuickLoadViewModel.urlToLoadAfterConnecting.value = searchTermOrURL + urlQuickLoadViewModel.maybeBeginBootstrap() + if (navHost.navController.previousBackStackEntry?.destination?.id == R.id.torConnectionAssistFragment) { + supportFragmentManager.popBackStack() + } else { + navHost.navController.navigate( + TorConnectionAssistFragmentDirections.actionConnectToTorBeforeOpeningLinks(), + ) + } + }, + ), + ), + ).show() + return + } openToBrowser(from, customTabSessionId) components.useCases.fenixBrowserUseCases.loadUrlOrSearch( @@ -1328,7 +1392,16 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { // return // } - navController.navigate(NavGraphDirections.actionStartupTorbootstrap()) + if (!settings().useHtmlConnectionUi) { + navController.navigate(NavGraphDirections.actionStartupTorConnectionAssist()) + } else { + navController.navigate(NavGraphDirections.actionStartupHome()) + openToBrowserAndLoad( + searchTermOrURL = "about:torconnect", + newTab = true, + from = BrowserDirection.FromHome, + ) + } } final override fun attachBaseContext(base: Context) { @@ -1521,4 +1594,28 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { private const val REQUEST_CODE_CAMERA_PERMISSIONS = 1 } + + fun restartApplication() { + startActivity( + Intent(applicationContext, HomeActivity::class.java).addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK, + ), + ) + shutDown() + } + + fun shutDown() : Nothing { + finishAndRemoveTask() + exitProcess(0) + } + + override fun onBootstrapStageChange(stage: TorConnectStage) { + if (stage.isBootstrapped) { + if (settings().useHtmlConnectionUi) { + components.useCases.tabsUseCases.removeAllTabs() + navHost.navController.navigate(NavGraphDirections.actionStartupHome()) + } + } + } + override fun onBootstrapProgress(progress: Double, hasWarnings: Boolean) = Unit } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -1970,6 +1970,10 @@ abstract class BaseBrowserFragment : @Suppress("DEPRECATION") it.announceForAccessibility(selectedTab.toDisplayTitle()) + if (getCurrentTab()?.content?.url == "about:torconnect") { + // FIXME: view is not available anymore. + // browserToolbarView.view.visibility = View.GONE + } } } else { view?.let { view -> initializeUI(view) } @@ -2003,6 +2007,26 @@ abstract class BaseBrowserFragment : ), ) } + + handleBetaHtmlTorConnect() + } + + private fun handleBetaHtmlTorConnect() { + val currentTab = getCurrentTab() ?: return + if (currentTab.content.url == "about:torconnect") { + if (!requireActivity().settings().useHtmlConnectionUi) { + requireContext().components.useCases.tabsUseCases.removeTab(currentTab.id) + (requireActivity() as HomeActivity).navigateToHome( + findNavController(), + ) + } else { + // This just makes it not flash (be visible for a split second) before handleTabSelected() hides it again + // FIXME: view is not available anymore. + // browserToolbarView.view.visibility = View.GONE + } + } else if (currentTab.content.url == "about:tor") { + requireContext().components.useCases.tabsUseCases.removeTab(currentTab.id) + } } private fun evaluateMessagesForMicrosurvey(components: Components) = diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMenuController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMenuController.kt @@ -170,6 +170,7 @@ class DefaultBrowserToolbarMenuController( } is ToolbarMenu.Item.Quit -> { deleteAndQuit(activity) + activity.shutDown() } is ToolbarMenu.Item.CustomizeReaderView -> { readerModeController.showControls() diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Activity.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Activity.kt @@ -61,6 +61,7 @@ import org.mozilla.fenix.settings.wallpaper.WallpaperSettingsFragmentDirections import org.mozilla.fenix.share.AddNewDeviceFragmentDirections import org.mozilla.fenix.tabstray.TabsTrayFragmentDirections import org.mozilla.fenix.tabstray.ui.TabManagementFragmentDirections +import org.mozilla.fenix.tor.TorConnectionAssistFragmentDirections import org.mozilla.fenix.trackingprotection.TrackingProtectionPanelDialogFragmentDirections import org.mozilla.fenix.translations.TranslationsDialogFragmentDirections import org.mozilla.fenix.translations.preferences.downloadlanguages.DownloadLanguagesPreferenceFragmentDirections @@ -350,6 +351,9 @@ private fun getHomeNavDirections( BrowserDirection.FromWebCompatReporterFragment -> WebCompatReporterFragmentDirections.actionGlobalBrowser() + + BrowserDirection.FromTorConnectionAssistFragment -> + TorConnectionAssistFragmentDirections.actionGlobalBrowser() } const val REQUEST_CODE_BROWSER_ROLE = 1 diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -66,6 +66,7 @@ import mozilla.components.feature.top.sites.TopSitesFeature import mozilla.components.lib.state.ext.consumeFlow import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.flow +import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.utils.KeyboardState import mozilla.components.support.utils.keyboardAsState @@ -182,9 +183,10 @@ import java.lang.ref.WeakReference import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.tor.TorHomePage +import org.mozilla.fenix.tor.UrlQuickLoadViewModel @Suppress("TooManyFunctions", "LargeClass") -class HomeFragment : Fragment() { +class HomeFragment : Fragment(), UserInteractionHandler { private val args by navArgs<HomeFragmentArgs>() @VisibleForTesting @@ -197,6 +199,7 @@ class HomeFragment : Fragment() { private val snackbarBinding = ViewBoundFeatureWrapper<SnackbarBinding>() private val homeViewModel: HomeScreenViewModel by activityViewModels() + private val urlQuickLoadViewModel: UrlQuickLoadViewModel by activityViewModels() @VisibleForTesting internal var homeNavigationBar: HomeNavigationBar? = null @@ -1010,6 +1013,18 @@ class HomeFragment : Fragment() { view = view, ) + urlQuickLoadViewModel.urlToLoadAfterConnecting.observe(viewLifecycleOwner) { + if (!it.isNullOrBlank()) { + (requireActivity() as HomeActivity).openToBrowserAndLoad( + searchTermOrURL = it, + newTab = true, + from = BrowserDirection.FromHome, + ) + // Only load this url once + urlQuickLoadViewModel.urlToLoadAfterConnecting.value = null + } + } + // DO NOT MOVE ANYTHING BELOW THIS addMarker CALL! requireComponents.core.engine.profiler?.addMarker( MarkersFragmentLifecycleCallbacks.MARKER_NAME, @@ -1494,4 +1509,8 @@ class HomeFragment : Fragment() { private const val ENCOURAGE_SEARCH_CFR_VERTICAL_OFFSET = 0 } + + override fun onBackPressed(): Boolean { + (requireActivity() as HomeActivity).shutDown() + } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeMenuView.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeMenuView.kt @@ -206,6 +206,7 @@ class HomeMenuView( homeActivity.finishAndRemoveTask() } } + homeActivity.shutDown() } HomeMenu.Item.ReconnectSync -> { navController.nav( diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/session/PrivateNotificationService.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/session/PrivateNotificationService.kt @@ -79,6 +79,7 @@ class PrivateNotificationService : AbstractPrivateNotificationService() { @SuppressLint("MissingSuperCall") override fun erasePrivateTabs() { val inPrivateMode = store.state.selectedTab?.content?.private ?: false + notificationsDelegate.shouldShutDownWithOnDestroyWhenIsFinishing = false // Trigger use case directly for now (instead of calling super.erasePrivateTabs) // as otherwise SessionManager and the store will be out of sync. diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt @@ -42,6 +42,7 @@ import mozilla.components.concept.sync.OAuthAccount import mozilla.components.concept.sync.Profile import mozilla.components.feature.addons.ui.AddonFilePicker import mozilla.components.support.base.feature.ViewBoundFeatureWrapper +import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.ktx.android.view.showKeyboard import mozilla.components.ui.widgets.withCenterAlignedButtons import mozilla.telemetry.glean.private.NoExtras @@ -82,7 +83,7 @@ import kotlin.system.exitProcess import org.mozilla.fenix.GleanMetrics.Settings as SettingsMetrics @Suppress("LargeClass", "TooManyFunctions") -class SettingsFragment : PreferenceFragmentCompat() { +class SettingsFragment : PreferenceFragmentCompat(), UserInteractionHandler { private val args by navArgs<SettingsFragmentArgs>() private lateinit var accountUiView: AccountUiView @@ -372,9 +373,9 @@ class SettingsFragment : PreferenceFragmentCompat() { SettingsFragmentDirections.actionSettingsFragmentToTabsSettingsFragment() } - resources.getString(R.string.pref_key_home) -> { - SettingsFragmentDirections.actionSettingsFragmentToHomeSettingsFragment() - } + // resources.getString(R.string.pref_key_home) -> { + // SettingsFragmentDirections.actionSettingsFragmentToHomeSettingsFragment() + // } resources.getString(R.string.pref_key_customize) -> { SettingsFragmentDirections.actionSettingsFragmentToCustomizationFragment() @@ -738,7 +739,7 @@ class SettingsFragment : PreferenceFragmentCompat() { @VisibleForTesting internal fun setupHomepagePreference(settings: Settings) { - with(requirePreference<Preference>(R.string.pref_key_home)) { + /*with(requirePreference<Preference>(R.string.pref_key_home)) { summary = when { settings.alwaysOpenTheHomepageWhenOpeningTheApp -> getString(R.string.opening_screen_homepage_summary) @@ -751,7 +752,7 @@ class SettingsFragment : PreferenceFragmentCompat() { else -> null } - } + }*/ } @VisibleForTesting @@ -927,4 +928,18 @@ class SettingsFragment : PreferenceFragmentCompat() { private const val FXA_SYNC_OVERRIDE_EXIT_DELAY = 2000L private const val AMO_COLLECTION_OVERRIDE_EXIT_DELAY = 3000L } + + override fun onBackPressed(): Boolean { + // If tor is already bootstrapped, skip going back to [TorConnectionAssistFragment] and instead go directly to [HomeFragment] + if (requireComponents.torController.isBootstrapped) { + val navController = findNavController() + if (navController.previousBackStackEntry?.destination?.id == R.id.torConnectionAssistFragment) { + navController.navigate( + SettingsFragmentDirections.actionGlobalHomeFragment(), + ) + return true + } + } + return false + } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/ConnectAssistUiState.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/ConnectAssistUiState.kt @@ -0,0 +1,302 @@ +package org.mozilla.fenix.tor + +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import mozilla.components.lib.crash.R as crashR +import org.mozilla.fenix.R + +enum class ConnectAssistUiState( + val progressBarVisible: Boolean, + @param:ColorRes val progressBackgroundTintColorResource: Int = R.color.progress_background_tint, + val backButtonVisible: Boolean, + val settingsButtonVisible: Boolean, + val torConnectImageVisible: Boolean, + @param:DrawableRes val torConnectImageResource: Int = R.drawable.connect, + val titleLargeTextViewVisible: Boolean, + @param:StringRes val titleLargeTextViewTextStringResource: Int = R.string.connection_assist_tor_connect_title, + val titleDescriptionVisible: Boolean, + @param:StringRes val learnMoreStringResource: Int? = null, + @param:StringRes val internetErrorDescription: Int? = null, + @param:StringRes val internetErrorDescription1: Int? = null, + @param:StringRes val internetErrorDescription2: Int? = null, + @param:StringRes val titleDescriptionTextStringResource: Int? = R.string.preferences_tor_network_settings_explanation, + val quickstartSwitchVisible: Boolean, + val regionDropDownVisible: Boolean, + @param:StringRes val regionDropDownDefaultItem: Int = R.string.connection_assist_automatic_country_detection, + val torBootstrapButton1Visible: Boolean, + @param:StringRes val torBootstrapButton1TextStringResource: Int = R.string.tor_bootstrap_connect, + val torBootstrapButton1ShouldTryABridge: Boolean = false, + val torBootstrapButton1ShouldOpenSettings: Boolean = false, + val torBootstrapButton2Visible: Boolean, + @param:StringRes val torBootstrapButton2TextStringResource: Int? = R.string.connection_assist_configure_connection_button, + val torBootstrapButton2ShouldOpenSettings: Boolean = true, + val wordmarkLogoVisible: Boolean = false, + val torBootstrapButton2ShouldRestartApp: Boolean = false, +) { + Loading( + progressBarVisible = false, + backButtonVisible = false, + settingsButtonVisible = false, + torConnectImageVisible = false, + titleLargeTextViewVisible = false, + titleDescriptionVisible = false, + quickstartSwitchVisible = false, + regionDropDownVisible = false, + torBootstrapButton1Visible = false, + torBootstrapButton2Visible = false, + wordmarkLogoVisible = true, + ), + Start( + progressBarVisible = false, + backButtonVisible = false, + settingsButtonVisible = true, + torConnectImageVisible = true, + torConnectImageResource = R.drawable.connect, + titleLargeTextViewVisible = true, + titleLargeTextViewTextStringResource = R.string.connection_assist_tor_connect_title, + titleDescriptionVisible = true, + titleDescriptionTextStringResource = R.string.preferences_tor_network_settings_explanation, + quickstartSwitchVisible = true, + regionDropDownVisible = false, + torBootstrapButton1Visible = true, + torBootstrapButton2Visible = true, + torBootstrapButton2TextStringResource = R.string.connection_assist_configure_connection_button, + torBootstrapButton2ShouldOpenSettings = true, + ), + Bootstrapping( + progressBarVisible = true, + backButtonVisible = false, + settingsButtonVisible = true, + torConnectImageVisible = true, + torConnectImageResource = R.drawable.connect, + titleLargeTextViewVisible = true, + titleLargeTextViewTextStringResource = R.string.connection_assist_connecting_title, + titleDescriptionVisible = true, + titleDescriptionTextStringResource = R.string.preferences_tor_network_settings_explanation, + quickstartSwitchVisible = true, + regionDropDownVisible = false, + torBootstrapButton1Visible = false, + torBootstrapButton2Visible = true, + torBootstrapButton2TextStringResource = R.string.btn_cancel, + torBootstrapButton2ShouldOpenSettings = false, + ), + Offline( + progressBarVisible = true, + progressBackgroundTintColorResource = R.color.warning_yellow, + backButtonVisible = true, + settingsButtonVisible = true, + torConnectImageVisible = true, + torConnectImageResource = R.drawable.globe_broken, + titleLargeTextViewVisible = true, + titleLargeTextViewTextStringResource = R.string.connection_assist_internet_error_title, + titleDescriptionVisible = true, + learnMoreStringResource = R.string.connection_assist_internet_error_learn_more, + internetErrorDescription = R.string.connection_assist_internet_error_description, + titleDescriptionTextStringResource = null, + quickstartSwitchVisible = false, + regionDropDownVisible = false, + torBootstrapButton1Visible = true, + torBootstrapButton1TextStringResource = R.string.connection_assist_internet_error_try_again, + torBootstrapButton2Visible = true, + torBootstrapButton2TextStringResource = R.string.connection_assist_configure_connection_button, + torBootstrapButton2ShouldOpenSettings = true, + ), + TryingAgain( + progressBarVisible = true, + backButtonVisible = true, + settingsButtonVisible = true, + torConnectImageVisible = true, + torConnectImageResource = R.drawable.connect, + titleLargeTextViewVisible = true, + titleLargeTextViewTextStringResource = R.string.connection_assist_trying_again_waiting_title, + titleDescriptionVisible = true, + learnMoreStringResource = R.string.connection_assist_internet_error_learn_more, + internetErrorDescription = R.string.connection_assist_internet_error_description, + titleDescriptionTextStringResource = null, + quickstartSwitchVisible = false, + regionDropDownVisible = false, + torBootstrapButton1Visible = false, + torBootstrapButton2Visible = true, + torBootstrapButton2TextStringResource = R.string.btn_cancel, + torBootstrapButton2ShouldOpenSettings = false, + ), + ChooseRegion( + progressBarVisible = true, + progressBackgroundTintColorResource = R.color.warning_yellow, + backButtonVisible = true, + settingsButtonVisible = true, + torConnectImageVisible = true, + torConnectImageResource = R.drawable.connect_broken, + titleLargeTextViewVisible = true, + titleLargeTextViewTextStringResource = R.string.connection_assist_cant_connect_to_tor_title, + titleDescriptionVisible = true, + learnMoreStringResource = R.string.connection_assist_internet_error_learn_more, + internetErrorDescription = R.string.connection_assist_try_a_bridge_description, + titleDescriptionTextStringResource = null, + quickstartSwitchVisible = false, + regionDropDownVisible = true, + torBootstrapButton1Visible = true, + torBootstrapButton1TextStringResource = R.string.connection_assist_try_a_bridge_button, + torBootstrapButton1ShouldTryABridge = true, + torBootstrapButton2Visible = false, + torBootstrapButton2TextStringResource = null, + torBootstrapButton2ShouldOpenSettings = true, + ), + TryingABridge( + progressBarVisible = true, + backButtonVisible = true, + settingsButtonVisible = true, + torConnectImageVisible = true, + torConnectImageResource = R.drawable.connect, + titleLargeTextViewVisible = true, + titleLargeTextViewTextStringResource = R.string.connection_assist_trying_a_bridge_title, + titleDescriptionVisible = true, + learnMoreStringResource = R.string.connection_assist_internet_error_learn_more, + internetErrorDescription = ChooseRegion.internetErrorDescription, + titleDescriptionTextStringResource = null, + quickstartSwitchVisible = true, + regionDropDownVisible = false, + torBootstrapButton1Visible = false, + torBootstrapButton2Visible = true, + torBootstrapButton2TextStringResource = R.string.btn_cancel, + torBootstrapButton2ShouldOpenSettings = false, + ), + RegionNotFound( + progressBarVisible = true, + progressBackgroundTintColorResource = R.color.warning_yellow, + backButtonVisible = true, + settingsButtonVisible = true, + torConnectImageVisible = true, + torConnectImageResource = R.drawable.browser_location, + titleLargeTextViewVisible = true, + titleLargeTextViewTextStringResource = R.string.connection_assist_location_error_title, + titleDescriptionVisible = true, + learnMoreStringResource = R.string.connection_assist_location_error_learn_more_link, + internetErrorDescription = R.string.connection_assist_location_error_description, + internetErrorDescription1 = R.string.connection_assist_find_bridge_location_description, + internetErrorDescription2 = R.string.connection_assist_select_country_try_again, + titleDescriptionTextStringResource = null, + quickstartSwitchVisible = false, + regionDropDownVisible = true, + regionDropDownDefaultItem = R.string.connection_assist_select_country_or_region, + torBootstrapButton1Visible = true, + torBootstrapButton1TextStringResource = R.string.connection_assist_try_a_bridge_button, + torBootstrapButton1ShouldTryABridge = true, + torBootstrapButton2Visible = false, + torBootstrapButton2TextStringResource = null, + torBootstrapButton2ShouldOpenSettings = true, + ), + TryingABridgeRegionNotFound( + progressBarVisible = true, + backButtonVisible = true, + settingsButtonVisible = true, + torConnectImageVisible = true, + torConnectImageResource = R.drawable.connect, + titleLargeTextViewVisible = true, + titleLargeTextViewTextStringResource = R.string.connection_assist_trying_a_bridge_title, + titleDescriptionVisible = true, + learnMoreStringResource = R.string.connection_assist_internet_error_learn_more, + internetErrorDescription = RegionNotFound.internetErrorDescription, + internetErrorDescription1 = RegionNotFound.internetErrorDescription1, + internetErrorDescription2 = RegionNotFound.internetErrorDescription2, + titleDescriptionTextStringResource = null, + quickstartSwitchVisible = true, + regionDropDownVisible = false, + torBootstrapButton1Visible = false, + torBootstrapButton2Visible = true, + torBootstrapButton2TextStringResource = R.string.btn_cancel, + torBootstrapButton2ShouldOpenSettings = false, + ), + ConfirmRegion( + progressBarVisible = true, + progressBackgroundTintColorResource = R.color.warning_yellow, + backButtonVisible = true, + settingsButtonVisible = true, + torConnectImageVisible = true, + torConnectImageResource = R.drawable.browser_location, + titleLargeTextViewVisible = true, + titleLargeTextViewTextStringResource = R.string.connection_assist_location_check_title, + titleDescriptionVisible = true, + learnMoreStringResource = R.string.connection_assist_location_error_learn_more_link, + internetErrorDescription = R.string.connection_assist_location_error_description, + internetErrorDescription1 = R.string.connection_assist_find_bridge_location_description, + internetErrorDescription2 = R.string.connection_assist_select_country_try_again, + titleDescriptionTextStringResource = null, + quickstartSwitchVisible = false, + regionDropDownVisible = true, + regionDropDownDefaultItem = R.string.connection_assist_select_country_or_region, + torBootstrapButton1Visible = true, + torBootstrapButton1TextStringResource = R.string.connection_assist_try_a_bridge_button, + torBootstrapButton1ShouldTryABridge = true, + torBootstrapButton2Visible = false, + torBootstrapButton2TextStringResource = null, + torBootstrapButton2ShouldOpenSettings = true, + ), + TryingABridgeConfirmRegion( + progressBarVisible = true, + backButtonVisible = true, + settingsButtonVisible = true, + torConnectImageVisible = true, + torConnectImageResource = R.drawable.connect, + titleLargeTextViewVisible = true, + titleLargeTextViewTextStringResource = R.string.connection_assist_trying_a_bridge_title, + titleDescriptionVisible = true, + learnMoreStringResource = R.string.connection_assist_internet_error_learn_more, + internetErrorDescription = ConfirmRegion.internetErrorDescription, + internetErrorDescription1 = ConfirmRegion.internetErrorDescription1, + internetErrorDescription2 = ConfirmRegion.internetErrorDescription2, + titleDescriptionTextStringResource = null, + quickstartSwitchVisible = true, + regionDropDownVisible = false, + torBootstrapButton1Visible = false, + torBootstrapButton2Visible = true, + torBootstrapButton2TextStringResource = R.string.btn_cancel, + torBootstrapButton2ShouldOpenSettings = false, + ), + LastTry( + progressBarVisible = true, + backButtonVisible = true, + settingsButtonVisible = true, + torConnectImageVisible = true, + torConnectImageResource = R.drawable.connect, + titleLargeTextViewVisible = true, + titleLargeTextViewTextStringResource = R.string.connection_assist_last_try_title, + titleDescriptionVisible = true, + learnMoreStringResource = R.string.connection_assist_location_error_learn_more_link, + internetErrorDescription = R.string.connection_assist_location_error_description, + internetErrorDescription1 = R.string.connection_assist_find_bridge_location_description, + internetErrorDescription2 = R.string.connection_assist_select_country_try_again, + titleDescriptionTextStringResource = null, + quickstartSwitchVisible = true, + regionDropDownVisible = false, + torBootstrapButton1Visible = false, + torBootstrapButton2Visible = true, + torBootstrapButton2TextStringResource = R.string.btn_cancel, + torBootstrapButton2ShouldOpenSettings = false, + ), + FinalError( + progressBarVisible = true, + progressBackgroundTintColorResource = R.color.warning_yellow, + backButtonVisible = true, + settingsButtonVisible = true, + torConnectImageVisible = true, + torConnectImageResource = R.drawable.connect_broken, + titleLargeTextViewVisible = true, + titleLargeTextViewTextStringResource = R.string.connection_assist_final_error_title, + titleDescriptionVisible = true, + learnMoreStringResource = R.string.connection_assist_final_error_learn_more_link, + internetErrorDescription = R.string.connection_assist_final_error_description1, + internetErrorDescription1 = R.string.connection_assist_final_error_troubleshoot_connection_link, + titleDescriptionTextStringResource = null, + quickstartSwitchVisible = false, + regionDropDownVisible = false, + torBootstrapButton1Visible = true, + torBootstrapButton1TextStringResource = R.string.connection_assist_configure_connection_button, + torBootstrapButton1ShouldOpenSettings = true, + torBootstrapButton2Visible = true, + torBootstrapButton2TextStringResource = crashR.string.mozac_lib_crash_dialog_button_restart, + torBootstrapButton2ShouldOpenSettings = false, + torBootstrapButton2ShouldRestartApp = true, + ) +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/QuickstartViewModel.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/QuickstartViewModel.kt @@ -0,0 +1,44 @@ +package org.mozilla.fenix.tor + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import mozilla.components.browser.engine.gecko.GeckoEngine +import org.mozilla.fenix.ext.components + +class QuickstartViewModel( + application: Application, +) : AndroidViewModel(application) { + + private val components = getApplication<Application>().components + private val torAndroidIntegration = + (components.core.engine as GeckoEngine).getTorIntegrationController() + + /** + * NOTE: Whilst the initial value for _quickstart is fetched from + * TorAndroidIntegration.quickstartGet (which is surfaced from TorConnect.quickstart), and we + * pass on any changes in value up to TorConnect.quickstart (via quickstartSet()), we do not + * listen for any changes to the TorConnect.quickstart value via "QuickstartChange" because we + * do not expect anything outside of TorConnectViewModel to change its value, so we expect its + * value to remain in sync with our local value. + */ + init { + torAndroidIntegration.quickstartGet { + _quickstart.value = it + components.settings.quickStart = it + } + } + + private val _quickstart = MutableLiveData(components.settings.quickStart) + fun quickstart(): LiveData<Boolean> { + return _quickstart + } + + fun quickstartSet(value: Boolean) { + torAndroidIntegration.quickstartSet(value) + _quickstart.value = value + components.settings.quickStart = value + } + +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/TorBootstrapProgressViewModel.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/TorBootstrapProgressViewModel.kt @@ -0,0 +1,35 @@ +package org.mozilla.fenix.tor + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import org.mozilla.fenix.ext.components +import org.mozilla.geckoview.TorAndroidIntegration.BootstrapStateChangeListener +import org.mozilla.geckoview.TorConnectStage + +class TorBootstrapProgressViewModel( + application: Application, +) : AndroidViewModel(application), BootstrapStateChangeListener { + + private val torAndroidIntegration = + application.components.core.geckoRuntime.torIntegrationController + + val progress: MutableLiveData<Int> by lazy { + MutableLiveData<Int>(0) + } + + init { + torAndroidIntegration.registerBootstrapStateChangeListener(this) + } + + override fun onCleared() { + torAndroidIntegration.unregisterBootstrapStateChangeListener(this) + super.onCleared() + } + + override fun onBootstrapStageChange(stage: TorConnectStage) = Unit + + override fun onBootstrapProgress(progress: Double, hasWarnings: Boolean) { + this.progress.value = progress.toInt() + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/TorConnectionAssistFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/TorConnectionAssistFragment.kt @@ -0,0 +1,413 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.tor + +import android.content.Intent +import android.graphics.Color +import android.os.Build +import android.os.Bundle +import android.text.SpannableString +import android.text.Spanned +import android.text.TextPaint +import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.view.isEmpty +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.fragment.findNavController +import kotlinx.coroutines.launch +import mozilla.components.support.base.feature.UserInteractionHandler +import mozilla.components.ui.colors.R as colorsR +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.R +import org.mozilla.fenix.databinding.FragmentTorConnectionAssistBinding +import org.mozilla.fenix.ext.hideToolbar + +class TorConnectionAssistFragment : Fragment(), UserInteractionHandler { + + private val TAG = "TorConnectionAssistFrag" + private val progressViewModel: TorBootstrapProgressViewModel by viewModels() + private val quickstartViewModel: QuickstartViewModel by activityViewModels() + private val torConnectionAssistViewModel : TorConnectionAssistViewModel by viewModels() + + private var _binding: FragmentTorConnectionAssistBinding? = null + private val binding get() = _binding!! + + private lateinit var regionDropDownSpinnerAdapter: ArrayAdapter<String> + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + _binding = FragmentTorConnectionAssistBinding.inflate( + inflater, container, false, + ) + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + torConnectionAssistViewModel.collectTorConnectStage() + } + } + + torConnectionAssistViewModel.shouldOpenHome.observe(viewLifecycleOwner) { + Log.d(TAG, "shouldOpenHome = $it") + if (it) { + openHome() + } + } + + return binding.root + } + + override fun onResume() { + super.onResume() + hideToolbar() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + torConnectionAssistViewModel.torConnectScreen.collect { screen -> + Log.d(TAG, "torConnectScreen is $screen") + showScreen(screen) + } + } + } + + quickstartViewModel.quickstart().observe( + viewLifecycleOwner, + ) { + binding.quickstartSwitch.isChecked = it + } + + progressViewModel.progress.observe( + viewLifecycleOwner, + ) { progress -> + setProgressBarCompat(progress) + } + + } + + private fun setProgressBarCompat(progress: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + binding.torBootstrapProgressBar.setProgress(progress, true) + } else { + binding.torBootstrapProgressBar.progress = progress + } + } + + private fun showScreen(screen: ConnectAssistUiState) { + setProgressBar(screen) + setSettingsButton(screen) + setBackButton(screen) + setTorConnectImage(screen) + setTitle(screen) + setQuickStart(screen) + updateRegionDropdown(screen) + setButton1(screen) + setButton2(screen) + setSplashLogo(screen) + } + + private fun setProgressBar(screen: ConnectAssistUiState) { + binding.torBootstrapProgressBar.visibility = + if (screen.progressBarVisible) View.VISIBLE else View.GONE + binding.torBootstrapProgressBar.progressBackgroundTintList = AppCompatResources.getColorStateList( + requireContext(), + screen.progressBackgroundTintColorResource, + ) + } + + private fun setSettingsButton(screen: ConnectAssistUiState) { + binding.settingsButton.visibility = if (screen.settingsButtonVisible) View.VISIBLE else View.GONE + binding.settingsButton.setOnClickListener { + openSettings() + } + } + + private fun setBackButton(screen: ConnectAssistUiState) { + binding.backButton.visibility = if (screen.backButtonVisible) View.VISIBLE else View.INVISIBLE + binding.backButton.setOnClickListener { + onBackPressed() + } + } + + private fun setTorConnectImage(screen: ConnectAssistUiState) { + binding.torConnectImage.visibility = if (screen.torConnectImageVisible) View.VISIBLE else View.GONE + binding.torConnectImage.setImageResource(screen.torConnectImageResource) + } + + private fun setTitle(screen: ConnectAssistUiState) { + binding.titleLargeTextView.visibility = + if (screen.titleLargeTextViewVisible) View.VISIBLE else View.GONE + binding.titleLargeTextView.text = getString(screen.titleLargeTextViewTextStringResource) + binding.titleDescription.visibility = + if (screen.titleDescriptionVisible) View.VISIBLE else View.GONE + if (screen.learnMoreStringResource != null && screen.internetErrorDescription != null) { + val learnMore: String = "" // getString(screen.learnMoreStringResource) tor-browser#43198 uncomment and add back once we have the "Learn more" screens for relevant pages + val internetErrorDescription: String = + if (screen.internetErrorDescription1 == null) { + getString( + screen.internetErrorDescription, + learnMore, + ) + } else if (screen.internetErrorDescription2 == null) { + getString( + screen.internetErrorDescription, + getString(screen.internetErrorDescription1), + learnMore, + ) + } else { + getString( + screen.internetErrorDescription, + getString(screen.internetErrorDescription1), + getString(screen.internetErrorDescription2), + learnMore, + ) + } + handleDescriptionWithClickable(internetErrorDescription, learnMore) + } else if (screen.titleDescriptionTextStringResource != null) { + binding.titleDescription.text = getString(screen.titleDescriptionTextStringResource) + } + } + + private fun setQuickStart(screen: ConnectAssistUiState) { + binding.quickstartSwitch.visibility = + if (screen.quickstartSwitchVisible) View.VISIBLE else View.GONE + binding.quickstartSwitch.setOnCheckedChangeListener { _, isChecked -> + quickstartViewModel.quickstartSet(isChecked) + } + } + + private fun updateRegionDropdown(screen: ConnectAssistUiState) { + if (screen.regionDropDownVisible) { + if (binding.countryDropDown.isEmpty()) { + regionDropDownSpinnerAdapter = initializeSpinner() + torConnectionAssistViewModel.fetchRegionNames() + } + + setFirstItemInCountryDropDown(getString(screen.regionDropDownDefaultItem)) + + if (screen == ConnectAssistUiState.ChooseRegion || screen == ConnectAssistUiState.ConfirmRegion || screen == ConnectAssistUiState.RegionNotFound) { + torConnectionAssistViewModel.selectDefaultRegion() + setDropDownSelectionToSelectedCountryCode() + } + + binding.unblockTheInternetInCountryDescription.visibility = View.VISIBLE + binding.countryDropDown.visibility = View.VISIBLE + } else { + binding.unblockTheInternetInCountryDescription.visibility = View.GONE + binding.countryDropDown.visibility = View.GONE + } + } + + private fun setDropDownSelectionToSelectedCountryCode() { + binding.countryDropDown.setSelection( + indexOfSelectedCountryCode(), + ) + } + + private fun indexOfSelectedCountryCode() : Int { + return torConnectionAssistViewModel.regionCodeNameMap.value?.keys?.indexOf( + torConnectionAssistViewModel.selectedCountryCode.value, + )?.plus(1) ?: 0 + } + + private fun setFirstItemInCountryDropDown(item: String) { + if (!regionDropDownSpinnerAdapter.isEmpty) { + regionDropDownSpinnerAdapter.remove(regionDropDownSpinnerAdapter.getItem(0)) + } + regionDropDownSpinnerAdapter.insert(item, 0) + } + + private fun initializeSpinner(): ArrayAdapter<String> { + val spinnerAdapter: ArrayAdapter<String> = + ArrayAdapter<String>( + requireContext(), + android.R.layout.simple_spinner_item, + android.R.id.text1, + ) + spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding.countryDropDown.adapter = spinnerAdapter + binding.countryDropDown.onItemSelectedListener = + object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long, + ) { + torConnectionAssistViewModel.setCountryCodeToSelectedItem(position) + updateButton1(torConnectionAssistViewModel.torConnectScreen.value) + } + + override fun onNothingSelected(parent: AdapterView<*>?) {} + } + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + torConnectionAssistViewModel.regionCodeNameMap.collect { + if (it != null) { + spinnerAdapter.clear() + spinnerAdapter.add(getString(torConnectionAssistViewModel.torConnectScreen.value.regionDropDownDefaultItem)) + spinnerAdapter.addAll(it.values) + } + } + } + } + + return spinnerAdapter + } + + private fun setButton1(screen: ConnectAssistUiState) { + binding.torBootstrapButton1.apply { + visibility = + if (screen.torBootstrapButton1Visible) View.VISIBLE else View.GONE + text = getString(screen.torBootstrapButton1TextStringResource) + setOnClickListener { + if (screen.torBootstrapButton1ShouldOpenSettings) { + openTorConnectionSettings() + } else { + torConnectionAssistViewModel.handleConnect(screen) + } + } + updateButton1(screen) + } + } + + private fun updateButton1(screen: ConnectAssistUiState) { + binding.torBootstrapButton1.apply { + if (!torConnectionAssistViewModel.button1ShouldBeDisabled(screen)) { + isEnabled = true + backgroundTintList = AppCompatResources.getColorStateList( + requireContext(), + R.color.connect_button_purple, + ) + setTextColor( + AppCompatResources.getColorStateList( + requireContext(), + colorsR.color.photonLightGrey05, + ), + ) + } else { + isEnabled = false + backgroundTintList = AppCompatResources.getColorStateList( + requireContext(), + R.color.disabled_connect_button_purple, + ) + setTextColor( + AppCompatResources.getColorStateList( + requireContext(), + R.color.disabled_text_gray_purple, + ), + ) + } + } + } + + private fun setButton2(screen: ConnectAssistUiState) { + binding.torBootstrapButton2.visibility = + if (screen.torBootstrapButton2Visible) View.VISIBLE else View.GONE + if (screen.torBootstrapButton2ShouldRestartApp) { + binding.torBootstrapButton2.text = + screen.torBootstrapButton2TextStringResource?.let { + getString( + it, + getString(R.string.app_name), + ) + } + } else { + binding.torBootstrapButton2.text = + screen.torBootstrapButton2TextStringResource?.let { + getString( + it, + ) + } + } + binding.torBootstrapButton2.setOnClickListener { + if (screen.torBootstrapButton2ShouldOpenSettings) { + openTorConnectionSettings() + } else if (screen.torBootstrapButton2ShouldRestartApp) { + (requireActivity() as HomeActivity).restartApplication() + } else { + torConnectionAssistViewModel.cancelTorBootstrap() + } + } + } + + private fun setSplashLogo(screen: ConnectAssistUiState) { + binding.wordmarkLogo.visibility = if (screen.wordmarkLogoVisible) View.VISIBLE else View.GONE + } + + /** + * from https://stackoverflow.com/questions/10696986/how-to-set-the-part-of-the-text-view-is-clickable + */ + private fun handleDescriptionWithClickable(errorDescription: String, learnMore: String) { + val errorDescriptionSpannableString = SpannableString(errorDescription) + val clickableSpan: ClickableSpan = object : ClickableSpan() { + override fun onClick(textView: View) { + showLearnMore() + } + + override fun updateDrawState(drawState: TextPaint) { + super.updateDrawState(drawState) + drawState.isUnderlineText = true + } + } + errorDescriptionSpannableString.setSpan( + clickableSpan, + errorDescription.length - learnMore.length, + errorDescription.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ) + binding.titleDescription.text = errorDescriptionSpannableString + binding.titleDescription.movementMethod = LinkMovementMethod.getInstance() + binding.titleDescription.highlightColor = Color.TRANSPARENT + } + + private fun showLearnMore() { + Log.d(TAG, "showLearnMore() tapped") + //TODO("Not yet implemented") + } + + private fun openHome() { + Log.d(TAG, "openHome()") + findNavController().navigate( + TorConnectionAssistFragmentDirections.actionHome(), + ) + } + + private fun openSettings(preferenceToScrollTo: String? = null) { + findNavController().navigate( + TorConnectionAssistFragmentDirections.actionTorConnectionAssistFragmentToSettingsFragment( + preferenceToScrollTo, + ), + ) + } + + private fun openTorConnectionSettings() { + openSettings(requireContext().getString(R.string.pref_key_connection)) + } + + override fun onBackPressed(): Boolean { + torConnectionAssistViewModel.handleBackButtonPressed(requireActivity() as HomeActivity) + return true + } + +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/TorConnectionAssistViewModel.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/TorConnectionAssistViewModel.kt @@ -0,0 +1,185 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.tor + +import android.app.Application +import android.util.Log +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import mozilla.components.browser.state.ext.getUrl +import mozilla.components.browser.state.state.recover.TabState +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.components +import org.mozilla.gecko.util.GeckoBundle +import org.mozilla.geckoview.TorAndroidIntegration.BootstrapStateChangeListener +import org.mozilla.geckoview.TorConnectStage +import org.mozilla.geckoview.TorConnectStageName + +class TorConnectionAssistViewModel( + application: Application, +) : AndroidViewModel(application), BootstrapStateChangeListener { + + private val TAG = "torConnectionAssistVM" + private val components = application.components + private val torAndroidIntegration = + components.core.geckoRuntime.torIntegrationController + + init { + torAndroidIntegration.registerBootstrapStateChangeListener(this) + loadAndUnloadDummyPage() + } + + private fun loadAndUnloadDummyPage() { + viewModelScope.launch(Dispatchers.IO) { + // Load local url (it just needs to begin with "about:" to get past filter) to initialize the browser, + // Domain fronting needs Services.io.getProtocolHandler("http")... to actually work, and it + // does not till the browser/engine is initialized, and this is so far the easiest way to do that. + // Load early here so that it is ready when needed if we get to the step where DF is invoked + // Then later remove it so it doesn't show for the user + components.useCases.tabsUseCases.addTab.invoke("about:") + // removeTabs doesn't work without a delay. + Thread.sleep(500) + // Remove loaded URL so it is never visible to the user + components.useCases.tabsUseCases.removeTabs.invoke( + components.core.store.state.tabs.filter { + it.getUrl() == "about:" || it.getUrl() == "about:blank" + }.map { it.id }, + ) + // recentlyClosedTabsStorage.value.removeAllTabs() doesn't seem to work, + // so instead we collect and iteratively remove all tabs from recent history. + // Nothing should ever show up in history so we remove everything, + // including old "about:" tabs that may have stacked up. + components.core.recentlyClosedTabsStorage.value.getTabs() + .collect { tabs: List<TabState> -> + for (tab in tabs) { + components.core.recentlyClosedTabsStorage.value.removeTab(tab) + } + } + } + } + + + fun fetchRegionNames() { + torAndroidIntegration.regionNamesGet { regionNames : GeckoBundle? -> + Log.d(TAG, "fetchRegionNames() returned $regionNames") + if (regionNames != null) { + val codes: Array<String> = regionNames.keys() + val regions = mutableMapOf<String, String>() + for (code in codes) { + regions[code] = regionNames.getString(code) + } + regionCodeNameMap.value = regions.toSortedMap(compareBy<String> { regions[it] }.thenBy { it }) + } + } + } + + override fun onCleared() { + torAndroidIntegration.unregisterBootstrapStateChangeListener(this) + super.onCleared() + } + + private val torConnectStage: MutableStateFlow<TorConnectStage?> by lazy { + MutableStateFlow(torAndroidIntegration.lastKnowStage.value) + } + + private val _torConnectScreen = MutableStateFlow(ConnectAssistUiState.Loading) + internal val torConnectScreen: StateFlow<ConnectAssistUiState> = _torConnectScreen + + val regionCodeNameMap: MutableStateFlow<Map<String, String>?> by lazy { + MutableStateFlow(null) + } + + val selectedCountryCode: MutableStateFlow<String> by lazy { + MutableStateFlow("automatic") + } + + fun selectDefaultRegion() { + selectedCountryCode.value = torConnectStage.value?.defaultRegion ?: "automatic" + } + + fun setCountryCodeToSelectedItem(position: Int) { + selectedCountryCode.value = + regionCodeNameMap.value?.keys?.toList() + ?.getOrNull(position - 1) ?: "automatic" + // position - 1 since we have the default/first value of automatic + Log.d(TAG, "selectedCountryCode = ${selectedCountryCode.value}") + } + + val shouldOpenHome: MutableLiveData<Boolean> by lazy { + MutableLiveData(false) + } + + fun handleConnect(screen: ConnectAssistUiState) { + if (screen.torBootstrapButton1ShouldTryABridge) { + Log.d(TAG, "beginAutoBootstrap with countryCode: ${selectedCountryCode.value}") + torAndroidIntegration.beginAutoBootstrap(selectedCountryCode.value) + } else { + Log.d(TAG, "beginBootstrap() on screen $screen") + torAndroidIntegration.beginBootstrap() + } + } + + fun cancelTorBootstrap() { + torAndroidIntegration.cancelBootstrap() + } + + suspend fun collectTorConnectStage() { + torConnectStage.collect { + Log.d(TAG, "torConnectStageName: ${it?.name}") + when (it?.name) { + TorConnectStageName.Disabled -> shouldOpenHome.value = true // TODO use TorConnect.enabled instead to determine this + TorConnectStageName.Loading -> _torConnectScreen.value = ConnectAssistUiState.Loading + TorConnectStageName.Start -> _torConnectScreen.value = ConnectAssistUiState.Start + TorConnectStageName.Bootstrapping -> _torConnectScreen.value = handleBootstrapTrigger(it.bootstrapTrigger) + TorConnectStageName.Offline -> _torConnectScreen.value = ConnectAssistUiState.Offline + TorConnectStageName.ChooseRegion -> _torConnectScreen.value = ConnectAssistUiState.ChooseRegion + TorConnectStageName.RegionNotFound -> _torConnectScreen.value = ConnectAssistUiState.RegionNotFound + TorConnectStageName.ConfirmRegion -> _torConnectScreen.value = ConnectAssistUiState.ConfirmRegion + TorConnectStageName.FinalError -> _torConnectScreen.value = ConnectAssistUiState.FinalError + TorConnectStageName.Bootstrapped -> shouldOpenHome.value = true + null -> {} + } + } + } + + private fun handleBootstrapTrigger(bootstrapTrigger: TorConnectStageName) : ConnectAssistUiState { + Log.d(TAG, "bootstrapTrigger: $bootstrapTrigger") + return when (bootstrapTrigger) { + TorConnectStageName.Start -> ConnectAssistUiState.Bootstrapping + TorConnectStageName.Offline -> ConnectAssistUiState.TryingAgain + TorConnectStageName.ChooseRegion -> ConnectAssistUiState.TryingABridge + TorConnectStageName.RegionNotFound -> ConnectAssistUiState.TryingABridgeRegionNotFound + TorConnectStageName.ConfirmRegion -> ConnectAssistUiState.TryingABridgeConfirmRegion + else -> { + Log.e(TAG, "Unexpected bootstrapTrigger of $bootstrapTrigger") + ConnectAssistUiState.TryingAgain + } + } + } + + fun handleBackButtonPressed(homeActivity: HomeActivity) { + when (torConnectScreen.value) { + ConnectAssistUiState.Loading -> homeActivity.shutDown() + ConnectAssistUiState.Start -> homeActivity.shutDown() + else -> torAndroidIntegration.startAgain() + } + } + + override fun onBootstrapStageChange(stage: TorConnectStage) { + torConnectStage.value = stage + } + + override fun onBootstrapProgress(progress: Double, hasWarnings: Boolean) {} + + fun button1ShouldBeDisabled(screen: ConnectAssistUiState): Boolean { + return selectedCountryCode.value == "automatic" && screen.regionDropDownDefaultItem == R.string.connection_assist_select_country_or_region + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/UrlQuickLoadViewModel.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/UrlQuickLoadViewModel.kt @@ -0,0 +1,26 @@ +package org.mozilla.fenix.tor + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import org.mozilla.fenix.ext.components +import org.mozilla.geckoview.TorConnectStageName + +class UrlQuickLoadViewModel(application: Application) : AndroidViewModel(application) { + + private val torAndroidIntegration = + application.components.core.geckoRuntime.torIntegrationController + + val urlToLoadAfterConnecting: MutableLiveData<String?> by lazy { + MutableLiveData<String?>(null) + } + + fun maybeBeginBootstrap() { + when (torAndroidIntegration.lastKnowStage.value?.name) { + TorConnectStageName.Offline -> torAndroidIntegration.beginBootstrap() + TorConnectStageName.Start -> torAndroidIntegration.beginBootstrap() + else -> {} + } + } + +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -2932,4 +2932,9 @@ class Settings( key = appContext.getPreferenceKey(R.string.pref_key_use_html_connection_ui), default = false, ) + + var quickStart by booleanPreference( + appContext.getPreferenceKey(R.string.pref_key_quick_start), + default = false, + ) } diff --git a/mobile/android/fenix/app/src/main/res/drawable/browser_location.xml b/mobile/android/fenix/app/src/main/res/drawable/browser_location.xml @@ -0,0 +1,17 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="40dp" + android:height="40dp" + android:viewportWidth="40" + android:viewportHeight="40"> + <group> + <clip-path + android:pathData="M0,0h40v40h-40z"/> + <path + android:pathData="M19.332,28.605C19.332,23.853 23.361,20 28.332,20C33.303,20 37.332,23.853 37.332,28.605C37.332,32.002 32.736,36.822 30.113,39.296C29.636,39.748 28.997,40 28.332,40C27.667,40 27.028,39.748 26.551,39.296C23.928,36.822 19.332,32.001 19.332,28.605ZM26.865,24.958C27.33,24.766 27.829,24.667 28.332,24.667C29.349,24.667 30.324,25.07 31.043,25.789C31.761,26.508 32.165,27.483 32.165,28.5C32.165,29.517 31.761,30.492 31.043,31.211C30.324,31.93 29.349,32.333 28.332,32.333C27.829,32.333 27.33,32.234 26.865,32.042C26.4,31.849 25.977,31.566 25.621,31.211C25.265,30.855 24.983,30.432 24.791,29.967C24.598,29.502 24.499,29.003 24.499,28.5C24.499,27.997 24.598,27.498 24.791,27.033C24.983,26.568 25.265,26.146 25.621,25.789C25.977,25.434 26.4,25.151 26.865,24.958Z" + android:fillColor="#FFA436" + android:fillType="evenOdd"/> + <path + android:pathData="M38.509,22.9C38.721,21.771 38.832,20.607 38.832,19.417C38.832,9.061 30.438,0.667 20.082,0.667C9.727,0.667 1.332,9.061 1.332,19.417C1.332,29.772 9.727,38.167 20.082,38.167C20.825,38.167 21.559,38.124 22.279,38.039C19.438,34.942 16.665,31.167 16.665,28.233C16.665,25.223 17.971,22.499 20.082,20.526V14.846C22.098,14.846 23.809,16.151 24.416,17.962C25.33,17.658 26.298,17.458 27.301,17.375C26.412,14.225 23.517,11.917 20.082,11.917L20.082,9.221C25.042,9.221 29.175,12.764 30.089,17.456C31.169,17.608 32.2,17.899 33.161,18.308C32.598,11.578 26.957,6.292 20.082,6.292V3.596C28.819,3.596 35.903,10.679 35.903,19.417C35.903,19.589 35.9,19.761 35.894,19.933C36.942,20.767 37.83,21.771 38.509,22.9Z" + android:fillColor="#FBFBFE"/> + </group> +</vector> diff --git a/mobile/android/fenix/app/src/main/res/drawable/connect.xml b/mobile/android/fenix/app/src/main/res/drawable/connect.xml @@ -0,0 +1,17 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="40dp" + android:height="40dp" + android:viewportWidth="40" + android:viewportHeight="40"> + <path + android:pathData="M20,1.253C9.647,1.253 1.253,9.647 1.253,20C1.253,28.967 7.547,36.46 15.96,38.307C16.84,38.5 17.733,38.633 18.653,38.693V24.373C16.787,23.8 15.427,22.06 15.427,20C15.427,17.473 17.473,15.427 20,15.427C22.527,15.427 24.573,17.473 24.573,20C24.573,22.06 23.213,23.8 21.347,24.373V38.693C22.267,38.633 23.16,38.5 24.04,38.307C32.453,36.46 38.747,28.967 38.747,20C38.747,9.647 30.353,1.253 20,1.253ZM24.04,35.293V26.32C26.12,24.987 27.5,22.653 27.5,20C27.5,15.86 24.14,12.5 20,12.5C15.86,12.5 12.5,15.86 12.5,20C12.5,22.653 13.88,24.987 15.96,26.32V35.293C9.18,33.513 4.18,27.347 4.18,20C4.18,11.26 11.26,4.18 20,4.18C28.74,4.18 35.82,11.26 35.82,20C35.82,27.347 30.82,33.513 24.04,35.293Z" + android:fillColor="#FBFBFE" + android:fillType="evenOdd"/> + <path + android:pathData="M20,6.873C12.753,6.873 6.873,12.753 6.873,20C6.873,25.84 10.687,30.787 15.96,32.487V29.36C12.34,27.8 9.807,24.193 9.807,20C9.807,14.367 14.367,9.807 20,9.807C25.633,9.807 30.193,14.367 30.193,20C30.193,24.193 27.66,27.8 24.04,29.36V32.487C29.313,30.787 33.127,25.84 33.127,20C33.127,12.753 27.247,6.873 20,6.873Z" + android:fillColor="#FBFBFE" + android:fillType="evenOdd"/> + <path + android:pathData="M20,22.1C21.16,22.1 22.1,21.159 22.1,20C22.1,18.84 21.16,17.9 20,17.9C18.84,17.9 17.9,18.84 17.9,20C17.9,21.159 18.84,22.1 20,22.1Z" + android:fillColor="#FBFBFE"/> +</vector> diff --git a/mobile/android/fenix/app/src/main/res/drawable/connect_broken.xml b/mobile/android/fenix/app/src/main/res/drawable/connect_broken.xml @@ -0,0 +1,37 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="40dp" + android:height="40dp" + android:viewportWidth="40" + android:viewportHeight="40"> + <group> + <clip-path + android:pathData="M0,0h40v40h-40z"/> + <path + android:pathData="M8.317,5.337C11.521,2.781 15.582,1.253 19.999,1.253C30.352,1.253 38.745,9.647 38.745,20C38.745,24.418 37.218,28.478 34.662,31.681L32.577,29.597C34.611,26.937 35.819,23.611 35.819,20C35.819,11.26 28.739,4.18 19.999,4.18C16.389,4.18 13.063,5.388 10.401,7.421L8.317,5.337Z" + android:fillColor="#FBFBFE"/> + <path + android:pathData="M5.89,7.656C3.002,10.954 1.252,15.273 1.252,20C1.252,28.967 7.545,36.46 15.959,38.307C16.839,38.5 17.732,38.633 18.652,38.693V24.373C16.785,23.8 15.425,22.06 15.425,20C15.425,19.19 15.635,18.43 16.004,17.771L13.887,15.653C13.013,16.88 12.499,18.38 12.499,20C12.499,22.653 13.879,24.987 15.959,26.32V35.293C9.179,33.513 4.179,27.347 4.179,20C4.179,16.08 5.603,12.493 7.963,9.73L5.89,7.656Z" + android:fillColor="#FBFBFE"/> + <path + android:pathData="M16.399,13.419L18.618,15.638C19.054,15.501 19.517,15.427 19.998,15.427C22.525,15.427 24.572,17.473 24.572,20C24.572,20.481 24.498,20.945 24.36,21.38L26.579,23.599C27.165,22.531 27.498,21.304 27.498,20C27.498,15.86 24.138,12.5 19.998,12.5C18.694,12.5 17.468,12.833 16.399,13.419Z" + android:fillColor="#FBFBFE"/> + <path + android:pathData="M24.349,26.112L22.232,23.995C21.954,24.151 21.658,24.278 21.349,24.373V38.693C22.269,38.633 23.162,38.5 24.042,38.307C27.176,37.619 30.015,36.147 32.345,34.109L30.271,32.034C28.492,33.552 26.372,34.681 24.042,35.293V26.32C24.146,26.253 24.249,26.184 24.349,26.112Z" + android:fillColor="#FBFBFE"/> + <path + android:pathData="M30.653,27.67C32.21,25.514 33.127,22.864 33.127,20C33.127,12.753 27.247,6.873 20,6.873C17.138,6.873 14.488,7.791 12.33,9.348L14.437,11.455C16.037,10.412 17.947,9.807 20,9.807C25.634,9.807 30.194,14.367 30.194,20C30.194,22.051 29.587,23.962 28.544,25.562L30.653,27.67Z" + android:fillColor="#FBFBFE"/> + <path + android:pathData="M26.272,28.037L28.357,30.121C27.095,31.163 25.635,31.973 24.041,32.487V29.36C24.844,29.014 25.593,28.568 26.272,28.037Z" + android:fillColor="#FBFBFE"/> + <path + android:pathData="M11.962,13.727L9.878,11.643C8.001,13.914 6.873,16.826 6.873,20C6.873,25.84 10.686,30.787 15.96,32.487V29.36C12.34,27.8 9.806,24.193 9.806,20C9.806,17.633 10.611,15.457 11.962,13.727Z" + android:fillColor="#FBFBFE"/> + <path + android:pathData="M17.922,19.688L20.311,22.077C20.21,22.092 20.105,22.1 19.999,22.1C18.84,22.1 17.899,21.16 17.899,20C17.899,19.894 17.907,19.79 17.922,19.688Z" + android:fillColor="#FBFBFE"/> + <path + android:pathData="M2.89,4.642L35.228,36.98C35.879,37.632 35.879,38.688 35.228,39.339L35.228,39.339C34.576,39.991 33.52,39.991 32.868,39.339L0.53,7.001C-0.121,6.35 -0.121,5.294 0.53,4.642C1.182,3.991 2.238,3.991 2.89,4.642Z" + android:fillColor="#FBFBFE"/> + </group> +</vector> diff --git a/mobile/android/fenix/app/src/main/res/drawable/globe_broken.xml b/mobile/android/fenix/app/src/main/res/drawable/globe_broken.xml @@ -0,0 +1,18 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="40dp" + android:height="40dp" + android:viewportWidth="40" + android:viewportHeight="40"> + <path + android:pathData="M4.209,1.999L37.355,35.145L35.145,37.355L1.999,4.209L4.209,1.999Z" + android:fillColor="#FBFBFE" + android:fillType="evenOdd"/> + <path + android:pathData="M7.869,5.703C3.82,9.142 1.25,14.271 1.25,20C1.25,30.03 9.126,38.221 19.031,38.725L19.041,38.733L19.047,38.726C19.363,38.742 19.681,38.75 20,38.75C20.32,38.75 20.638,38.742 20.954,38.726L20.96,38.733L20.97,38.725C26.306,38.453 31.053,35.951 34.297,32.132L32.079,29.913C30.228,32.166 27.759,33.891 24.931,34.831C26.854,32.438 28.243,29.75 29.097,26.931L26.534,24.368C25.642,28.517 23.465,32.438 20,35.474C15.763,31.76 13.451,26.722 13.063,21.563H23.728L20.603,18.438H13.063C13.22,16.35 13.692,14.282 14.479,12.313L12.102,9.936C10.844,12.632 10.12,15.52 9.93,18.438H4.453C4.872,14.209 6.978,10.477 10.087,7.922L7.869,5.703ZM15.069,34.831C11.952,30.951 10.239,26.295 9.93,21.563H4.453C5.07,27.779 9.331,32.924 15.069,34.831Z" + android:fillColor="#FBFBFE" + android:fillType="evenOdd"/> + <path + android:pathData="M13.678,7.093C14.106,6.433 14.569,5.791 15.069,5.169C14.263,5.437 13.486,5.769 12.744,6.159L10.448,3.863C12.985,2.358 15.907,1.434 19.031,1.275L19.041,1.267L19.047,1.274C19.363,1.258 19.681,1.25 20,1.25C20.32,1.25 20.638,1.258 20.954,1.274L20.96,1.267L20.97,1.275C30.875,1.779 38.75,9.97 38.75,20C38.75,23.489 37.798,26.755 36.138,29.553L33.842,27.257C34.752,25.525 35.346,23.601 35.548,21.563H30.071C30.033,22.146 29.974,22.728 29.893,23.308L25.023,18.438H26.938C26.55,13.278 24.238,8.24 20,4.526C18.361,5.963 17.01,7.598 15.947,9.361L13.678,7.093ZM30.071,18.438H35.548C34.931,12.221 30.67,7.076 24.931,5.169C28.049,9.049 29.762,13.705 30.071,18.438Z" + android:fillColor="#FBFBFE" + android:fillType="evenOdd"/> +</vector> diff --git a/mobile/android/fenix/app/src/main/res/drawable/progress_gradient.xml b/mobile/android/fenix/app/src/main/res/drawable/progress_gradient.xml @@ -5,23 +5,20 @@ <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@android:id/background"> <shape> - <solid android:color="?attr/layer2" /> + <solid android:color="@color/progress_background_tint" /> </shape> </item> <item android:id="@android:id/progress"> <scale android:scaleWidth="100%"> <shape> - <corners - android:bottomLeftRadius="0dp" - android:bottomRightRadius="8dp" - android:topLeftRadius="0dp" - android:topRightRadius="8dp"/> <gradient - android:angle="45" - android:centerColor="#F10366" - android:endColor="#FF9100" - android:startColor="#6173FF" /> + android:angle="0" + android:endColor="#00DBDE" + android:startColor="#FC00FF" /> + <corners + android:bottomRightRadius="3dp" + android:topRightRadius="3dp" /> </shape> </scale> </item> diff --git a/mobile/android/fenix/app/src/main/res/layout/fragment_tor_connection_assist.xml b/mobile/android/fenix/app/src/main/res/layout/fragment_tor_connection_assist.xml @@ -0,0 +1,189 @@ +<?xml version="1.0" encoding="utf-8"?><!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@drawable/tor_bootstrap_background_gradient" + android:paddingBottom="16dp"> + + <ProgressBar + android:id="@+id/tor_bootstrap_progress_bar" + style="?android:attr/progressBarStyleHorizontal" + android:layout_width="match_parent" + android:layout_height="6dp" + android:visibility="invisible" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/settings_button" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="8dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <ImageView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:contentDescription="@string/settings" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/mozac_ic_settings_24" /> + </androidx.constraintlayout.widget.ConstraintLayout> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/back_button" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:visibility="invisible" + android:contentDescription="@string/connection_assist_back_button_content_description_start_again" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <ImageView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:contentDescription="@string/settings" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/mozac_ic_back_24" /> + </androidx.constraintlayout.widget.ConstraintLayout> + + + <ImageView + android:id="@+id/tor_connect_image" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_marginStart="24dp" + android:contentDescription="@string/tor_bootstrap_connect" + android:visibility="visible" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/back_button" + app:layout_constraintVertical_bias="0.05" + app:srcCompat="@drawable/connect" /> + + <TextView + android:id="@+id/title_large_text_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:paddingHorizontal="24dp" + android:text="@string/connection_assist_tor_connect_title" + android:textColor="@color/photonLightGrey05" + android:textSize="22sp" + app:layout_constraintTop_toBottomOf="@id/tor_connect_image" /> + + <TextView + android:id="@+id/title_description" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:lineSpacingExtra="6dp" + android:paddingHorizontal="24dp" + android:paddingVertical="16dp" + android:text="@string/preferences_tor_network_settings_explanation" + android:textColor="@color/photonLightGrey05" + android:textSize="14sp" + app:layout_constraintTop_toBottomOf="@id/title_large_text_view" /> + + <androidx.appcompat.widget.SwitchCompat + android:id="@+id/quickstart_switch" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingHorizontal="24dp" + android:paddingVertical="8dp" + android:text="@string/connection_assist_always_connect_automatically_toggle_description" + android:textColor="@color/photonLightGrey05" + app:layout_constraintTop_toBottomOf="@id/title_description" /> + + <TextView + android:id="@+id/unblock_the_internet_in_country_description" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingHorizontal="24dp" + android:layout_marginTop="24dp" + android:text="@string/connection_assist_unblock_the_internet_in_country_or_region" + android:textColor="@color/photonLightGrey05" + android:visibility="gone" + app:layout_constraintTop_toBottomOf="@id/title_description" /> + + <androidx.appcompat.widget.AppCompatSpinner + android:id="@+id/country_drop_down" + style="@style/Widget.AppCompat.Spinner.Underlined" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="24dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="24dp" + android:textColor="@color/photonLightGrey05" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/unblock_the_internet_in_country_description" /> + + <ImageView + android:id="@+id/wordmarkLogo" + android:layout_width="192dp" + android:layout_height="192dp" + android:contentDescription="@string/app_name" + android:scaleX="1.8" + android:scaleY="1.8" + app:srcCompat="@drawable/ic_launcher_foreground" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <Button + android:id="@+id/tor_bootstrap_button_1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="24dp" + android:layout_marginEnd="24dp" + android:layout_marginBottom="8dp" + android:background="@drawable/rounded_corners" + android:backgroundTint="@color/connect_button_purple" + android:minWidth="360dp" + android:text="@string/tor_bootstrap_connect" + android:textAlignment="center" + android:textAllCaps="false" + android:textColor="@color/photonLightGrey05" + android:textSize="14sp" + android:textStyle="bold" + app:layout_constraintBottom_toTopOf="@id/tor_bootstrap_button_2" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + + <Button + android:id="@+id/tor_bootstrap_button_2" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="24dp" + android:layout_marginEnd="24dp" + android:layout_marginBottom="8dp" + android:background="@drawable/rounded_corners" + android:backgroundTint="@color/configure_connection_button_white" + android:minWidth="360dp" + android:text="@string/connection_assist_configure_connection_button" + android:textAlignment="center" + android:textAllCaps="false" + android:textColor="@color/photonDarkGrey90" + android:textSize="14sp" + android:textStyle="bold" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/mobile/android/fenix/app/src/main/res/navigation/nav_graph.xml b/mobile/android/fenix/app/src/main/res/navigation/nav_graph.xml @@ -16,6 +16,18 @@ app:popUpToInclusive="true" /> <action + android:id="@+id/action_startup_tor_connection_assist" + app:destination="@+id/torConnectionAssistFragment" + app:popUpTo="@id/startupFragment" + app:popUpToInclusive="true" /> + + <action + android:id="@+id/action_connect_to_tor_before_opening_links" + app:destination="@+id/torConnectionAssistFragment" + app:popUpTo="@id/torConnectionAssistFragment" + app:popUpToInclusive="true"/> + + <action android:id="@+id/action_global_home" app:destination="@id/homeFragment" app:popUpTo="@id/homeFragment" @@ -318,6 +330,31 @@ </fragment> <fragment + android:id="@+id/torConnectionAssistFragment" + android:name="org.mozilla.fenix.tor.TorConnectionAssistFragment" + tools:layout="@layout/fragment_tor_connection_assist"> + <action + android:id="@+id/action_home" + app:destination="@id/homeFragment" + app:popUpTo="@id/torConnectionAssistFragment" + app:popUpToInclusive="true" /> + <action + android:id="@+id/action_torConnectionAssistFragment_to_SettingsFragment" + app:destination="@id/settingsFragment" + app:enterAnim="@anim/slide_in_right" + app:exitAnim="@anim/slide_out_left" + app:popEnterAnim="@anim/slide_in_left" + app:popExitAnim="@anim/slide_out_right" /> + <action + android:id="@+id/action_torConnectionAssistFragment_to_TorConnectionSettings" + app:destination="@id/settingsFragment" + app:enterAnim="@anim/slide_in_right" + app:exitAnim="@anim/slide_out_left" + app:popEnterAnim="@anim/slide_in_left" + app:popExitAnim="@anim/slide_out_right" /> + </fragment> + + <fragment android:id="@+id/storiesFragment" android:name="org.mozilla.fenix.home.pocket.StoriesFragment" /> @@ -840,6 +877,13 @@ app:exitAnim="@anim/slide_out_left" app:popEnterAnim="@anim/slide_in_left" app:popExitAnim="@anim/slide_out_right" /> + <action + android:id="@+id/action_settingsFragment_to_TorConnectionAssistFragment" + app:destination="@id/torConnectionAssistFragment" + app:enterAnim="@anim/slide_in_right" + app:exitAnim="@anim/slide_out_left" + app:popEnterAnim="@anim/slide_in_left" + app:popExitAnim="@anim/slide_out_right" /> </fragment> <dialog android:id="@+id/profilerStartDialogFragment" diff --git a/mobile/android/fenix/app/src/main/res/values/colors.xml b/mobile/android/fenix/app/src/main/res/values/colors.xml @@ -316,4 +316,12 @@ <!-- Lock slash critical fill colors for mozac_ic_lock_slash_critical_24 --> <color name="mozac_ui_lock_slash_bg_fill_critical" tools:ignore="UnusedResources">@color/fx_mobile_icon_color_secondary</color> <color name="mozac_ui_lock_slash_fill_critical" tools:ignore="UnusedResources">@color/fx_mobile_icon_color_critical</color> + + <!-- Connection Assist --> + <color name="connect_button_purple">#9059FF</color> + <color name="disabled_connect_button_purple">#5C42A9</color> + <color name="disabled_text_gray_purple">#8782A9</color> + <color name="configure_connection_button_white">#E1E0E7</color> + <color name="warning_yellow">#FFA436</color> + <color name="progress_background_tint">#55148C</color> </resources> diff --git a/mobile/android/locales/filter.py b/mobile/android/locales/filter.py @@ -23,6 +23,7 @@ def test(mod, path, entity=None): "toolkit/branding/brandings.ftl", "toolkit/global/processTypes.ftl", "toolkit/global/resetProfile.ftl", + "toolkit/intl/regionNames.ftl", ): return "error" if re.match(r"toolkit/about/[^/]*About.ftl", path): diff --git a/mobile/android/locales/jar.mn b/mobile/android/locales/jar.mn @@ -53,6 +53,8 @@ relativesrcdir toolkit/locales: toolkit/branding (%toolkit/branding/*brandings.ftl) toolkit/global (%toolkit/global/*processTypes.ftl) toolkit/global (%toolkit/global/*resetProfile.ftl) +#country/region names (used by TorConnect; see tor-browser#43633) + toolkit/intl/regionNames.ftl (%toolkit/intl/regionNames.ftl) #endif # Do not add files below the endif. Reviewers, expand more context above # for comments. diff --git a/mobile/android/locales/l10n.toml b/mobile/android/locales/l10n.toml @@ -197,6 +197,10 @@ exclude-multi-locale = [ reference = "toolkit/locales/en-US/toolkit/global/resetProfile.ftl" l10n = "{l}toolkit/toolkit/global/resetProfile.ftl" +[[paths]] + reference = "toolkit/locales/en-US/toolkit/intl/regionNames.ftl" + l10n = "{l}toolkit/toolkit/intl/regionNames.ftl" + [[filters]] path = [ "{l}mobile/android/mobile-l10n.js", diff --git a/mobile/locales/filter.py b/mobile/locales/filter.py @@ -23,6 +23,7 @@ def test(mod, path, entity=None): "toolkit/branding/brandings.ftl", "toolkit/global/processTypes.ftl", "toolkit/global/resetProfile.ftl", + "toolkit/intl/regionNames.ftl", ): return "error" if re.match(r"toolkit/about/[^/]*About.ftl", path):