You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
iceraven-browser/app/src/main/java/org/mozilla/fenix/addons/InstalledAddonDetailsFragme...

426 lines
16 KiB
Kotlin

/* 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.addons
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.Navigation
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.concept.engine.webextension.EnableSource
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.AddonManager
import mozilla.components.feature.addons.AddonManagerException
import mozilla.components.feature.addons.ui.translateName
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.FragmentInstalledAddOnDetailsBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.showToolbar
/**
* An activity to show the details of a installed add-on.
*/
@Suppress("LargeClass", "TooManyFunctions")
class InstalledAddonDetailsFragment : Fragment() {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal lateinit var addon: Addon
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal val binding get() = _binding!!
private var _binding: FragmentInstalledAddOnDetailsBinding? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
if (!::addon.isInitialized) {
addon = AddonDetailsFragmentArgs.fromBundle(requireNotNull(arguments)).addon
}
setBindingAndBindUI(
FragmentInstalledAddOnDetailsBinding.inflate(
inflater,
container,
false,
),
)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
bindAddon()
}
override fun onResume() {
super.onResume()
context?.let {
showToolbar(title = addon.translateName(it))
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun setBindingAndBindUI(binding: FragmentInstalledAddOnDetailsBinding) {
_binding = binding
bindUI()
}
private fun bindAddon() {
lifecycleScope.launch(Dispatchers.IO) {
try {
val addons = requireContext().components.addonManager.getAddons()
lifecycleScope.launch(Dispatchers.Main) {
runIfFragmentIsAttached {
addons.find { addon.id == it.id }.let {
if (it == null) {
throw AddonManagerException(Exception("Addon ${addon.id} not found"))
} else {
addon = it
bindUI()
}
binding.addOnProgressBar.isVisible = false
binding.addonContainer.isVisible = true
}
}
}
} catch (e: AddonManagerException) {
lifecycleScope.launch(Dispatchers.Main) {
runIfFragmentIsAttached {
showSnackBar(
binding.root,
getString(R.string.mozac_feature_addons_failed_to_query_add_ons),
)
findNavController().popBackStack()
}
}
}
}
}
private fun bindUI() {
bindEnableSwitch()
bindSettings()
bindDetails()
bindPermissions()
bindAllowInPrivateBrowsingSwitch()
bindRemoveButton()
bindReportButton()
}
@VisibleForTesting
internal fun provideEnableSwitch() = binding.enableSwitch
@VisibleForTesting
internal fun providePrivateBrowsingSwitch() = binding.allowInPrivateBrowsingSwitch
@VisibleForTesting
@SuppressWarnings("LongMethod")
internal fun bindEnableSwitch() {
val switch = provideEnableSwitch()
val privateBrowsingSwitch = providePrivateBrowsingSwitch()
switch.isChecked = addon.isEnabled()
// When the ad-on is blocklisted or not correctly signed, we do not want to enable the toggle switch
// because users shouldn't be able to re-enable an add-on in this state.
if (
addon.isDisabledAsBlocklisted() ||
addon.isDisabledAsNotCorrectlySigned() ||
addon.isDisabledAsIncompatible()
) {
switch.isEnabled = false
return
}
switch.setOnCheckedChangeListener { v, isChecked ->
val addonManager = v.context.components.addonManager
switch.isClickable = false
disableButtons()
if (isChecked) {
enableAddon(
addonManager,
onSuccess = {
runIfFragmentIsAttached {
this.addon = it
switch.isClickable = true
privateBrowsingSwitch.isVisible = it.isEnabled()
privateBrowsingSwitch.isChecked =
it.incognito != Addon.Incognito.NOT_ALLOWED && it.isAllowedInPrivateBrowsing()
binding.settings.isVisible = shouldSettingsBeVisible()
enableButtons()
context?.let {
showSnackBar(
binding.root,
getString(
R.string.mozac_feature_addons_successfully_enabled,
addon.translateName(it),
),
)
}
}
},
onError = {
runIfFragmentIsAttached {
switch.isClickable = true
enableButtons()
switch.isChecked = addon.isEnabled()
context?.let {
showSnackBar(
binding.root,
getString(
R.string.mozac_feature_addons_failed_to_enable,
addon.translateName(it),
),
)
}
}
},
)
} else {
binding.settings.isVisible = false
addonManager.disableAddon(
addon,
onSuccess = {
runIfFragmentIsAttached {
this.addon = it
switch.isClickable = true
privateBrowsingSwitch.isVisible = it.isEnabled()
enableButtons()
context?.let {
showSnackBar(
binding.root,
getString(
R.string.mozac_feature_addons_successfully_disabled,
addon.translateName(it),
),
)
}
}
},
onError = {
runIfFragmentIsAttached {
switch.isClickable = true
switch.isChecked = addon.isEnabled()
enableButtons()
context?.let {
showSnackBar(
binding.root,
getString(
R.string.mozac_feature_addons_failed_to_disable,
addon.translateName(it),
),
)
}
}
},
)
}
}
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun enableAddon(
addonManager: AddonManager,
onSuccess: (Addon) -> Unit,
onError: (Throwable) -> Unit,
) {
// If the addon is migrated from Fennec and supported in Fenix, for the addon to be enabled,
// we need to also request the addon to be enabled as supported by the app
if (addon.isSupported() && addon.isDisabledAsUnsupported()) {
addonManager.enableAddon(
addon,
EnableSource.APP_SUPPORT,
{ enabledAddon ->
addonManager.enableAddon(enabledAddon, EnableSource.USER, onSuccess, onError)
},
onError,
)
} else {
addonManager.enableAddon(addon, EnableSource.USER, onSuccess, onError)
}
}
@VisibleForTesting
internal fun bindAllowInPrivateBrowsingSwitch() {
val switch = providePrivateBrowsingSwitch()
switch.isVisible = addon.isEnabled()
if (addon.incognito == Addon.Incognito.NOT_ALLOWED) {
switch.isChecked = false
switch.isEnabled = false
switch.text = requireContext().getString(R.string.mozac_feature_addons_not_allowed_in_private_browsing)
return
}
switch.isChecked = addon.isAllowedInPrivateBrowsing()
switch.setOnCheckedChangeListener { v, isChecked ->
val addonManager = v.context.components.addonManager
switch.isClickable = false
disableButtons()
addonManager.setAddonAllowedInPrivateBrowsing(
addon,
isChecked,
onSuccess = {
runIfFragmentIsAttached {
this.addon = it
switch.isClickable = true
enableButtons()
}
},
onError = {
runIfFragmentIsAttached {
switch.isChecked = addon.isAllowedInPrivateBrowsing()
switch.isClickable = true
enableButtons()
}
},
)
}
}
private fun bindReportButton() {
binding.reportAddOn.setOnClickListener {
val shouldCreatePrivateSession = (activity as HomeActivity).browsingModeManager.mode.isPrivate
it.context.components.useCases.tabsUseCases.selectOrAddTab(
url = "${BuildConfig.AMO_BASE_URL}/android/feedback/addon/${addon.id}/",
private = shouldCreatePrivateSession,
ignoreFragment = true,
)
// Send user to the newly open tab.
Navigation.findNavController(it).navigate(
InstalledAddonDetailsFragmentDirections.actionGlobalBrowser(null),
)
}
}
private fun bindSettings() {
binding.settings.apply {
isVisible = shouldSettingsBeVisible()
setOnClickListener {
val settingUrl = addon.installedState?.optionsPageUrl ?: return@setOnClickListener
val directions = if (addon.installedState?.openOptionsPageInTab == true) {
val components = it.context.components
val shouldCreatePrivateSession =
(activity as HomeActivity).browsingModeManager.mode.isPrivate
// If the addon settings page is already open in a tab, select that one
components.useCases.tabsUseCases.selectOrAddTab(
url = settingUrl,
private = shouldCreatePrivateSession,
ignoreFragment = true,
)
InstalledAddonDetailsFragmentDirections.actionGlobalBrowser(null)
} else {
InstalledAddonDetailsFragmentDirections
.actionInstalledAddonFragmentToAddonInternalSettingsFragment(addon)
}
Navigation.findNavController(this).navigate(directions)
}
}
}
private fun bindDetails() {
binding.details.setOnClickListener {
val directions =
InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonDetailsFragment(
addon,
)
Navigation.findNavController(binding.root).navigate(directions)
}
}
private fun bindPermissions() {
binding.permissions.setOnClickListener {
val directions =
InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonPermissionsDetailsFragment(
addon,
)
Navigation.findNavController(binding.root).navigate(directions)
}
}
private fun bindRemoveButton() {
binding.removeAddOn.setOnClickListener {
setAllInteractiveViewsClickable(binding, false)
requireContext().components.addonManager.uninstallAddon(
addon,
onSuccess = {
runIfFragmentIsAttached {
setAllInteractiveViewsClickable(binding, true)
context?.let {
showSnackBar(
binding.root,
getString(
R.string.mozac_feature_addons_successfully_uninstalled,
addon.translateName(it),
),
)
}
binding.root.findNavController().popBackStack()
}
},
onError = { _, _ ->
runIfFragmentIsAttached {
setAllInteractiveViewsClickable(binding, true)
context?.let {
showSnackBar(
binding.root,
getString(
R.string.mozac_feature_addons_failed_to_uninstall,
addon.translateName(it),
),
)
}
}
},
)
}
}
private fun setAllInteractiveViewsClickable(
binding: FragmentInstalledAddOnDetailsBinding,
clickable: Boolean,
) {
binding.enableSwitch.isClickable = clickable
binding.settings.isClickable = clickable
binding.details.isClickable = clickable
binding.permissions.isClickable = clickable
binding.removeAddOn.isClickable = clickable
binding.reportAddOn.isClickable = clickable
}
private fun enableButtons() {
binding.removeAddOn.isEnabled = true
binding.reportAddOn.isEnabled = true
}
private fun disableButtons() {
binding.removeAddOn.isEnabled = false
binding.reportAddOn.isEnabled = false
}
private fun shouldSettingsBeVisible() = !addon.installedState?.optionsPageUrl.isNullOrEmpty()
}