For #19886 - Add connection sub-menu.

This commit is contained in:
Arturo Mejia 2021-07-09 00:16:34 -04:00
parent 63368779df
commit 07bb1113f8
22 changed files with 798 additions and 106 deletions

View File

@ -0,0 +1,81 @@
/* 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.android
import android.app.Dialog
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.appcompat.view.ContextThemeWrapper
import com.google.android.material.R
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import org.mozilla.fenix.HomeActivity
/**
* Base [AppCompatDialogFragment] that adds behaviour to create a top or bottom dialog.
*/
abstract class FenixDialogFragment : AppCompatDialogFragment() {
/**
* Indicates the position of the dialog top or bottom.
*/
abstract val gravity: Int
/**
* The layout id that will be render on the dialog.
*/
abstract val layoutId: Int
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return if (gravity == Gravity.BOTTOM) {
BottomSheetDialog(requireContext(), this.theme).apply {
setOnShowListener {
val bottomSheet =
findViewById<View>(R.id.design_bottom_sheet) as FrameLayout
val behavior = BottomSheetBehavior.from(bottomSheet)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}
} else {
Dialog(requireContext()).applyCustomizationsForTopDialog(inflateRootView())
}
}
private fun Dialog.applyCustomizationsForTopDialog(rootView: View): Dialog {
addContentView(
rootView,
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
)
)
window?.apply {
setGravity(gravity)
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
// This must be called after addContentView, or it won't fully fill to the edge.
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
return this
}
fun inflateRootView(container: ViewGroup? = null): View {
val contextThemeWrapper = ContextThemeWrapper(
activity,
(activity as HomeActivity).themeManager.currentThemeResource
)
return LayoutInflater.from(contextThemeWrapper).inflate(
layoutId,
container,
false
)
}
}

View File

@ -127,10 +127,7 @@ class BrowserToolbarView(
hint = secondaryTextColor,
separator = separatorColor,
trackingProtection = primaryTextColor,
highlight = ContextCompat.getColor(
context,
R.color.whats_new_notification_color
)
highlight = primaryTextColor
)
display.hint = context.getString(R.string.search_hint)

View File

@ -0,0 +1,68 @@
/* 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.settings.quicksettings
import android.content.Context
import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import mozilla.components.browser.state.state.SessionState
import mozilla.components.concept.engine.permission.SitePermissions
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.runIfFragmentIsAttached
/**
* [ConnectionDetailsController] controller.
*
* Delegated by View Interactors, handles container business logic and operates changes on it,
* complex Android interactions or communication with other features.
*/
interface ConnectionDetailsController {
/**
* @see [WebSiteInfoInteractor.onBackPressed]
*/
fun handleBackPressed()
}
/**
* Default behavior of [ConnectionDetailsController].
*
* @param dismiss callback allowing to request this entire Fragment to be dismissed.
*/
class DefaultConnectionDetailsController(
private val context: Context,
private val fragment: Fragment,
private val navController: NavController,
internal var sitePermissions: SitePermissions?,
private val gravity: Int,
private val getCurrentTab: () -> SessionState?,
private val dismiss: () -> Unit
) : ConnectionDetailsController {
override fun handleBackPressed() {
getCurrentTab()?.let { tab ->
context.components.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
fragment.runIfFragmentIsAttached {
dismiss()
val isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains
val directions =
BrowserFragmentDirections.actionGlobalQuickSettingsSheetDialogFragment(
sessionId = tab.id,
url = tab.content.url,
title = tab.content.title,
isSecured = tab.content.securityInfo.secure,
sitePermissions = sitePermissions,
gravity = gravity,
certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights,
isTrackingProtectionEnabled = isTrackingProtectionEnabled
)
navController.nav(R.id.quickSettingsSheetDialogFragment, directions)
}
}
}
}
}

View File

