/* 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.app.Dialog import android.content.Context import android.content.Intent import android.content.pm.PackageManager.PERMISSION_GRANTED import android.graphics.Color import android.graphics.drawable.ColorDrawable 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.net.toUri import androidx.core.widget.NestedScrollView import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.NavHostFragment.findNavController 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.launch import mozilla.components.browser.session.Session import mozilla.components.feature.sitepermissions.SitePermissions import org.mozilla.fenix.FenixViewModelProvider import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.IntentReceiverActivity import org.mozilla.fenix.R import org.mozilla.fenix.browser.BrowserFragment import org.mozilla.fenix.exceptions.ExceptionDomains import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.tryGetHostFromUrl import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.getAutoDisposeObservable import org.mozilla.fenix.mvi.getManagedEmitter import org.mozilla.fenix.settings.PhoneFeature import com.google.android.material.R as MaterialR private const val REQUEST_CODE_QUICK_SETTINGS_PERMISSIONS = 4 @SuppressWarnings("TooManyFunctions") class QuickSettingsSheetDialogFragment : AppCompatDialogFragment() { private val safeArguments get() = requireNotNull(arguments) private val sessionId: String by lazy { QuickSettingsSheetDialogFragmentArgs.fromBundle(safeArguments).sessionId } private val url: String by lazy { QuickSettingsSheetDialogFragmentArgs.fromBundle(safeArguments).url } private val isSecured: Boolean by lazy { QuickSettingsSheetDialogFragmentArgs.fromBundle(safeArguments).isSecured } private val isTrackingProtectionOn: Boolean by lazy { QuickSettingsSheetDialogFragmentArgs.fromBundle(safeArguments).isTrackingProtectionOn } private val promptGravity: Int by lazy { QuickSettingsSheetDialogFragmentArgs.fromBundle(safeArguments).gravity } private lateinit var quickSettingsComponent: QuickSettingsComponent private var sitePermissions: SitePermissions? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { sitePermissions = QuickSettingsSheetDialogFragmentArgs.fromBundle(safeArguments).sitePermissions val rootView = inflateRootView(container) requireComponents.core.sessionManager.findSessionById(sessionId)?.register(sessionObserver, view = rootView) quickSettingsComponent = QuickSettingsComponent( rootView as NestedScrollView, ActionBusFactory.get(this), FenixViewModelProvider.create( this, QuickSettingsViewModel::class.java ) { QuickSettingsViewModel( QuickSettingsState( QuickSettingsState.Mode.Normal( url, isSecured, isTrackingProtectionOn, sitePermissions ) ) ) } ) 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 (promptGravity == 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()) } } private fun Dialog.applyCustomizationsForTopDialog(rootView: View): Dialog { addContentView( rootView, LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT ) ) window?.apply { setGravity(promptGravity) 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 } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, grantResults: IntArray ) { if (arePermissionsGranted(requestCode, grantResults)) { val feature = requireNotNull(PhoneFeature.findFeatureBy(permissions)) getManagedEmitter() .onNext(QuickSettingsChange.PermissionGranted(feature, sitePermissions)) } } private fun arePermissionsGranted(requestCode: Int, grantResults: IntArray) = requestCode == REQUEST_CODE_QUICK_SETTINGS_PERMISSIONS && grantResults.all { it == PERMISSION_GRANTED } private fun toggleTrackingProtection(context: Context, url: String) { val host = url.tryGetHostFromUrl() lifecycleScope.launch { ExceptionDomains(context).toggle(host) } } @ExperimentalCoroutinesApi override fun onResume() { super.onResume() getAutoDisposeObservable() .subscribe { when (it) { is QuickSettingsAction.SelectBlockedByAndroid -> { requestPermissions(it.permissions, REQUEST_CODE_QUICK_SETTINGS_PERMISSIONS) } is QuickSettingsAction.SelectTrackingProtectionSettings -> { val directions = QuickSettingsSheetDialogFragmentDirections .actionQuickSettingsSheetDialogFragmentToTrackingProtectionFragment() findNavController(this@QuickSettingsSheetDialogFragment).navigate(directions) } is QuickSettingsAction.SelectReportProblem -> { lifecycleScope.launch(Dispatchers.Main) { val reportUrl = String.format(BrowserFragment.REPORT_SITE_ISSUE_URL, it.url) requireComponents.useCases.tabsUseCases.addTab.invoke(reportUrl) val sessionManager = requireComponents.core.sessionManager if (sessionManager.findSessionById(sessionId)?.isCustomTabSession() == true) { val intent = Intent(context, IntentReceiverActivity::class.java) intent.action = Intent.ACTION_VIEW startActivity(intent) } } dismiss() } is QuickSettingsAction.ToggleTrackingProtection -> { val trackingEnabled = it.trackingProtection context?.let { context: Context -> toggleTrackingProtection(context, url) } lifecycleScope.launch(Dispatchers.Main) { getManagedEmitter().onNext( QuickSettingsChange.Change( url, isSecured, trackingEnabled, sitePermissions ) ) requireContext().components.useCases.sessionUseCases.reload.invoke() } } is QuickSettingsAction.TogglePermission -> { lifecycleScope.launch(Dispatchers.IO) { sitePermissions = quickSettingsComponent.toggleSitePermission( context = requireContext(), featurePhone = it.featurePhone, url = url, sitePermissions = sitePermissions ) launch(Dispatchers.Main) { getManagedEmitter() .onNext( QuickSettingsChange.Stored( it.featurePhone, sitePermissions ) ) requireContext().components.useCases.sessionUseCases.reload.invoke() } } } } } if (isVisible) { getManagedEmitter() .onNext(QuickSettingsChange.PromptRestarted(sitePermissions)) } } private val sessionObserver = object : Session.Observer { override fun onUrlChanged(session: Session, url: String) { lifecycleScope.launch(Dispatchers.IO) { val host = session.url.toUri().host val sitePermissions: SitePermissions? = host?.let { val storage = requireContext().components.core.permissionStorage storage.findSitePermissionsBy(it) } launch(Dispatchers.Main) { getManagedEmitter().onNext( QuickSettingsChange.Change( url, session.securityInfo.secure, session.trackerBlockingEnabled, sitePermissions ) ) } } } override fun onTrackerBlockingEnabledChanged(session: Session, blockingEnabled: Boolean) { getManagedEmitter().onNext( QuickSettingsChange.Change( session.url, session.securityInfo.secure, blockingEnabled, sitePermissions ) ) } override fun onSecurityChanged(session: Session, securityInfo: Session.SecurityInfo) { getManagedEmitter().onNext( QuickSettingsChange.Change( session.url, securityInfo.secure, session.trackerBlockingEnabled, sitePermissions ) ) } } }