/* 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.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.LinearLayout import androidx.appcompat.app.AppCompatDialogFragment import androidx.appcompat.view.ContextThemeWrapper import androidx.core.widget.NestedScrollView import androidx.navigation.fragment.NavHostFragment.findNavController import com.google.android.material.bottomsheet.BottomSheetDialog import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import mozilla.components.browser.session.Session import mozilla.components.feature.sitepermissions.SitePermissions import mozilla.components.support.ktx.kotlin.toUri import org.mozilla.fenix.FenixViewModelProvider import org.mozilla.fenix.HomeActivity 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.mvi.ActionBusFactory import org.mozilla.fenix.mvi.getAutoDisposeObservable import org.mozilla.fenix.mvi.getManagedEmitter import org.mozilla.fenix.settings.PhoneFeature import java.net.MalformedURLException import java.net.URL import kotlin.coroutines.CoroutineContext private const val REQUEST_CODE_QUICK_SETTINGS_PERMISSIONS = 4 @SuppressWarnings("TooManyFunctions") class QuickSettingsSheetDialogFragment : AppCompatDialogFragment(), CoroutineScope { 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 lateinit var job: Job private var sitePermissions: SitePermissions? = null override val coroutineContext: CoroutineContext get() = Dispatchers.IO + job override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) job = Job() } 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 { val customDialog = if (promptGravity == BOTTOM) { return BottomSheetDialog(requireContext(), this.theme) } else { Dialog(requireContext()) } return customDialog.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)) } } override fun onDestroy() { super.onDestroy() job.cancel() } 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 = try { URL(url).host } catch (e: MalformedURLException) { url } launch { if (!ExceptionDomains.load(context).contains(host)) { ExceptionDomains.add(context, host) } else { ExceptionDomains.remove(context, listOf(host)) } } } 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 -> { launch(Dispatchers.Main) { val reportUrl = String.format(BrowserFragment.REPORT_SITE_ISSUE_URL, it.url) requireComponents.useCases.tabsUseCases.addTab.invoke(reportUrl) } dismiss() } is QuickSettingsAction.ToggleTrackingProtection -> { val trackingEnabled = it.trackingProtection context?.let { context: Context -> toggleTrackingProtection(context, url) } launch(Dispatchers.Main) { getManagedEmitter().onNext( QuickSettingsChange.Change( url, isSecured, trackingEnabled, sitePermissions ) ) requireContext().components.useCases.sessionUseCases.reload.invoke() } } is QuickSettingsAction.TogglePermission -> { launch { 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) { super.onUrlChanged(session, url) launch { 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) { super.onTrackerBlockingEnabledChanged(session, blockingEnabled) getManagedEmitter().onNext( QuickSettingsChange.Change( session.url, session.securityInfo.secure, blockingEnabled, sitePermissions ) ) } override fun onSecurityChanged(session: Session, securityInfo: Session.SecurityInfo) { super.onSecurityChanged(session, securityInfo) getManagedEmitter().onNext( QuickSettingsChange.Change( session.url, securityInfo.secure, session.trackerBlockingEnabled, sitePermissions ) ) } } }