@ -0,0 +1,23 @@
/* 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.settings.quicksettings
/**
* [ConnectionPanelDialogFragment] interactor.
*
* Implements callbacks for each of [ConnectionPanelDialogFragment]'s Views declared possible user interactions,
* delegates all such user events to the [ConnectionDetailsController].
*
* @param controller [ConnectionDetailsController] which will be delegated for all users interactions,
* it expected to contain all business logic for how to act in response.
*/
class ConnectionDetailsInteractor(
private val controller: ConnectionDetailsController
) : WebSiteInfoInteractor {
override fun onBackPressed() {
controller.handleBackPressed()
}
}

View File

@ -0,0 +1,71 @@
/* 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.settings.quicksettings
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import kotlinx.android.synthetic.main.fragment_connection_details_dialog.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.state.selector.findTabOrCustomTab
import mozilla.components.browser.state.state.SessionState
import org.mozilla.fenix.R
import org.mozilla.fenix.android.FenixDialogFragment
import org.mozilla.fenix.ext.requireComponents
@ExperimentalCoroutinesApi
class ConnectionPanelDialogFragment : FenixDialogFragment() {
@VisibleForTesting
private lateinit var connectionView: WebsiteInfoView
private val args by navArgs<ConnectionPanelDialogFragmentArgs>()
override val gravity: Int get() = args.gravity
override val layoutId: Int = R.layout.fragment_connection_details_dialog
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val rootView = inflateRootView(container)
val controller = DefaultConnectionDetailsController(
context = requireContext(),
fragment = this,
navController = findNavController(),
sitePermissions = args.sitePermissions,
gravity = args.gravity,
getCurrentTab = ::getCurrentTab,
dismiss = ::dismiss
)
val interactor = ConnectionDetailsInteractor(controller)
connectionView = WebsiteInfoView(
container = rootView.connectionDetailsInfoLayout,
interactor = interactor,
isDetailsMode = true
)
return rootView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
connectionView.update(
WebsiteInfoState.createWebsiteInfoState(
args.url,
args.title,
args.isSecured,
args.certificateName
)
)
}
internal fun getCurrentTab(): SessionState? {
return requireComponents.core.store.state.findTabOrCustomTab(args.sessionId)
}
}

View File

@ -69,6 +69,12 @@ interface QuickSettingsController {
* @see [TrackingProtectionInteractor.onBlockedItemsClicked]
*/
fun handleBlockedItemsClicked()
/**
* Navigates to the connection details. Called when a user clicks on the
* "Secured or Insecure Connection" section.
*/
fun handleConnectionDetailsClicked()
}
/**
@ -205,6 +211,23 @@ class DefaultQuickSettingsController(
navController.nav(R.id.quickSettingsSheetDialogFragment, directions)
}
override fun handleConnectionDetailsClicked() {
dismiss.invoke()
val state = quickSettingsStore.state.webInfoState
val directions = ConnectionPanelDialogFragmentDirections
.actionGlobalConnectionDetailsDialogFragment(
sessionId = sessionId,
title = state.websiteTitle,
url = state.websiteUrl,
isSecured = state.websiteSecurityUiValues == WebsiteSecurityUiValues.SECURE,
certificateName = state.certificateName,
gravity = context.components.settings.toolbarPosition.androidGravity,
sitePermissions = sitePermissions
)
navController.nav(R.id.quickSettingsSheetDialogFragment, directions)
}
/**
* Request a certain set of runtime Android permissions.
*

View File

@ -41,7 +41,31 @@ data class WebsiteInfoState(
val websiteTitle: String,
val websiteSecurityUiValues: WebsiteSecurityUiValues,
val certificateName: String
) : State
) : State {
companion object {
/**
* Construct an initial [WebsiteInfoState]
* based on the current website's status and connection.
* While being displayed users have no way of modifying it.
*
* @param websiteUrl [String] the URL of the current web page.
* @param websiteTitle [String] the title of the current web page.
* @param isSecured [Boolean] whether the connection is secured (TLS) or not.
* @param certificateName [String] the certificate name of the current web page.
*/
fun createWebsiteInfoState(
websiteUrl: String,
websiteTitle: String,
isSecured: Boolean,
certificateName: String
): WebsiteInfoState {
val uiValues =
if (isSecured) WebsiteSecurityUiValues.SECURE else WebsiteSecurityUiValues.INSECURE
return WebsiteInfoState(websiteUrl, websiteTitle, uiValues, certificateName)
}
}
}
enum class WebsiteSecurityUiValues(
@StringRes val securityInfoRes: Int,
@ -55,7 +79,7 @@ enum class WebsiteSecurityUiValues(
),
INSECURE(
R.string.quick_settings_sheet_insecure_connection,
R.drawable.mozac_ic_globe,
R.drawable.mozac_ic_broken_lock,
R.color.photonRed50
)
}

