From 07bb1113f8fcedc73065af724656fb650f0ea30b Mon Sep 17 00:00:00 2001 From: Arturo Mejia Date: Fri, 9 Jul 2021 00:16:34 -0400 Subject: [PATCH] For #19886 - Add connection sub-menu. --- .../fenix/android/FenixDialogFragment.kt | 81 +++++++++++++ .../components/toolbar/BrowserToolbarView.kt | 5 +- .../ConnectionDetailsController.kt | 68 +++++++++++ .../ConnectionDetailsInteractor.kt | 23 ++++ .../ConnectionPanelDialogFragment.kt | 71 +++++++++++ .../quicksettings/QuickSettingsController.kt | 23 ++++ .../QuickSettingsFragmentState.kt | 28 ++++- .../QuickSettingsFragmentStore.kt | 21 +--- .../quicksettings/QuickSettingsInteractor.kt | 6 +- .../QuickSettingsSheetDialogFragment.kt | 66 +--------- .../quicksettings/WebSiteInfoInteractor.kt | 21 ++++ .../settings/quicksettings/WebsiteInfoView.kt | 68 +++++++++-- .../connection_details_website_info.xml | 114 ++++++++++++++++++ .../fragment_connection_details_dialog.xml | 14 +++ .../res/layout/quicksettings_website_info.xml | 14 +-- app/src/main/res/navigation/nav_graph.xml | 32 +++++ .../ConnectionDetailsInteractorTest.kt | 34 ++++++ .../DefaultConnectionDetailsControllerTest.kt | 106 ++++++++++++++++ .../DefaultQuickSettingsControllerTest.kt | 34 ++++++ .../QuickSettingsFragmentStoreTest.kt | 5 +- .../QuickSettingsInteractorTest.kt | 9 ++ .../quicksettings/WebsiteInfoViewTest.kt | 61 +++++++++- 22 files changed, 798 insertions(+), 106 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/android/FenixDialogFragment.kt create mode 100644 app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionDetailsController.kt create mode 100644 app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionDetailsInteractor.kt create mode 100644 app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionPanelDialogFragment.kt create mode 100644 app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebSiteInfoInteractor.kt create mode 100644 app/src/main/res/layout/connection_details_website_info.xml create mode 100644 app/src/main/res/layout/fragment_connection_details_dialog.xml create mode 100644 app/src/test/java/org/mozilla/fenix/settings/quicksettings/ConnectionDetailsInteractorTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/settings/quicksettings/DefaultConnectionDetailsControllerTest.kt diff --git a/app/src/main/java/org/mozilla/fenix/android/FenixDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/android/FenixDialogFragment.kt new file mode 100644 index 000000000..09577577a --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/android/FenixDialogFragment.kt @@ -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(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 + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt index e3f38d074..c1e4ce092 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt @@ -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) diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionDetailsController.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionDetailsController.kt new file mode 100644 index 000000000..4e978c725 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionDetailsController.kt @@ -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) + } + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionDetailsInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionDetailsInteractor.kt new file mode 100644 index 000000000..f8f3d0764 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionDetailsInteractor.kt @@ -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() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionPanelDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionPanelDialogFragment.kt new file mode 100644 index 000000000..021e3a8cf --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionPanelDialogFragment.kt @@ -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() + + 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) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt index 83a06fc43..b1bf6d74a 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt @@ -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. * diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentState.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentState.kt index bcf6ce193..bebea7236 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentState.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentState.kt @@ -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 ) } diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStore.kt index 45218a763..cc2464d0c 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStore.kt @@ -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] diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractor.kt index 08e3e0217..25f597732 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractor.kt @@ -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() + } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt index 9b3c12c20..f9f0c644f 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt @@ -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(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 } diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebSiteInfoInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebSiteInfoInteractor.kt new file mode 100644 index 000000000..150fd8053 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebSiteInfoInteractor.kt @@ -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 +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsiteInfoView.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsiteInfoView.kt index cbf2f1b62..f40bdd728 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsiteInfoView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsiteInfoView.kt @@ -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() + } } diff --git a/app/src/main/res/layout/connection_details_website_info.xml b/app/src/main/res/layout/connection_details_website_info.xml new file mode 100644 index 000000000..1ee94a809 --- /dev/null +++ b/app/src/main/res/layout/connection_details_website_info.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_connection_details_dialog.xml b/app/src/main/res/layout/fragment_connection_details_dialog.xml new file mode 100644 index 000000000..45718e468 --- /dev/null +++ b/app/src/main/res/layout/fragment_connection_details_dialog.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/layout/quicksettings_website_info.xml b/app/src/main/res/layout/quicksettings_website_info.xml index a041e1a98..400721e15 100644 --- a/app/src/main/res/layout/quicksettings_website_info.xml +++ b/app/src/main/res/layout/quicksettings_website_info.xml @@ -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" /> + style="@style/QuickSettingsText"> + app:srcCompat="@drawable/mozac_ic_lock"/> + + + + + + + + + + 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 + ) + ) + } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/DefaultQuickSettingsControllerTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/DefaultQuickSettingsControllerTest.kt index ed65e1575..3ea1c4b81 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/DefaultQuickSettingsControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/DefaultQuickSettingsControllerTest.kt @@ -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 + ) + ) + } + } } diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStoreTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStoreTest.kt index bf8994126..3a231abfa 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStoreTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStoreTest.kt @@ -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) diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractorTest.kt index ed1281684..773dbb9b2 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractorTest.kt @@ -73,4 +73,13 @@ class QuickSettingsInteractorTest { controller.handleBlockedItemsClicked() } } + + @Test + fun `WHEN calling onConnectionDetailsClicked THEN delegate to the controller`() { + interactor.onConnectionDetailsClicked() + + verify { + controller.handleConnectionDetailsClicked() + } + } } diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/WebsiteInfoViewTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/WebsiteInfoViewTest.kt index 0b278642c..9c22a1882 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/WebsiteInfoViewTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/WebsiteInfoViewTest.kt @@ -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) + } }