LeOSium-WF/app/src/main/java/net/waterfox/android/addons/AddonsManagementFragment.kt

244 lines
9.8 KiB
Kotlin
Raw Normal View History

2024-12-12 08:45:34 +01:00
/* 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 net.waterfox.android.addons
import android.content.Context
import android.graphics.Typeface
import android.graphics.fonts.FontStyle.FONT_WEIGHT_MEDIUM
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.accessibility.AccessibilityEvent
import androidx.annotation.VisibleForTesting
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import mozilla.components.concept.engine.webextension.InstallationMethod
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.AddonManager
import mozilla.components.feature.addons.AddonManagerException
import mozilla.components.feature.addons.ui.AddonsManagerAdapter
import mozilla.components.feature.addons.ui.AddonsManagerAdapterDelegate
import net.waterfox.android.BrowserDirection
import net.waterfox.android.BuildConfig
import net.waterfox.android.HomeActivity
import net.waterfox.android.R
import net.waterfox.android.components.WaterfoxSnackbar
import net.waterfox.android.databinding.FragmentAddOnsManagementBinding
import net.waterfox.android.ext.components
import net.waterfox.android.ext.requireComponents
import net.waterfox.android.ext.runIfFragmentIsAttached
import net.waterfox.android.ext.settings
import net.waterfox.android.ext.showToolbar
import net.waterfox.android.settings.SupportUtils
import net.waterfox.android.theme.ThemeManager
/**
* Fragment use for managing add-ons.
*/
@Suppress("TooManyFunctions", "LargeClass")
class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management) {
private var binding: FragmentAddOnsManagementBinding? = null
private var addons: List<Addon> = emptyList()
private var adapter: AddonsManagerAdapter? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentAddOnsManagementBinding.bind(view)
bindRecyclerView()
(activity as HomeActivity).webExtensionPromptFeature.onAddonChanged = {
runIfFragmentIsAttached {
adapter?.updateAddon(it)
}
}
}
override fun onResume() {
super.onResume()
showToolbar(getString(R.string.preferences_addons))
}
override fun onDestroyView() {
super.onDestroyView()
// letting go of the resources to avoid memory leak.
adapter = null
binding = null
(activity as HomeActivity).webExtensionPromptFeature.onAddonChanged = {}
}
private fun bindRecyclerView() {
val managementView = AddonsManagementView(
navController = findNavController(),
onInstallButtonClicked = ::installAddon,
onMoreAddonsButtonClicked = ::openAMO,
onLearnMoreClicked = ::openLearnMoreLink,
)
val recyclerView = binding?.addOnsList
recyclerView?.layoutManager = LinearLayoutManager(requireContext())
val shouldRefresh = adapter != null
lifecycleScope.launch(IO) {
try {
addons = requireContext().components.addonManager.getAddons()
// Add-ons that should be excluded in Mozilla Online builds
val excludedAddonIDs = emptyList<String>()
lifecycleScope.launch(Dispatchers.Main) {
runIfFragmentIsAttached {
if (!shouldRefresh) {
adapter = AddonsManagerAdapter(
addonsManagerDelegate = managementView,
addons = addons,
style = createAddonStyle(requireContext()),
excludedAddonIDs = excludedAddonIDs,
store = requireComponents.core.store,
)
}
binding?.addOnsProgressBar?.isVisible = false
binding?.addOnsEmptyMessage?.isVisible = false
recyclerView?.adapter = adapter
if (shouldRefresh) {
adapter?.updateAddons(addons)
}
}
}
} catch (e: AddonManagerException) {
lifecycleScope.launch(Dispatchers.Main) {
runIfFragmentIsAttached {
binding?.let {
showSnackBar(
it.root,
2025-06-30 10:56:29 +02:00
getString(mozilla.components.feature.addons.R.string.mozac_feature_addons_failed_to_query_extensions),
2024-12-12 08:45:34 +01:00
)
}
binding?.addOnsProgressBar?.isVisible = false
binding?.addOnsEmptyMessage?.isVisible = true
}
}
}
}
}
@VisibleForTesting
internal fun showErrorSnackBar(text: String, anchorView: View? = this.view) {
runIfFragmentIsAttached {
anchorView?.let {
showSnackBar(it, text, WaterfoxSnackbar.LENGTH_LONG)
}
}
}
private fun createAddonStyle(context: Context): AddonsManagerAdapter.Style {
val sectionsTypeFace = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Typeface.create(Typeface.DEFAULT, FONT_WEIGHT_MEDIUM, false)
} else {
Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
}
return AddonsManagerAdapter.Style(
sectionsTextColor = ThemeManager.resolveAttribute(R.attr.textPrimary, context),
addonNameTextColor = ThemeManager.resolveAttribute(R.attr.textPrimary, context),
addonSummaryTextColor = ThemeManager.resolveAttribute(R.attr.textSecondary, context),
sectionsTypeFace = sectionsTypeFace,
addonAllowPrivateBrowsingLabelDrawableRes = R.drawable.ic_add_on_private_browsing_label,
)
}
@VisibleForTesting
internal fun provideAddonManger(): AddonManager {
return requireContext().components.addonManager
}
internal fun provideAccessibilityServicesEnabled(): Boolean {
return requireContext().settings().accessibilityServicesEnabled
}
internal fun installAddon(addon: Addon) {
binding?.addonProgressOverlay?.overlayCardView?.visibility = View.VISIBLE
if (provideAccessibilityServicesEnabled()) {
binding?.let { announceForAccessibility(it.addonProgressOverlay.addOnsOverlayText.text) }
}
val installOperation = provideAddonManger().installAddon(
url = addon.downloadUrl,
installationMethod = InstallationMethod.MANAGER,
onSuccess = {
runIfFragmentIsAttached {
adapter?.updateAddon(it)
binding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE
}
},
onError = { _ ->
binding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE
},
)
binding?.addonProgressOverlay?.cancelButton?.setOnClickListener {
lifecycleScope.launch(Dispatchers.Main) {
val safeBinding = binding
// Hide the installation progress overlay once cancellation is successful.
if (installOperation.cancel().await()) {
safeBinding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE
}
}
}
}
private fun announceForAccessibility(announcementText: CharSequence) {
val event = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
AccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT)
} else {
@Suppress("DEPRECATION")
AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT)
}
binding?.addonProgressOverlay?.overlayCardView?.onInitializeAccessibilityEvent(event)
event.text.add(announcementText)
event.contentDescription = null
binding?.addonProgressOverlay?.overlayCardView?.let {
it.parent?.requestSendAccessibilityEvent(
it,
event,
)
}
}
private fun openAMO() {
openLinkInNewTab(AMO_HOMEPAGE_FOR_ANDROID)
}
private fun openLearnMoreLink(link: AddonsManagerAdapterDelegate.LearnMoreLinks, addon: Addon) {
val url = when (link) {
AddonsManagerAdapterDelegate.LearnMoreLinks.BLOCKLISTED_ADDON ->
"${BuildConfig.AMO_BASE_URL}/android/blocked-addon/${addon.id}/"
AddonsManagerAdapterDelegate.LearnMoreLinks.ADDON_NOT_CORRECTLY_SIGNED ->
SupportUtils.getSumoURLForTopic(requireContext(), SupportUtils.SumoTopic.UNSIGNED_ADDONS)
}
openLinkInNewTab(url)
}
private fun openLinkInNewTab(url: String) {
(activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = url,
newTab = true,
from = BrowserDirection.FromAddonsManagementFragment,
)
}
companion object {
// This is locale-less on purpose so that the content negotiation happens on the AMO side because the current
// user language might not be supported by AMO and/or the language might not be exactly what AMO is expecting
// (e.g. `en` instead of `en-US`).
private const val AMO_HOMEPAGE_FOR_ANDROID = "${BuildConfig.AMO_BASE_URL}/android/"
}
}