For #19886 - Add connection sub-menu.
parent
63368779df
commit
07bb1113f8
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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>
|
@ -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>
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue