Verified Commit b462d44f authored by Matthew Finkel's avatar Matthew Finkel Committed by aguestuser
Browse files

Add Tor integration and UI

Bug 40001: Start Tor as part of the Fenix initialization

Bug 40028: Implement Tor Service controller

Bug 40028: Integrate Tor Controller into HomeFragment

Bug 40028: Implement Tor connect and logger screens

Bug 40028: Implement Tor Onboarding

Bug 40028: Implement new home screen

Bug 40028: Define bootstrapping events and Quick Start

Bug 40041: Implement Tor Network Settings

Bug 40041: Integrate Tor Network Settings

Bug 40179: Show Snowflake bridge option on Release

Bug 40176: Re-render Home fragment on resume
parent f9aa56be
......@@ -19,6 +19,8 @@ default:
- 32GB
before_script:
- set -e
# Prepare our Debian environment.
- apt-get update -qq
- apt-get upgrade -qy
......@@ -47,6 +49,33 @@ default:
# Create local.properties file.
- echo "sdk.dir=$PWD" > local.properties
# Fetch tor library build dependencies
- wget --quiet https://people.torproject.org/~sysrqb/mirrors/tor-onion-proxy-library/0.0.3/android-release.aar
- wget --quiet https://people.torproject.org/~sysrqb/mirrors/tor-onion-proxy-library/0.0.3/universal-0.0.3.jar
- wget --quiet https://people.torproject.org/~sysrqb/mirrors/tor-android-service/1.0/jsocksAndroid-release.aar
- wget --quiet https://people.torproject.org/~sysrqb/mirrors/tor-android-service/1.0/service-release.aar
- wget --quiet https://people.torproject.org/~sysrqb/mirrors/tor/0.4.4.4-rc/tor_x86.tar.gz
- wget --quiet https://people.torproject.org/~sysrqb/mirrors/tor/0.4.4.4-rc/tor_x86_64.tar.gz
- wget --quiet https://people.torproject.org/~sysrqb/mirrors/tor/0.4.4.4-rc/tor_armv7.tar.gz
- wget --quiet https://people.torproject.org/~sysrqb/mirrors/tor/0.4.4.4-rc/tor_aarch64.tar.gz
- echo "4e2bd087df6128a31c598d297367c106762f539c25d71cc343aae25386e4bee9 android-release.aar" | sha256sum -c
- echo "0082d1d035a0808d03d7e1009398ed2c3e3ea4c51811198f6ff83ccda456036f universal-0.0.3.jar" | sha256sum -c
- echo "3dca44a48fdbd3f6c44f7ea335ae85fe542676d8e48c6438f84a2c852daf4f54 jsocksAndroid-release.aar" | sha256sum -c
- echo "d38967569af56d809f09f08b888962971149411832c3bc2b7b0b64a43ceb0dcd service-release.aar" | sha256sum -c
- echo "0faa344f658d2e38c7cc881f51c1d8658aac56801f7def1b75be72a8d3f90c54 tor_x86.tar.gz" | sha256sum -c
- echo "acf1d403ca12e3302d1150d2b5fba6585aa83d46db16f15ad33fae39645c5b5d tor_x86_64.tar.gz" | sha256sum -c
- echo "132cb40bbc15bd8f72abd15324705ab720f8b316a2fca84baaeb5db37f602c32 tor_armv7.tar.gz" | sha256sum -c
- echo "6c453c5f7566c87d422ef60750cba794d9616e9197114be5dc1194f75b55b04e tor_aarch64.tar.gz" | sha256sum -c
- mv android-release.aar universal-0.0.3.jar jsocksAndroid-release.aar service-release.aar app/
- tar -C app/src/main -xf tor_x86.tar.gz
- tar -C app/src/main -xf tor_x86_64.tar.gz
- tar -C app/src/main -xf tor_armv7.tar.gz
- tar -C app/src/main -xf tor_aarch64.tar.gz
- rm tor_x86.tar.gz tor_x86_64.tar.gz tor_armv7.tar.gz tor_aarch64.tar.gz
stages:
- build
- buildFenixProduction
......@@ -57,7 +86,7 @@ stages:
buildDebug:
stage: build
script:
- ./gradlew clean app:assembleDebug --stacktrace
- ./gradlew -PdisableTor=true clean app:assembleDebug --stacktrace
# Disable for now.
#artifacts:
......@@ -71,7 +100,7 @@ testLight:
# from config/pre-push-recommended.sh
# `tee` into a log file because resulting output is larger than 4 MB
# (4 MB is max log size)
- ./gradlew ktlint detekt assembleDebug assembleDebugAndroidTest testDebug | tee testLight_gradle.log
- ./gradlew -PdisableTor=true ktlint detekt assembleDebug assembleDebugAndroidTest testDebug | tee testLight_gradle.log
artifacts:
paths:
- testLight_gradle.log
......@@ -85,25 +114,25 @@ buildFenixProduction:
only:
- schedules
script:
- ./gradlew clean app:assembleNightly --stacktrace
- ./gradlew -PdisableTor=true clean app:assembleNightly --stacktrace
buildFennecBeta:
stage: buildFennecBeta
only:
- schedules
script:
- ./gradlew clean app:assembleBeta --stacktrace
- ./gradlew -PdisableTor=true clean app:assembleBeta --stacktrace
buildFennecProduction:
stage: buildFennecProduction
only:
- schedules
script:
- ./gradlew clean app:assembleRelease --stacktrace
- ./gradlew -PdisableTor=true clean app:assembleRelease --stacktrace
testAll:
stage: test
only:
- schedules
script:
- ./gradlew clean test
- ./gradlew -PdisableTor=true clean test
......@@ -260,12 +260,20 @@ android.applicationVariants.all { variant ->
def useReleaseVersioning = variant.buildType.buildConfigFields['USE_RELEASE_VERSIONING']?.value ?: false
def versionName = variant.buildType.name == 'nightly' ? Config.nightlyVersionName() : Config.releaseVersionName(project)
def disableTor = false
if (project.hasProperty("disableTor")) {
disableTor = project.getProperty("disableTor")
}
println("----------------------------------------------")
println("Variant name: " + variant.name)
println("Application ID: " + [variant.mergedFlavor.applicationId, variant.buildType.applicationIdSuffix].findAll().join())
println("Build type: " + variant.buildType.name)
println("Flavor: " + variant.flavorName)
println("Telemetry enabled: " + !isDebugOrDCD)
println("Tor is disabled: " + disableTor)
buildConfigField "boolean", "DISABLE_TOR", "$disableTor"
if (useReleaseVersioning) {
// The Google Play Store does not allow multiple APKs for the same app that all have the
......@@ -640,6 +648,18 @@ dependencies {
testImplementation "org.mozilla.telemetry:glean-native-forUnitTests:${project.ext.glean_version}"
lintChecks project(":mozilla-lint-rules")
// Tor Android Services Dependencies
implementation 'net.freehaven.tor.control:jtorctl:0.2'
implementation 'org.slf4j:slf4j-api:1.7.25'
implementation 'org.slf4j:slf4j-android:1.7.25'
// Tor Android Services.
implementation files('service-release.aar')
// Tor Onion Proxy Library.
implementation files('universal-0.0.3.jar')
implementation files('android-release.aar')
}
protobuf {
......
......@@ -335,6 +335,13 @@
android:value="androidx.startup"
tools:node="remove" />
</provider>
<!-- Define Orbotservice's TorService -->
<service
android:name="org.torproject.android.service.TorService"
android:enabled="true"
android:exported="false"
android:stopWithTask="true">
</service>
</application>
</manifest>
......@@ -87,6 +87,7 @@ import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.utils.Settings.Companion.TOP_SITES_PROVIDER_MAX_THRESHOLD
import java.util.UUID
import java.util.concurrent.TimeUnit
import org.torproject.android.service.util.Prefs
/**
*The main application class for Fenix. Records data to measure initialization performance.
......@@ -100,6 +101,8 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
private val logger = Logger("FenixApplication")
var terminating = false
open val components by lazy { Components(this) }
var visibilityLifecycleCallback: VisibilityLifecycleCallback? = null
......@@ -136,6 +139,21 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
PerfStartup.applicationOnCreate.stopAndAccumulate(completeMethodDurationTimerId)
}
fun isTerminating() = terminating
fun terminate() {
onTerminate()
System.exit(0)
}
override fun onTerminate() {
terminating = true
super.onTerminate()
components.torController.stop()
components.torController.stopTor()
}
@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
protected open fun initializeGlean() {
val telemetryEnabled = settings().isTelemetryEnabled
......@@ -170,6 +188,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
}
@CallSuper
@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
open fun setupInMainProcessOnly() {
ProfilerMarkerFactProcessor.create { components.core.engine.profiler }.register()
......@@ -195,6 +214,11 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
if (!megazordSetup.isCompleted) {
runBlockingIncrement { megazordSetup.await() }
}
GlobalScope.launch(Dispatchers.IO) {
// Give TAS the base Context
Prefs.setContext(applicationContext)
}
}
setupLeakCanary()
......@@ -216,6 +240,8 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
initVisualCompletenessQueueAndQueueTasks()
ProcessLifecycleOwner.get().lifecycle.addObserver(TelemetryLifecycleObserver(components.core.store))
components.torController.start()
}
@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
......
......@@ -153,6 +153,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
private var isToolbarInflated = false
private var isBeingRecreated = false
private val webExtensionPopupFeature by lazy {
WebExtensionPopupFeature(components.core.store, ::openPopup)
}
......@@ -456,6 +458,14 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.core.contileTopSitesUpdater.stopPeriodicWork()
components.core.pocketStoriesService.stopPeriodicStoriesRefresh()
privateNotificationObserver?.stop()
if (!isBeingRecreated && !(application as FenixApplication).isTerminating()) {
// We assume the Activity is being destroyed because the user
// swiped away the app on the Recent screen. When this happens,
// we assume the user expects the entire Application is destroyed
// and not only the top Activity/Task. Therefore we kill the
// underlying Application, as well.
(application as FenixApplication).terminate()
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
......@@ -475,6 +485,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
message = "recreate()"
)
isBeingRecreated = true
super.recreate()
}
......
......@@ -33,6 +33,7 @@ import org.mozilla.fenix.perf.StartupActivityLog
import org.mozilla.fenix.perf.StartupStateProvider
import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.tor.TorController
import org.mozilla.fenix.utils.ClipboardHandler
import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.wallpapers.WallpaperFileManager
......@@ -187,6 +188,7 @@ class Components(private val context: Context) {
val startupActivityLog by lazyMonitored { StartupActivityLog() }
val startupStateProvider by lazyMonitored { StartupStateProvider(startupActivityLog, appStartReasonProvider) }
val appStore by lazyMonitored { AppStore() }
val torController by lazyMonitored { TorController(context) }
}
/**
......
......@@ -31,6 +31,8 @@ import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.TOP
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.view.children
import androidx.core.view.doOnLayout
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
......@@ -74,6 +76,7 @@ import mozilla.components.support.ktx.android.content.res.resolveAttribute
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import mozilla.components.ui.tabcounter.TabCounterMenu
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.HomeActivity
......@@ -128,6 +131,7 @@ import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.SupportUtils.SumoTopic.HELP
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
import org.mozilla.fenix.tor.bootstrap.TorQuickStart
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.Settings.Companion.TOP_SITES_PROVIDER_MAX_THRESHOLD
import org.mozilla.fenix.utils.ToolbarPopupWindow
......@@ -187,6 +191,7 @@ class HomeFragment : Fragment() {
private val recentTabsListFeature = ViewBoundFeatureWrapper<RecentTabsListFeature>()
private val recentBookmarksFeature = ViewBoundFeatureWrapper<RecentBookmarksFeature>()
private val historyMetadataFeature = ViewBoundFeatureWrapper<RecentVisitsFeature>()
private val torQuickStart by lazy { TorQuickStart(requireContext()) }
@VisibleForTesting
internal var getMenuButton: () -> MenuButton? = { binding.menuButton }
......@@ -230,9 +235,22 @@ class HomeFragment : Fragment() {
val activity = activity as HomeActivity
val components = requireComponents
// Splits by full stops or commas and puts the parts in different lines.
// Ignoring separators at the end of the string, it is expected
// that there are at most two parts (e.g. "Explore. Privately.").
val localBinding = binding;
binding.exploreprivately.text = localBinding
.exploreprivately
.text
?.replace(" *([.,。।]) *".toRegex(), "$1\n")
?.trim()
currentMode = CurrentMode(
requireContext(),
onboarding,
torQuickStart,
!BuildConfig.DISABLE_TOR,
components.torController,
browsingModeManager,
::dispatchModeChanges
)
......@@ -357,7 +375,11 @@ class HomeFragment : Fragment() {
hideOnboarding = ::hideOnboardingAndOpenSearch,
registerCollectionStorageObserver = ::registerCollectionStorageObserver,
removeCollectionWithUndo = ::removeCollectionWithUndo,
showTabTray = ::openTabsTray
showTabTray = ::openTabsTray,
handleTorBootstrapConnect = ::handleTorBootstrapConnect,
cancelTorBootstrap = ::cancelTorBootstrap,
initiateTorBootstrap = ::initiateTorBootstrap,
openTorNetworkSettings = ::openTorNetworkSettings
),
recentTabController = DefaultRecentTabsController(
selectTabUseCase = components.useCases.tabsUseCases.selectTab,
......@@ -406,6 +428,9 @@ class HomeFragment : Fragment() {
displayWallpaperIfEnabled()
adjustHomeFragmentView(currentMode.getCurrentMode())
showSessionControlView()
// DO NOT MOVE ANYTHING BELOW THIS addMarker CALL!
requireComponents.core.engine.profiler?.addMarker(
MarkersFragmentLifecycleCallbacks.MARKER_NAME, profilerStartTime, "HomeFragment.onCreateView",
......@@ -497,6 +522,108 @@ class HomeFragment : Fragment() {
}
}
// This function should be paired with showSessionControlView()
@SuppressWarnings("ComplexMethod", "NestedBlockDepth", "LongMethod")
private fun adjustHomeFragmentView(mode: Mode) {
binding.sessionControlRecyclerView.apply {
visibility = View.INVISIBLE
}
if (mode == Mode.Bootstrap) {
binding.sessionControlRecyclerView.apply {
setPadding(0, 0, 0, 0)
(layoutParams as ViewGroup.MarginLayoutParams).setMargins(0, 0, 0, 0)
}
binding.homeAppBar.apply {
visibility = View.GONE
// Reset this as SCROLL in case it was previously set as NO_SCROLL after bootstrap
children.forEach {
(it.layoutParams as AppBarLayout.LayoutParams).scrollFlags =
AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL
}
}
binding.onionPatternImage.apply {
visibility = View.GONE
}
binding.toolbarLayout.apply {
visibility = View.GONE
}
} else {
// Keep synchronized with xml layout (somehow).
binding.sessionControlRecyclerView.apply {
setPadding(
SESSION_CONTROL_VIEW_PADDING,
SESSION_CONTROL_VIEW_PADDING,
SESSION_CONTROL_VIEW_PADDING,
SESSION_CONTROL_VIEW_PADDING
)
// Default margin until it is re-set below (either set immediately or after Layout)
(layoutParams as ViewGroup.MarginLayoutParams).setMargins(
0,
0,
0,
DEFAULT_ONBOARDING_FINISH_MARGIN
)
}
binding.toolbarLayout.apply {
visibility = View.VISIBLE
// If the Layout rendering pass was completed, then we have a |height| value,
// if it wasn't completed then we have 0.
if (height == 0) {
// Set the bottom margin after the toolbar height is defined during Layout
doOnLayout {
val toolbarLayoutHeight = binding.toolbarLayout.height
binding.sessionControlRecyclerView.apply {
(layoutParams as ViewGroup.MarginLayoutParams).setMargins(
0,
0,
0,
toolbarLayoutHeight - SESSION_CONTROL_VIEW_PADDING
)
}
}
} else {
binding.sessionControlRecyclerView.apply {
(layoutParams as ViewGroup.MarginLayoutParams).setMargins(
0,
0,
0,
height - SESSION_CONTROL_VIEW_PADDING
)
}
}
}
// Hide the onion pattern during Onboarding, too.
binding.onionPatternImage.apply {
visibility = if (onboarding.userHasBeenOnboarded()) {
View.VISIBLE
} else {
View.GONE
}
}
binding.homeAppBar.apply {
visibility = View.VISIBLE
children.forEach {
(it.layoutParams as AppBarLayout.LayoutParams).scrollFlags =
if (onboarding.userHasBeenOnboarded()) {
AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL
} else {
AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL
}
}
}
}
}
// This function should be paired with adjustHomeFragmentView()
private fun showSessionControlView() {
binding.sessionControlRecyclerView.apply {
visibility = View.VISIBLE
}
}
@Suppress("LongMethod", "ComplexMethod")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// DO NOT ADD ANYTHING ABOVE THIS getProfilerTime CALL!
......@@ -794,9 +921,11 @@ class HomeFragment : Fragment() {
}
private fun dispatchModeChanges(mode: Mode) {
if (mode != Mode.fromBrowsingMode(browsingModeManager.mode)) {
homeFragmentStore.dispatch(HomeFragmentAction.ModeChange(mode))
}
homeFragmentStore.dispatch(HomeFragmentAction.ModeChange(mode))
adjustHomeFragmentView(mode)
updateSessionControlView()
showSessionControlView()
}
@VisibleForTesting
......@@ -820,12 +949,23 @@ class HomeFragment : Fragment() {
}
}
override fun onStop() {
super.onStop()
currentMode.unregisterTorListener()
}
override fun onResume() {
super.onResume()
if (browsingModeManager.mode == BrowsingMode.Private) {
activity?.window?.setBackgroundDrawableResource(R.drawable.private_home_background_gradient)
}
// fenix#40176: Ensure the Home fragment is rendered correctly when we resume.
val mode = currentMode.getCurrentMode()
adjustHomeFragmentView(mode)
updateSessionControlView()
showSessionControlView()
hideToolbar()
// Whenever a tab is selected its last access timestamp is automatically updated by A-C.
......@@ -921,6 +1061,10 @@ class HomeFragment : Fragment() {
mode = currentMode.getCurrentMode()
)
)
adjustHomeFragmentView(
currentMode.getCurrentMode()
)
showSessionControlView()
}
}
......@@ -1265,6 +1409,25 @@ class HomeFragment : Fragment() {
FeatureFlags.showWallpapers &&
(activity as? HomeActivity)?.themeManager?.currentTheme?.isPrivate?.not() ?: false
private fun handleTorBootstrapConnect() {
requireComponents.torController.onTorConnecting()
}
private fun cancelTorBootstrap() {
requireComponents.torController.stopTor()
}
private fun initiateTorBootstrap(withDebugLogging: Boolean = false) {
requireComponents.torController.initiateTorBootstrap(lifecycleScope, withDebugLogging)
}
private fun openTorNetworkSettings() {
val directions =
HomeFragmentDirections
.actionHomeFragmentToTorNetworkSettingsFragment()
findNavController().navigate(directions)
}
companion object {
const val ALL_NORMAL_TABS = "all_normal"
const val ALL_PRIVATE_TABS = "all_private"
......@@ -1286,5 +1449,9 @@ class HomeFragment : Fragment() {
// Elevation for undo toasts
internal const val TOAST_ELEVATION = 80f
// Layout
private const val DEFAULT_ONBOARDING_FINISH_MARGIN = 60
private const val SESSION_CONTROL_VIEW_PADDING = 16
}
}
......@@ -9,6 +9,9 @@ import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.Profile
import org.mozilla.fenix.tor.TorController
import org.mozilla.fenix.tor.TorEvents
import org.mozilla.fenix.tor.bootstrap.TorQuickStart
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.ext.components
......@@ -21,6 +24,7 @@ sealed class Mode {
object Normal : Mode()
object Private : Mode()
data class Onboarding(val state: OnboardingState) : Mode()
object Bootstrap : Mode()
companion object {
fun fromBrowsingMode(browsingMode: BrowsingMode) = when (browsingMode) {
......@@ -40,16 +44,26 @@ sealed class OnboardingState {
object SignedIn : OnboardingState()
}
@SuppressWarnings("LongParameterList", "TooManyFunctions")
class CurrentMode(
private val context: Context,
private val onboarding: FenixOnboarding,
private val torQuickStart: TorQuickStart,
private val shouldStartTor: Boolean,
private val torController: TorController,
private val browsingModeManager: BrowsingModeManager,
private val dispatchModeChanges: (mode: Mode) -> Unit
) : AccountObserver {
) : AccountObserver, TorEvents {
private val accountManager by lazy { context.components.backgroundServices.accountManager }
fun getCurrentMode() = if (onboarding.userHasBeenOnboarded()) {
init {
torController.registerTorListener(this)
}
fun getCurrentMode() = if (shouldStartTor && (!torQuickStart.quickStartTor() && !torController.isBootstrapped)) {
Mode.Bootstrap
} else if (onboarding.userHasBeenOnboarded()) {
Mode.fromBrowsingMode(browsingModeManager.mode)
} else {
val account = accountManager.authenticatedAccount()
......@@ -64,6 +78,26 @@ class CurrentMode(
dispatchModeChanges(getCurrentMode())
}
@SuppressWarnings("EmptyFunctionBlock")
override fun onTorConnecting() {
}
override fun onTorConnected() {
dispatchModeChanges(getCurrentMode())
}
override fun onTorStopped() {
dispatchModeChanges(getCurrentMode())
}
@SuppressWarnings("EmptyFunctionBlock")
override fun onTorStatusUpdate(entry: String?, status: String?) {
}
fun unregisterTorListener() {
torController.unregisterTorListener(this)
}
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) = emitModeChanges()
override fun onAuthenticationProblems() = emitModeChanges()
override fun onLoggedOut() = emitModeChanges()
......
......@@ -35,6 +35,7 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.CustomizeHomeButtonViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.NoCollectionsMessageViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.PrivateBrowsingDescriptionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TorBootstrapPagerViewHolder