View File

@ -16,6 +16,7 @@ import mozilla.components.lib.state.Store
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.settings.PhoneFeature
import org.mozilla.fenix.settings.quicksettings.QuickSettingsFragmentStore.Companion.createStore
import org.mozilla.fenix.settings.quicksettings.WebsiteInfoState.Companion.createWebsiteInfoState
import org.mozilla.fenix.settings.quicksettings.ext.shouldBeEnabled
import org.mozilla.fenix.settings.quicksettings.ext.shouldBeVisible
import org.mozilla.fenix.trackingprotection.TrackingProtectionState
@ -91,26 +92,6 @@ class QuickSettingsFragmentStore(
)
)
/**
* Construct an initial [WebsiteInfoState] to be rendered by [WebsiteInfoView]
* based on the current website's status and connection.
*
* While being displayed users have no way of modifying it.
*
* @param websiteUrl [String] the URL of the current web page.
* @param isSecured [Boolean] whether the connection is secured (TLS) or not.
*/
@VisibleForTesting
fun createWebsiteInfoState(
websiteUrl: String,
websiteTitle: String,
isSecured: Boolean,
certificateName: String
): WebsiteInfoState {
val uiValues = if (isSecured) WebsiteSecurityUiValues.SECURE else WebsiteSecurityUiValues.INSECURE
return WebsiteInfoState(websiteUrl, websiteTitle, uiValues, certificateName)
}
/**
* Construct an initial [WebsitePermissions
* State] to be rendered by [WebsitePermissionsView]

View File

@ -15,7 +15,7 @@ package org.mozilla.fenix.settings.quicksettings
*/
class QuickSettingsInteractor(
private val controller: QuickSettingsController
) : WebsitePermissionInteractor, TrackingProtectionInteractor {
) : WebsitePermissionInteractor, TrackingProtectionInteractor, WebSiteInfoInteractor {
override fun onPermissionsShown() {
controller.handlePermissionsShown()
}
@ -35,4 +35,8 @@ class QuickSettingsInteractor(
override fun onBlockedItemsClicked() {
controller.handleBlockedItemsClicked()
}
override fun onConnectionDetailsClicked() {
controller.handleConnectionDetailsClicked()
}
}

View File

@ -4,39 +4,27 @@
package org.mozilla.fenix.settings.quicksettings
import android.app.Dialog
import android.content.Intent
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle
import android.view.Gravity.BOTTOM
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.plus
import mozilla.components.lib.state.ext.consumeFrom
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.IntentReceiverActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.FragmentQuickSettingsDialogSheetBinding
import org.mozilla.fenix.android.FenixDialogFragment
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.settings.PhoneFeature
import com.google.android.material.R as MaterialR
/**
* Dialog that presents the user with information about
@ -44,7 +32,7 @@ import com.google.android.material.R as MaterialR
* - website tracking protection.
* - website permission.
*/
class QuickSettingsSheetDialogFragment : AppCompatDialogFragment() {
class QuickSettingsSheetDialogFragment : FenixDialogFragment() {
private lateinit var quickSettingsStore: QuickSettingsFragmentStore
private lateinit var quickSettingsController: QuickSettingsController
@ -58,6 +46,9 @@ class QuickSettingsSheetDialogFragment : AppCompatDialogFragment() {
private lateinit var binding: FragmentQuickSettingsDialogSheetBinding
override val gravity: Int get() = args.gravity
override val layoutId: Int = R.layout.fragment_quick_settings_dialog_sheet
@Suppress("DEPRECATION")
// https://github.com/mozilla-mobile/fenix/issues/19920
override fun onCreateView(
@ -106,7 +97,7 @@ class QuickSettingsSheetDialogFragment : AppCompatDialogFragment() {
interactor = QuickSettingsInteractor(quickSettingsController)
websiteInfoView = WebsiteInfoView(binding.websiteInfoLayout)
websiteInfoView = WebsiteInfoView(binding.websiteInfoLayout, interactor = interactor)
websitePermissionsView =
WebsitePermissionsView(binding.websitePermissionsLayout, interactor)
trackingProtectionView =
@ -115,33 +106,6 @@ class QuickSettingsSheetDialogFragment : AppCompatDialogFragment() {
return rootView
}
private fun inflateRootView(container: ViewGroup? = null): View {
val contextThemeWrapper = ContextThemeWrapper(
activity,
(activity as HomeActivity).themeManager.currentThemeResource
)
return LayoutInflater.from(contextThemeWrapper).inflate(
R.layout.fragment_quick_settings_dialog_sheet,
container,
false
)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return if (args.gravity == BOTTOM) {
BottomSheetDialog(requireContext(), this.theme).apply {
setOnShowListener {
val bottomSheet =
findViewById<View>(MaterialR.id.design_bottom_sheet) as FrameLayout
val behavior = BottomSheetBehavior.from(bottomSheet)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}
} else {
Dialog(requireContext()).applyCustomizationsForTopDialog(inflateRootView())
}
}
@ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -177,24 +141,6 @@ class QuickSettingsSheetDialogFragment : AppCompatDialogFragment() {
tryToRequestPermissions = false
}
private fun Dialog.applyCustomizationsForTopDialog(rootView: View): Dialog {
addContentView(
rootView,
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
)
)
window?.apply {
setGravity(args.gravity)
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
// This must be called after addContentView, or it won't fully fill to the edge.
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
return this
}
private fun arePermissionsGranted(requestCode: Int, grantResults: IntArray) =
requestCode == REQUEST_CODE_QUICK_SETTINGS_PERMISSIONS && grantResults.all { it == PERMISSION_GRANTED }

View File

@ -0,0 +1,21 @@
/* 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.settings.quicksettings
/**
* Contract declaring all possible user interactions with [WebsitePermissionsView].
*/
interface WebSiteInfoInteractor {
/**
* Indicates there are website permissions allowed / blocked for the current website.
* which, status which is shown to the user.
*/
fun onConnectionDetailsClicked() = Unit
/**
* Called whenever back is pressed.
*/
fun onBackPressed() = Unit
}

View File

@ -6,6 +6,7 @@ package org.mozilla.fenix.settings.quicksettings
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.core.content.ContextCompat.getColor
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.support.ktx.android.content.getDrawableWithTint
@ -20,18 +21,32 @@ import org.mozilla.fenix.databinding.QuicksettingsWebsiteInfoBinding
* Currently it does not support any user interaction.
*
* @param container [ViewGroup] in which this View will inflate itself.
* @param icons [BrowserIcons] instance for rendering the sites icon.
* @param icons Icons component for loading, caching and processing website icons.
* @param interactor [WebSiteInfoInteractor] which will have delegated to all user interactions.
* @param isDetailsMode Indicates if the view should be shown in detailed mode or not.
* In normal mode only the url and connection status are visible.
* In detailed mode, the title, certificate and back button are visible,
* additionally to all the views in normal mode.
*/
class WebsiteInfoView(
container: ViewGroup,
private val icons: BrowserIcons = container.context.components.core.icons
private val icons: BrowserIcons = container.context.components.core.icons,
val interactor: WebSiteInfoInteractor,
val isDetailsMode: Boolean = false
) {
val binding = QuicksettingsWebsiteInfoBinding.inflate(
LayoutInflater.from(container.context),
container,
true
)
val layoutId =
if (isDetailsMode) R.layout.connection_details_website_info else R.layout.quicksettings_website_info
override val containerView: View = LayoutInflater.from(container.context)
.inflate(layoutId, container, true)
/**
* Allows changing what this View displays.
*
@ -41,17 +56,56 @@ class WebsiteInfoView(
icons.loadIntoView(binding.favicon_image, state.websiteUrl)
bindUrl(state.websiteUrl)
bindSecurityInfo(state.websiteSecurityUiValues)
if (isDetailsMode) {
bindCertificateName(state.certificateName)
bindTitle(state.websiteTitle)
bindBackButtonListener()
}
}
private fun bindUrl(websiteUrl: String) {
binding.url.text = websiteUrl.tryGetHostFromUrl()
url.text = if (isDetailsMode) websiteUrl else websiteUrl.tryGetHostFromUrl()
}
private fun bindSecurityInfo(uiValues: WebsiteSecurityUiValues) {
val tint = getColor(binding.root.context, uiValues.iconTintRes)
binding.securityInfo.setText(uiValues.securityInfoRes)
binding.securityInfoIcon.setImageDrawable(
binding.root.context.getDrawableWithTint(uiValues.iconRes, tint)
val tint = getColor(containerView.context, uiValues.iconTintRes)
securityInfo.setText(uiValues.securityInfoRes)
if (!isDetailsMode) {
bindConnectionDetailsListener()
}
securityInfoIcon.setImageDrawable(
containerView.context.getDrawableWithTint(uiValues.iconRes, tint)
)
}
@VisibleForTesting
internal fun bindConnectionDetailsListener() {
securityInfo.setOnClickListener {
interactor.onConnectionDetailsClicked()
}
}
@VisibleForTesting
internal fun bindBackButtonListener() {
details_back.isVisible = true
details_back.setOnClickListener {
interactor.onBackPressed()
}
}
@VisibleForTesting
internal fun bindTitle(websiteTitle: String) {
title.text = websiteTitle
if (websiteTitle.isEmpty()) {
title_container.isVisible = false
}
}
@VisibleForTesting
internal fun bindCertificateName(cert: String) {
val certificateLabel =
containerView.context.getString(R.string.certificate_info_verified_by, cert)
certificateInfo.text = certificateLabel
certificateInfo.isVisible = cert.isNotEmpty()
}
}

View File

@ -0,0 +1,114 @@
<?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/. -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/website_info_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?foundation"
android:orientation="horizontal">
<ImageView
android:id="@+id/details_back"
android:layout_width="@dimen/quicksettings_item_height"
android:layout_height="@dimen/quicksettings_item_height"
android:scaleType="center"
android:contentDescription="@string/etp_back_button_content_description"
app:tint="?attr/primaryText"
app:srcCompat="@drawable/mozac_ic_back" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/title_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:minHeight="@dimen/tracking_protection_item_height"
android:paddingVertical="8dp">
<ImageView
android:id="@+id/favicon_image"
android:layout_width="24dp"
android:layout_height="24dp"
android:importantForAccessibility="no"
android:scaleType="fitCenter"
android:layout_marginStart="16dp"
android:layout_gravity="center_vertical"
tools:drawable="@drawable/ic_internet" />
<TextView
android:id="@+id/title"
style="@style/QuickSettingsText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingStart="8dp"
android:textStyle="bold"
tools:text="Wikipedia"
tools:ignore="RtlSymmetry" />
</LinearLayout>
<TextView
android:id="@+id/url"
style="@style/QuickSettingsText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
tools:text="https://wikipedia.org" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/tracking_protection_item_height"
android:orientation="horizontal"
style="@style/QuickSettingsText"
android:paddingVertical="8dp">
<ImageView
android:id="@+id/securityInfoIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:srcCompat="@drawable/mozac_ic_lock"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="0dp"
android:orientation="vertical">
<TextView
android:id="@+id/securityInfo"
style="@style/QuickSettingsLargeText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="0dp"
android:paddingEnd="0dp"
tools:text="Connection is secure" />
<TextView
android:id="@+id/certificateInfo"
style="@style/QuickSettingsSmallText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:paddingTop="0dp"
android:paddingBottom="2dp"
tools:text="Verified By: E-Corp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,14 @@
<?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.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/quick_settings_sheet"
android:fillViewport="true">
<FrameLayout
android:id="@+id/connectionDetailsInfoLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</androidx.core.widget.NestedScrollView>

View File

@ -35,8 +35,10 @@
style="@style/QuickSettingsText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingTop="8dp"
tools:text="https://wikipedia.org" />
tools:text="https://wikipedia.org"
tools:ignore="RtlSymmetry" />
</LinearLayout>
<LinearLayout
@ -44,16 +46,13 @@
android:layout_height="wrap_content"
android:minHeight="@dimen/tracking_protection_item_height"
android:orientation="horizontal"
style="@style/QuickSettingsText"
android:paddingTop="8dp"
android:paddingBottom="8dp">
style="@style/QuickSettingsText">
<ImageView
android:id="@+id/securityInfoIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:srcCompat="@drawable/mozac_ic_lock"
android:paddingEnd="8dp"/>
app:srcCompat="@drawable/mozac_ic_lock"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -61,8 +60,9 @@
<TextView
android:id="@+id/securityInfo"
style="@style/QuickSettingsLargeText"
android:minHeight="@dimen/quicksettings_item_height"
android:layout_width="match_parent"
android:paddingStart="0dp"
android:paddingStart="8dp"
android:layout_height="wrap_content"
app:drawableEndCompat="@drawable/ic_arrowhead_right"
android:paddingEnd="0dp"

View File

@ -110,6 +110,9 @@
<action
android:id="@+id/action_global_quickSettingsSheetDialogFragment"
app:destination="@id/quickSettingsSheetDialogFragment" />
<action
android:id="@+id/action_global_connectionDetailsDialogFragment"
app:destination="@id/connectionPanelDialogFragment" />
<action
android:id="@+id/action_global_tabsTrayFragment"
app:destination="@id/tabsTrayFragment"
@ -813,6 +816,35 @@
android:defaultValue="80"
app:argType="integer" />
</dialog>
<dialog
android:id="@+id/connectionPanelDialogFragment"
android:name="org.mozilla.fenix.settings.quicksettings.ConnectionPanelDialogFragment"
tools:layout="@layout/quicksettings_website_info">
<argument
android:name="sessionId"
app:argType="string" />
<argument
android:name="title"
app:argType="string" />
<argument
android:name="url"
app:argType="string" />
<argument
android:name="isSecured"
app:argType="boolean" />
<argument
android:name="certificateName"
android:defaultValue=" "
app:argType="string" />
<argument
android:name="sitePermissions"
app:argType="mozilla.components.concept.engine.permission.SitePermissions"
app:nullable="true" />
<argument
android:name="gravity"
android:defaultValue="80"
app:argType="integer" />
</dialog>
<fragment
android:id="@+id/trackingProtectionBlockingFragment"
android:name="org.mozilla.fenix.trackingprotection.TrackingProtectionBlockingFragment"

View File

@ -0,0 +1,34 @@
/* 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.settings.quicksettings
import io.mockk.mockk
import io.mockk.verify
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
class ConnectionDetailsInteractorTest {
private lateinit var controller: ConnectionDetailsController
private lateinit var interactor: ConnectionDetailsInteractor
@Before
fun setUp() {
controller = mockk(relaxed = true)
interactor = ConnectionDetailsInteractor(controller)
}
@Test
fun `WHEN onBackPressed is called THEN delegate the controller`() {
interactor.onBackPressed()
verify {
controller.handleBackPressed()
}
}
}

View File

@ -0,0 +1,106 @@
/* 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.settings.quicksettings
import android.content.Context
import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.mockk.slot
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.concept.engine.permission.SitePermissions
import mozilla.components.feature.session.TrackingProtectionUseCases
import mozilla.components.support.test.robolectric.testContext
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@ExperimentalCoroutinesApi
@RunWith(FenixRobolectricTestRunner::class)
class DefaultConnectionDetailsControllerTest {
private lateinit var context: Context
@MockK(relaxed = true)
private lateinit var navController: NavController
@MockK(relaxed = true)
private lateinit var fragment: Fragment
@MockK(relaxed = true)
private lateinit var sitePermissions: SitePermissions
@MockK(relaxed = true)
private lateinit var dismiss: () -> Unit
private lateinit var controller: DefaultConnectionDetailsController
private lateinit var tab: TabSessionState
private var gravity = 54
@Before
fun setUp() {
MockKAnnotations.init(this)
val trackingProtectionUseCases: TrackingProtectionUseCases = mockk(relaxed = true)
context = spyk(testContext)
tab = createTab("https://mozilla.org")
controller = DefaultConnectionDetailsController(
fragment = fragment,
context = context,
navController = navController,
sitePermissions = sitePermissions,
gravity = gravity,
getCurrentTab = { tab },
dismiss = dismiss
)
every { fragment.context } returns context
every { context.components.useCases.trackingProtectionUseCases } returns trackingProtectionUseCases
val onComplete = slot<(Boolean) -> Unit>()
every {
trackingProtectionUseCases.containsException.invoke(
any(),
capture(onComplete)
)
}.answers { onComplete.captured.invoke(true) }
}
@Test
fun `WHEN handleBackPressed is called THEN should call dismiss and navigate`() {
controller.handleBackPressed()
verify {
dismiss.invoke()
navController.nav(
R.id.quickSettingsSheetDialogFragment,
BrowserFragmentDirections.actionGlobalQuickSettingsSheetDialogFragment(
sessionId = tab.id,
url = tab.content.url,
title = tab.content.title,
isSecured = tab.content.securityInfo.secure,
sitePermissions = sitePermissions,
certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights,
isTrackingProtectionEnabled = true
)
)
}
}
}

View File

@ -388,4 +388,38 @@ class DefaultQuickSettingsControllerTest {
)
}
}
@Test
fun `WHEN handleConnectionDetailsClicked THEN call dismiss and navigate to the connection details dialog`() {
every { context.components.core.store } returns browserStore
every { context.components.settings } returns appSettings
every { context.components.settings.toolbarPosition.androidGravity } returns mockk(relaxed = true)
val state = WebsiteInfoState.createWebsiteInfoState(
websiteUrl = tab.content.url,
websiteTitle = tab.content.title,
isSecured = true,
certificateName = "certificateName"
)
every { store.state.webInfoState } returns state
controller.handleConnectionDetailsClicked()
verify {
dismiss.invoke()
navController.nav(
R.id.quickSettingsSheetDialogFragment,
QuickSettingsSheetDialogFragmentDirections.actionGlobalConnectionDetailsDialogFragment(
sessionId = tab.id,
url = state.websiteUrl,
title = state.websiteTitle,
isSecured = true,
sitePermissions = sitePermissions,
gravity = context.components.settings.toolbarPosition.androidGravity
)
)
}
}
}

View File

@ -37,6 +37,7 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.settings.PhoneFeature
import org.mozilla.fenix.settings.quicksettings.QuickSettingsFragmentStore.Companion.toWebsitePermission
import org.mozilla.fenix.settings.quicksettings.WebsiteInfoState.Companion.createWebsiteInfoState
import org.mozilla.fenix.settings.quicksettings.ext.shouldBeEnabled
import org.mozilla.fenix.settings.quicksettings.ext.shouldBeVisible
import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_BLOCK_ALL
@ -100,7 +101,7 @@ class QuickSettingsFragmentStoreTest {
val certificateIssuer = "issuer"
val securedStatus = true
val state = QuickSettingsFragmentStore.createWebsiteInfoState(websiteUrl, websiteTitle, securedStatus, certificateIssuer)
val state = createWebsiteInfoState(websiteUrl, websiteTitle, securedStatus, certificateIssuer)
assertNotNull(state)
assertSame(websiteUrl, state.websiteUrl)
@ -115,7 +116,7 @@ class QuickSettingsFragmentStoreTest {
val certificateIssuer = "issuer"
val securedStatus = false
val state = QuickSettingsFragmentStore.createWebsiteInfoState(websiteUrl, websiteTitle, securedStatus, certificateIssuer)
val state = createWebsiteInfoState(websiteUrl, websiteTitle, securedStatus, certificateIssuer)
assertNotNull(state)
assertSame(websiteUrl, state.websiteUrl)

View File

@ -73,4 +73,13 @@ class QuickSettingsInteractorTest {
controller.handleBlockedItemsClicked()
}
}
@Test
fun `WHEN calling onConnectionDetailsClicked THEN delegate to the controller`() {
interactor.onConnectionDetailsClicked()
verify {
controller.handleConnectionDetailsClicked()
}
}
}

View File

@ -7,6 +7,7 @@ package org.mozilla.fenix.settings.quicksettings
import android.widget.FrameLayout
import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.icons.IconRequest
@ -15,6 +16,7 @@ import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.QuicksettingsWebsiteInfoBinding
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@ -24,13 +26,15 @@ class WebsiteInfoViewTest {
private lateinit var view: WebsiteInfoView
private lateinit var icons: BrowserIcons
private lateinit var binding: QuicksettingsWebsiteInfoBinding
private lateinit var interactor: WebSiteInfoInteractor
@Before
fun setup() {
icons = mockk(relaxed = true)
view = WebsiteInfoView(FrameLayout(testContext), icons)
every { icons.loadIntoView(view.favicon_image, any()) } returns mockk()
interactor = mockk(relaxed = true)
view = WebsiteInfoView(FrameLayout(testContext), icons, interactor)
binding = view.binding
every { icons.loadIntoView(view.favicon_image, any()) } returns mockk()
}
@Test
@ -44,7 +48,7 @@ class WebsiteInfoViewTest {
certificateName = ""
))
verify { icons.loadIntoView(binding.favicon_image, IconRequest(websiteUrl)) }
verify { icons.loadIntoView(binding..favicon_image, IconRequest(websiteUrl)) }
assertEquals("mozilla.org", binding.url.text)
assertEquals("Secure Connection", binding.securityInfo.text)
@ -63,4 +67,55 @@ class WebsiteInfoViewTest {
assertEquals("Insecure Connection", binding.securityInfo.text)
}
@Test
fun `WHEN updating on detailed mode THEN bind the certificate, title and back button listener`() {
val view = spyk(WebsiteInfoView(FrameLayout(testContext), icons, interactor, isDetailsMode = true))
view.update(WebsiteInfoState(
websiteUrl = "https://mozilla.org",
websiteTitle = "Mozilla",
websiteSecurityUiValues = WebsiteSecurityUiValues.INSECURE,
certificateName = "Certificate"
))
verify {
view.bindCertificateName("Certificate")
view.bindTitle("Mozilla")
view.bindBackButtonListener()
}
}
@Test
fun `WHEN updating on not detailed mode THEN only connection details listener should be binded`() {
val view = WebsiteInfoView(FrameLayout(testContext), icons, interactor, isDetailsMode = false)
view.update(WebsiteInfoState(
websiteUrl = "https://mozilla.org",
websiteTitle = "Mozilla",
websiteSecurityUiValues = WebsiteSecurityUiValues.INSECURE,
certificateName = "Certificate"
))
verify(exactly = 0) {
view.bindCertificateName("Certificate")
view.bindTitle("Mozilla")
view.bindBackButtonListener()
}
verify {
view.bindConnectionDetailsListener()
}
}
@Test
fun `WHEN rendering THEN use the correct layout`() {
val normalView = WebsiteInfoView(FrameLayout(testContext), icons, interactor, isDetailsMode = false)
assertEquals(R.layout.quicksettings_website_info, normalView.layoutId)
val detailedView = WebsiteInfoView(FrameLayout(testContext), icons, interactor, isDetailsMode = true)
assertEquals(R.layout.connection_details_website_info, detailedView.layoutId)
}
}