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/AddonsManagementFragment.kt

346 lines
14 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.content.Context
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import android.view.inputmethod.EditorInfo
import androidx.appcompat.widget.SearchView
import androidx.core.content.res.ResourcesCompat
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.android.synthetic.main.fragment_add_ons_management.addonProgressOverlay
import kotlinx.android.synthetic.main.fragment_add_ons_management.view.add_ons_empty_message
import kotlinx.android.synthetic.main.fragment_add_ons_management.view.add_ons_list
import kotlinx.android.synthetic.main.fragment_add_ons_management.view.add_ons_progress_bar
import kotlinx.android.synthetic.main.overlay_add_on_progress.view.add_ons_overlay_text
import kotlinx.android.synthetic.main.overlay_add_on_progress.view.cancel_button
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.AddonManagerException
import mozilla.components.feature.addons.ui.PermissionsDialogFragment
import mozilla.components.feature.addons.ui.translatedName
import io.github.forkmaintainers.iceraven.components.PagedAddonInstallationDialogFragment
import io.github.forkmaintainers.iceraven.components.PagedAddonsManagerAdapter
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.theme.ThemeManager
import java.util.Locale
import java.util.concurrent.CancellationException
/**
* Fragment use for managing add-ons.
*/
@Suppress("LargeClass", "TooManyFunctions")
class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management) {
/**
* Whether or not an add-on installation is in progress.
*/
private var isInstallationInProgress = false
private var adapter: PagedAddonsManagerAdapter? = null
private var addons: List<Addon>? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
setHasOptionsMenu(true)
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
bindRecyclerView(view)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.addons_menu, menu)
val searchItem = menu.findItem(R.id.search)
val searchView: SearchView = searchItem.actionView as SearchView
searchView.imeOptions = EditorInfo.IME_ACTION_DONE
searchView.queryHint = getString(R.string.addons_search_hint)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
return searchAddons(query.trim())
}
override fun onQueryTextChange(newText: String): Boolean {
return searchAddons(newText.trim())
}
})
}
private fun searchAddons(addonNameSubStr: String): Boolean {
if (adapter == null) {
return false
}
val searchedAddons = arrayListOf<Addon>()
addons?.forEach { addon ->
val names = addon.translatableName
names["en-US"]?.let { name ->
if (name.toLowerCase(Locale.ENGLISH).contains(addonNameSubStr.toLowerCase(Locale.ENGLISH))) {
searchedAddons.add(addon)
}
}
}
updateUI(searchedAddons)
return true
}
private fun updateUI(searchedAddons: List<Addon>) {
adapter?.updateAddons(searchedAddons)
if (searchedAddons.isEmpty()) {
view?.let { view ->
view.add_ons_empty_message.visibility = View.VISIBLE
view.add_ons_list.visibility = View.GONE
}
} else {
view?.let { view ->
view.add_ons_empty_message.visibility = View.GONE
view.add_ons_list.visibility = View.VISIBLE
}
}
}
override fun onResume() {
super.onResume()
showToolbar(getString(R.string.preferences_addons))
}
override fun onStart() {
super.onStart()
findPreviousDialogFragment()?.let { dialog ->
dialog.onPositiveButtonClicked = onPositiveButtonClicked
}
}
override fun onDestroyView() {
super.onDestroyView()
// letting go of the resources to avoid memory leak.
adapter = null
}
private fun bindRecyclerView(view: View) {
val managementView = AddonsManagementView(
navController = findNavController(),
showPermissionDialog = ::showPermissionDialog
)
val recyclerView = view.add_ons_list
recyclerView.layoutManager = LinearLayoutManager(requireContext())
val shouldRefresh = adapter != null
lifecycleScope.launch(IO) {
try {
addons = requireContext().components.addonManager.getAddons()
lifecycleScope.launch(Dispatchers.Main) {
runIfFragmentIsAttached {
if (!shouldRefresh) {
adapter = PagedAddonsManagerAdapter(
requireContext().components.addonCollectionProvider,
managementView,
addons!!,
style = createAddonStyle(requireContext())
)
}
isInstallationInProgress = false
view.add_ons_progress_bar.isVisible = false
view.add_ons_empty_message.isVisible = false
recyclerView.adapter = adapter
if (shouldRefresh) {
adapter?.updateAddons(addons!!)
}
}
}
} catch (e: AddonManagerException) {
lifecycleScope.launch(Dispatchers.Main) {
runIfFragmentIsAttached {
showSnackBar(
view,
getString(R.string.mozac_feature_addons_failed_to_query_add_ons)
)
isInstallationInProgress = false
view.add_ons_progress_bar.isVisible = false
view.add_ons_empty_message.isVisible = true
}
}
}
}
}
private fun createAddonStyle(context: Context): PagedAddonsManagerAdapter.Style {
return PagedAddonsManagerAdapter.Style(
sectionsTextColor = ThemeManager.resolveAttribute(R.attr.primaryText, context),
addonNameTextColor = ThemeManager.resolveAttribute(R.attr.primaryText, context),
addonSummaryTextColor = ThemeManager.resolveAttribute(R.attr.secondaryText, context),
sectionsTypeFace = ResourcesCompat.getFont(context, R.font.metropolis_semibold),
addonAllowPrivateBrowsingLabelDrawableRes = R.drawable.ic_add_on_private_browsing_label
)
}
private fun findPreviousDialogFragment(): PermissionsDialogFragment? {
return parentFragmentManager.findFragmentByTag(PERMISSIONS_DIALOG_FRAGMENT_TAG) as? PermissionsDialogFragment
}
private fun hasExistingPermissionDialogFragment(): Boolean {
return findPreviousDialogFragment() != null
}
private fun hasExistingAddonInstallationDialogFragment(): Boolean {
return parentFragmentManager.findFragmentByTag(INSTALLATION_DIALOG_FRAGMENT_TAG)
as? PagedAddonInstallationDialogFragment != null
}
private fun showPermissionDialog(addon: Addon) {
if (!isInstallationInProgress && !hasExistingPermissionDialogFragment()) {
val dialog = PermissionsDialogFragment.newInstance(
addon = addon,
promptsStyling = PermissionsDialogFragment.PromptsStyling(
gravity = Gravity.BOTTOM,
shouldWidthMatchParent = true,
positiveButtonBackgroundColor = ThemeManager.resolveAttribute(
R.attr.accent,
requireContext()
),
positiveButtonTextColor = ThemeManager.resolveAttribute(
R.attr.contrastText,
requireContext()
),
positiveButtonRadius = (resources.getDimensionPixelSize(R.dimen.tab_corner_radius)).toFloat()
),
onPositiveButtonClicked = onPositiveButtonClicked
)
dialog.show(parentFragmentManager, PERMISSIONS_DIALOG_FRAGMENT_TAG)
}
}
private fun showInstallationDialog(addon: Addon) {
if (!isInstallationInProgress && !hasExistingAddonInstallationDialogFragment()) {
requireComponents.analytics.metrics.track(Event.AddonInstalled(addon.id))
val addonCollectionProvider = requireContext().components.addonCollectionProvider
val dialog = PagedAddonInstallationDialogFragment.newInstance(
addon = addon,
addonCollectionProvider = addonCollectionProvider,
promptsStyling = PagedAddonInstallationDialogFragment.PromptsStyling(
gravity = Gravity.BOTTOM,
shouldWidthMatchParent = true,
confirmButtonBackgroundColor = ThemeManager.resolveAttribute(
R.attr.accent,
requireContext()
),
confirmButtonTextColor = ThemeManager.resolveAttribute(
R.attr.contrastText,
requireContext()
),
confirmButtonRadius = (resources.getDimensionPixelSize(R.dimen.tab_corner_radius)).toFloat()
),
onConfirmButtonClicked = { _, allowInPrivateBrowsing ->
if (allowInPrivateBrowsing) {
requireContext().components.addonManager.setAddonAllowedInPrivateBrowsing(
addon,
allowInPrivateBrowsing,
onSuccess = {
runIfFragmentIsAttached {
adapter?.updateAddon(it)
}
}
)
}
}
)
dialog.show(parentFragmentManager, INSTALLATION_DIALOG_FRAGMENT_TAG)
}
}
private val onPositiveButtonClicked: ((Addon) -> Unit) = { addon ->
addonProgressOverlay?.visibility = View.VISIBLE
if (requireContext().settings().accessibilityServicesEnabled) {
announceForAccessibility(addonProgressOverlay.add_ons_overlay_text.text)
}
isInstallationInProgress = true
val installOperation = requireContext().components.addonManager.installAddon(
addon,
onSuccess = {
runIfFragmentIsAttached {
isInstallationInProgress = false
adapter?.updateAddon(it)
addonProgressOverlay?.visibility = View.GONE
showInstallationDialog(it)
}
},
onError = { _, e ->
this@AddonsManagementFragment.view?.let { view ->
// No need to display an error message if installation was cancelled by the user.
if (e !is CancellationException) {
val rootView = activity?.getRootView() ?: view
showSnackBar(
rootView,
getString(
R.string.mozac_feature_addons_failed_to_install,
addon.translatedName
)
)
}
addonProgressOverlay?.visibility = View.GONE
isInstallationInProgress = false
}
}
)
addonProgressOverlay.cancel_button.setOnClickListener {
lifecycleScope.launch(Dispatchers.Main) {
// Hide the installation progress overlay once cancellation is successful.
if (installOperation.cancel().await()) {
addonProgressOverlay.visibility = View.GONE
}
}
}
}
private fun announceForAccessibility(announcementText: CharSequence) {
val event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_ANNOUNCEMENT
)
addonProgressOverlay.onInitializeAccessibilityEvent(event)
event.text.add(announcementText)
event.contentDescription = null
addonProgressOverlay.parent.requestSendAccessibilityEvent(addonProgressOverlay, event)
}
companion object {
private const val PERMISSIONS_DIALOG_FRAGMENT_TAG = "ADDONS_PERMISSIONS_DIALOG_FRAGMENT"
private const val INSTALLATION_DIALOG_FRAGMENT_TAG = "ADDONS_INSTALLATION_DIALOG_FRAGMENT"
}
}