Commit 232247fd authored by Matthew Finkel's avatar Matthew Finkel
Browse files

Bug 40028: Implement Tor connect and logger screens

parent f39de0b6
......@@ -253,7 +253,8 @@ class HomeFragment : Fragment() {
handleSwipedItemDeletionCancel = ::handleSwipedItemDeletionCancel,
handleTorBootstrapConnect = ::handleTorBootstrapConnect,
cancelTorBootstrap = ::cancelTorBootstrap,
initiateTorBootstrap = ::initiateTorBootstrap
initiateTorBootstrap = ::initiateTorBootstrap,
openTorNetworkSettings = ::openTorNetworkSettings
)
)
......@@ -1062,6 +1063,9 @@ class HomeFragment : Fragment() {
requireComponents.torController.initiateTorBootstrap(lifecycleScope, withDebugLogging)
}
private fun openTorNetworkSettings() {
}
companion object {
const val ALL_NORMAL_TABS = "all_normal"
const val ALL_PRIVATE_TABS = "all_private"
......
......@@ -21,6 +21,7 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHol
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.NoCollectionsMessageViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.PrivateBrowsingDescriptionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TorBootstrapPagerViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TopSitePagerViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingAutomaticSignInViewHolder
......@@ -60,6 +61,8 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
object PrivateBrowsingDescription : AdapterItem(PrivateBrowsingDescriptionViewHolder.LAYOUT_ID)
object NoCollectionsMessage : AdapterItem(NoCollectionsMessageViewHolder.LAYOUT_ID)
object TorBootstrap : AdapterItem(TorBootstrapPagerViewHolder.LAYOUT_ID)
object CollectionHeader : AdapterItem(CollectionHeaderViewHolder.LAYOUT_ID)
data class CollectionItem(
val collection: TabCollection,
......@@ -152,6 +155,11 @@ class SessionControlAdapter(
view,
interactor
)
TorBootstrapPagerViewHolder.LAYOUT_ID -> TorBootstrapPagerViewHolder(
view,
components,
interactor
)
NoCollectionsMessageViewHolder.LAYOUT_ID ->
NoCollectionsMessageViewHolder(
view,
......
......@@ -156,9 +156,9 @@ interface SessionControlController {
fun handleTorBootstrapConnectClicked()
/**
* @see [TorBootstrapInteractor.onTorBootstrapConnectingClicked]
* @see [TorBootstrapInteractor.onTorStopBootstrapping]
*/
fun handleTorBootstrapConnectingClicked()
fun handleTorStopBootstrapping()
/**
* @see [TorBootstrapInteractor.onTorStartBootstrapping]
......@@ -169,6 +169,11 @@ interface SessionControlController {
* @see [TorBootstrapInteractor.onTorStartDebugBootstrapping]
*/
fun handleTorStartDebugBootstrapping()
/**
* @see [TorBootstrapInteractor.onTorBootstrapNetworkSettingsClicked]
*/
fun handleTorNetworkSettingsClicked()
}
@Suppress("TooManyFunctions", "LargeClass")
......@@ -196,7 +201,8 @@ class DefaultSessionControlController(
private val handleSwipedItemDeletionCancel: () -> Unit,
private val handleTorBootstrapConnect: () -> Unit,
private val initiateTorBootstrap: (Boolean) -> Unit,
private val cancelTorBootstrap: () -> Unit
private val cancelTorBootstrap: () -> Unit,
private val openTorNetworkSettings: () -> Unit
) : SessionControlController {
override fun handleCollectionAddTabTapped(collection: TabCollection) {
......@@ -462,7 +468,7 @@ class DefaultSessionControlController(
handleTorBootstrapConnect()
}
override fun handleTorBootstrapConnectingClicked() {
override fun handleTorStopBootstrapping() {
cancelTorBootstrap()
}
......@@ -473,4 +479,8 @@ class DefaultSessionControlController(
override fun handleTorStartDebugBootstrapping() {
initiateTorBootstrap(true)
}
override fun handleTorNetworkSettingsClicked() {
openTorNetworkSettings()
}
}
......@@ -144,6 +144,34 @@ interface TipInteractor {
fun onCloseTip(tip: Tip)
}
interface TorBootstrapInteractor {
/**
* Initiates Tor bootstrapping. Called when a user clicks on the "Connect" button.
*/
fun onTorBootstrapConnectClicked()
/**
* Initiates Tor bootstrapping. Called when a user clicks on the "Connect" button.
*/
fun onTorStartBootstrapping()
/**
* Stop Tor bootstrapping. Called when a user clicks on the "settings" cog/button.
*/
fun onTorStopBootstrapping()
/**
* Initiates Tor bootstrapping with debug logging. Called when bootstrapping fails with
* the control.txt file not existing.
*/
fun onTorStartDebugBootstrapping()
/**
* Open Tor Network Settings preference screen
*/
fun onTorBootstrapNetworkSettingsClicked()
}
/**
* Interface for top site related actions in the [SessionControlInteractor].
*/
......@@ -181,7 +209,7 @@ interface TopSiteInteractor {
class SessionControlInteractor(
private val controller: SessionControlController
) : CollectionInteractor, OnboardingInteractor, TopSiteInteractor, TipInteractor,
TabSessionInteractor, ToolbarInteractor {
TabSessionInteractor, ToolbarInteractor, TorBootstrapInteractor {
override fun onCollectionAddTabTapped(collection: TabCollection) {
controller.handleCollectionAddTabTapped(collection)
}
......@@ -265,4 +293,24 @@ class SessionControlInteractor(
override fun onRemoveCollectionsPlaceholder() {
controller.handleRemoveCollectionsPlaceholder()
}
override fun onTorBootstrapConnectClicked() {
controller.handleTorBootstrapConnectClicked()
}
override fun onTorStopBootstrapping() {
controller.handleTorStopBootstrapping()
}
override fun onTorStartBootstrapping() {
controller.handleTorStartBootstrapping()
}
override fun onTorStartDebugBootstrapping() {
controller.handleTorStartDebugBootstrapping()
}
override fun onTorBootstrapNetworkSettingsClicked() {
controller.handleTorNetworkSettingsClicked()
}
}
......@@ -68,6 +68,8 @@ private fun showCollections(
private fun privateModeAdapterItems() = listOf(AdapterItem.PrivateBrowsingDescription)
private fun bootstrapAdapterItems() = listOf(AdapterItem.TorBootstrap)
private fun onboardingAdapterItems(onboardingState: OnboardingState): List<AdapterItem> {
val items: MutableList<AdapterItem> = mutableListOf(AdapterItem.OnboardingHeader)
......@@ -117,6 +119,7 @@ private fun HomeFragmentState.toAdapterList(): List<AdapterItem> = when (mode) {
)
is Mode.Private -> privateModeAdapterItems()
is Mode.Onboarding -> onboardingAdapterItems(mode.state)
is Mode.Bootstrap -> bootstrapAdapterItems()
}
private fun collectionTabItems(collection: TabCollection) =
......
/* 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.home.sessioncontrol.viewholders
import android.view.View
import androidx.appcompat.widget.SwitchCompat
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.tor_bootstrap_connect.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.tor.TorEvents
import org.mozilla.fenix.tor.bootstrap.TorQuickStart
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.home.sessioncontrol.TorBootstrapInteractor
class TorBootstrapConnectViewHolder(
private val view: View,
private val components: Components,
private val interactor: TorBootstrapInteractor
) : RecyclerView.ViewHolder(view), TorEvents {
init {
val torQuickStart = TorQuickStart(view.context)
setQuickStartDescription(view, torQuickStart)
with(view.quick_start_toggle as SwitchCompat) {
setOnCheckedChangeListener { _, isChecked ->
torQuickStart.setQuickStartTor(isChecked)
setQuickStartDescription(view, torQuickStart)
}
isChecked = torQuickStart.quickStartTor()
}
with(view.tor_bootstrap_network_settings_button) {
setOnClickListener {
interactor.onTorStopBootstrapping()
interactor.onTorBootstrapNetworkSettingsClicked()
with(view.tor_bootstrap_progress) {
visibility = View.INVISIBLE
}
with(view.tor_bootstrap_connect_button) {
visibility = View.VISIBLE
}
}
}
with(view.tor_bootstrap_connect_button) {
setOnClickListener {
interactor.onTorBootstrapConnectClicked()
interactor.onTorStartBootstrapping()
visibility = View.INVISIBLE
with(view.tor_bootstrap_progress) {
visibility = View.VISIBLE
}
}
}
components.torController.registerTorListener(this)
}
private fun setQuickStartDescription(view: View, torQuickStart: TorQuickStart) {
val resources = view.context.resources
val appName = resources.getString(R.string.app_name)
if (torQuickStart.quickStartTor()) {
view.tor_bootstrap_quick_start_description.text = resources.getString(
R.string.tor_bootstrap_quick_start_enabled, appName
)
} else {
view.tor_bootstrap_quick_start_description.text = resources.getString(
R.string.tor_bootstrap_quick_start_disabled
)
}
}
@SuppressWarnings("EmptyFunctionBlock")
override fun onTorConnecting() {
}
override fun onTorConnected() {
components.torController.unregisterTorListener(this)
}
@SuppressWarnings("EmptyFunctionBlock")
override fun onTorStopped() {
}
override fun onTorStatusUpdate(entry: String?, status: String?) {
if (entry == null) return
view.tor_bootstrap_status_message.text = entry
if (entry.startsWith(BOOTSTRAPPED_PREFIX)) {
val percentIdx = entry.indexOf("%")
val percent = entry.substring(
BOOTSTRAPPED_PREFIX.length,
percentIdx
)
with(view.tor_bootstrap_progress) {
progress = percent.toInt()
}
}
}
companion object {
const val LAYOUT_ID = R.layout.tor_bootstrap_connect
const val BOOTSTRAPPED_PREFIX = "NOTICE: Bootstrapped "
}
}
/* 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.home.sessioncontrol.viewholders
import android.text.method.ScrollingMovementMethod
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.tor_bootstrap_logger.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.tor.TorEvents
import org.mozilla.fenix.components.Components
class TorBootstrapLoggerViewHolder(
private val view: View,
private val components: Components
) : RecyclerView.ViewHolder(view), TorEvents {
private var entries = mutableListOf<String>()
init {
components.torController.registerTorListener(this)
val currentEntries = components.torController.logEntries
.filter { it.first != null }
.filter { !(it.first!!.startsWith("Circuit") && it.second == "ON") }
// Keep synchronized with format in onTorStatusUpdate
.flatMap { listOf("(${it.second}) '${it.first}'") }
val entriesLen = currentEntries.size
val subListOffset = if (entriesLen > MAX_NEW_ENTRIES) MAX_NEW_ENTRIES else entriesLen
entries = currentEntries.subList((entriesLen - subListOffset), entriesLen) as MutableList<String>
val initLog = "---------------" + view.resources.getString(R.string.tor_initializing_log) + "---------------"
entries.add(0, initLog)
with(view.tor_bootstrap_log_entries) {
movementMethod = ScrollingMovementMethod()
text = formatLogEntries(entries)
}
}
private fun formatLogEntries(entries: List<String>) = entries.joinToString("\n")
@SuppressWarnings("EmptyFunctionBlock")
override fun onTorConnecting() {
}
override fun onTorConnected() {
components.torController.unregisterTorListener(this)
}
@SuppressWarnings("EmptyFunctionBlock")
override fun onTorStopped() {
}
override fun onTorStatusUpdate(entry: String?, status: String?) {
if (status == null || entry == null) return
if (status == "ON" && entry.startsWith("Circuit")) return
if (entries.size > MAX_LINES) {
entries = entries.drop(1) as MutableList<String>
}
entries.add("($status) '$entry'")
view.tor_bootstrap_log_entries.text = formatLogEntries(entries)
}
companion object {
const val LAYOUT_ID = R.layout.tor_bootstrap_logger
const val MAX_NEW_ENTRIES = 24
const val MAX_LINES = 25
}
}
/* 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.home.sessioncontrol.viewholders
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.tor_bootstrap_pager.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.home.sessioncontrol.TorBootstrapInteractor
import org.mozilla.fenix.home.sessioncontrol.viewholders.torbootstrap.BootstrapPagerAdapter
class TorBootstrapPagerViewHolder(
view: View,
components: Components,
interactor: TorBootstrapInteractor
) : RecyclerView.ViewHolder(view) {
private val bootstrapPagerAdapter = BootstrapPagerAdapter(components, interactor)
init {
view.bootstrap_pager.apply {
adapter = bootstrapPagerAdapter
}
}
companion object {
const val LAYOUT_ID = R.layout.tor_bootstrap_pager
}
}
/* 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.home.sessioncontrol.viewholders.torbootstrap
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.home.sessioncontrol.TorBootstrapInteractor
import org.mozilla.fenix.home.sessioncontrol.viewholders.TorBootstrapConnectViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TorBootstrapLoggerViewHolder
class BootstrapPagerAdapter(
private val components: Components,
private val interactor: TorBootstrapInteractor
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
if (viewType == BOOTSTRAP_UI_PAGE_TYPE) {
val viewDVH = LayoutInflater.from(parent.context)
.inflate(TorBootstrapConnectViewHolder.LAYOUT_ID, parent, false)
return TorBootstrapConnectViewHolder(viewDVH, components, interactor)
} else {
val viewLVH = LayoutInflater.from(parent.context)
.inflate(TorBootstrapLoggerViewHolder.LAYOUT_ID, parent, false)
return TorBootstrapLoggerViewHolder(viewLVH, components)
}
}
@SuppressWarnings("EmptyFunctionBlock")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
}
override fun getItemViewType(position: Int): Int = position
override fun getItemCount(): Int = BOOTSTRAP_PAGE_COUNT
companion object {
const val BOOTSTRAP_UI_PAGE_TYPE = 0
const val BOOTSTRAP_LOG_PAGE_TYPE = 1
const val BOOTSTRAP_PAGE_COUNT = 2
}
}
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="302dp"
android:height="263dp"
android:viewportWidth="302"
android:viewportHeight="263">
<path
android:pathData="M279.49,222.64l-94.22,-0l-0,-79.61l94.22,-0z"
android:strokeWidth="1"
android:fillType="nonZero"
android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient
android:startY="203.70642"
android:startX="231.9"
android:endY="160.04314"
android:endX="232.90999"
android:type="linear">
<item android:offset="0.08" android:color="#FF7E4696"/>
<item android:offset="0.39" android:color="#9B7E4696"/>
<item android:offset="0.85" android:color="#007E4696"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M112.74,217.05l-102.32,-0l-0,-102.32l102.32,-0z"
android:strokeWidth="1"
android:fillType="nonZero"
android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient
android:startY="203.36"
android:startX="60.96"
android:endY="124.990005"
android:endX="62.249996"
android:type="linear">
<item android:offset="0.08" android:color="#FF00D9B5"/>
<item android:offset="0.3" android:color="#BA00D9B5"/>
<item android:offset="0.8" android:color="#1100D9B5"/>
<item android:offset="0.85" android:color="#0000D9B5"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M58,1L183.49,1C186.526,1.016 188.984,3.474 189,6.51L189,100.25L52.47,100.25L52.47,6.51C52.486,3.466 54.956,1.005 58,1Z"
android:strokeLineJoin="round"
android:strokeWidth="1.76"
android:fillColor="#F0D4FD"
android:strokeColor="#65318E"
android:fillType="nonZero"
android:strokeLineCap="round"/>
<path
android:pathData="M60.55,8.87h120.41v108.53h-120.41z"
android:strokeLineJoin="round"
android:strokeWidth="1.76"
android:fillColor="#FFFFFF"
android:strokeColor="#490260"
android:fillType="nonZero"
android:strokeLineCap="round"/>
<path
android:pathData="M60.55,8.87h120.41v108.53h-120.41z"
android:strokeWidth="1"
android:fillType="nonZero"
android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient
android:startY="-56.010002"
android:startX="120.75"
android:endY="120.520004"
android:endX="120.75"
android:type="linear">
<item android:offset="0.08" android:color="#FF00D9B5"/>
<item android:offset="0.12" android:color="#F700D9B5"/>
<item android:offset="0.19" android:color="#E200D9B5"/>
<item android:offset="0.26" android:color="#BF00D9B5"/>
<item android:offset="0.35" android:color="#8E00D9B5"/>
<item android:offset="0.44" android:color="#5100D9B5"/>
<item android:offset="0.54" android:color="#0700D9B5"/>
<item android:offset="0.54" android:color="#0000D9B5"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M77.5,120.83h134.33v10.38h-134.33z"
android:strokeLineJoin="round"
android:strokeWidth="1.76"
android:fillColor="#F0D4FD"
android:strokeColor="#65318E"
android:fillType="nonZero"
android:strokeLineCap="round"/>
<path
android:pathData="M52.47,100.25l0,10.29l25.03,20.67l0,-10.38z"
android:strokeLineJoin="round"
android:strokeWidth="1.76"
android:fillColor="#65318E"
android:strokeColor="#F0D4FD"
android:fillType="nonZero"
android:strokeLineCap="round"/>
<path
android:pathData="M60.33,33.17h43.89v37.87h-43.89z"
android:strokeWidth="1.76"
android:fillColor="#FFFFFF"
android:strokeColor="#65318E"
android:fillType="nonZero"/>
<path
android:pathData="M178.05,170.61L61.43,170.61L61.43,87C61.43,85.895 62.325,85 63.43,85L176,85C177.105,85 178,85.895 178,87L178.05,170.61Z"
android:strokeLineJoin="round"
android:strokeWidth="1.76"
android:fillColor="#00D9B5"
android:strokeColor="#490260"
android:fillType="nonZero"
android:strokeLineCap="round"/>
<path
android:pathData="M66.88,89.57h105.71v76.37h-105.71z"
android:strokeLineJoin="round"
android:strokeWidth="1.76"
android:fillColor="#F0D4FD"
android:strokeColor="#490260"
android:fillType="nonZero"