From 51754f194ac65002ab3cf998d044b75213b4cb2e Mon Sep 17 00:00:00 2001 From: akliuxingyuan Date: Wed, 25 Oct 2023 21:39:18 +0800 Subject: [PATCH] fix build --- .../components/PagedAMOAddonsProvider.kt | 100 +++++++++++----- .../components/PagedAddonsManagerAdapter.kt | 36 +++++- .../fenix/addons/AddonsManagementFragment.kt | 1 - .../shopping/ui/ReviewQualityCheckContent.kt | 107 ------------------ 4 files changed, 104 insertions(+), 140 deletions(-) delete mode 100644 app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckContent.kt diff --git a/app/src/main/java/io/github/forkmaintainers/iceraven/components/PagedAMOAddonsProvider.kt b/app/src/main/java/io/github/forkmaintainers/iceraven/components/PagedAMOAddonsProvider.kt index 9a5ff26f6..7b71fdd64 100644 --- a/app/src/main/java/io/github/forkmaintainers/iceraven/components/PagedAMOAddonsProvider.kt +++ b/app/src/main/java/io/github/forkmaintainers/iceraven/components/PagedAMOAddonsProvider.kt @@ -28,10 +28,11 @@ import java.io.File import java.io.IOException import java.util.Date import java.util.Locale +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeUnit internal const val API_VERSION = "api/v4" -internal const val DEFAULT_SERVER_URL = "https://addons.mozilla.org" +internal const val DEFAULT_SERVER_URL = "https://services.addons.mozilla.org" internal const val COLLECTION_FILE_NAME_PREFIX = "%s_components_addon_collection" internal const val COLLECTION_FILE_NAME = "%s_components_addon_collection_%s.json" internal const val COLLECTION_FILE_NAME_WITH_LANGUAGE = "%s_components_addon_collection_%s_%s.json" @@ -40,22 +41,15 @@ internal const val MINUTE_IN_MS = 60 * 1000 internal const val DEFAULT_READ_TIMEOUT_IN_SECONDS = 20L /** - * Provide access to the AMO collections API. - * https://addons-server.readthedocs.io/en/latest/topics/api/collections.html - * - * Unlike the android-components version, supports multiple-page responses and - * custom collection accounts. - * - * Needs to extend AddonsProvider because AddonsManagerAdapter won't - * take just any AddonsProvider. + * Implement an add-ons provider that uses the AMO API. * * @property context A reference to the application context. * @property client A [Client] for interacting with the AMO HTTP api. * @property serverURL The url of the endpoint to interact with e.g production, staging * or testing. Defaults to [DEFAULT_SERVER_URL]. - * @property maxCacheAgeInMinutes maximum time (in minutes) the collection cache - * should remain valid before a refresh is attempted. Defaults to -1, meaning no - * cache is being used by default + * @property maxCacheAgeInMinutes maximum time (in minutes) the cached featured add-ons + * should remain valid before a refresh is attempted. Defaults to -1, meaning no cache + * is being used by default */ @Suppress("LongParameterList") class PagedAMOAddonProvider( @@ -65,6 +59,10 @@ class PagedAMOAddonProvider( private val maxCacheAgeInMinutes: Long = -1, ) : AddonsProvider { + // This map acts as an in-memory cache for the installed add-ons. + @VisibleForTesting + internal val installedAddons = ConcurrentHashMap() + private val logger = Logger("PagedAddonCollectionProvider") private val diskCacheLock = Any() @@ -102,8 +100,10 @@ class PagedAMOAddonProvider( /** * Interacts with the collections endpoint to provide a list of available * add-ons. May return a cached response, if [allowCache] is true, and the - * cache is not expired (see [maxCacheAgeInMinutes]) or fetching from - * AMO failed. + * cache is not expired (see [maxCacheAgeInMinutes]) or fetching from AMO + * failed. + * + * See: https://addons-server.readthedocs.io/en/latest/topics/api/collections.html * * @param allowCache whether or not the result may be provided * from a previously cached response, defaults to true. Note that @@ -117,7 +117,6 @@ class PagedAMOAddonProvider( * @throws IOException if the request failed, or could not be executed due to cancellation, * a connectivity problem or a timeout. */ - @Throws(IOException::class) override suspend fun getFeaturedAddons( allowCache: Boolean, @@ -127,19 +126,18 @@ class PagedAMOAddonProvider( // We want to make sure we always use useFallbackFile = false here, as it warranties // that we are trying to fetch the latest localized add-ons when the user changes // language from the previous one. - val cachedAvailableAddons = - if (allowCache && !cacheExpired(context, language, useFallbackFile = false)) { - readFromDiskCache(language, useFallbackFile = false) - } else { - null - } + val cachedFeaturedAddons = if (allowCache && !cacheExpired(context, language, useFallbackFile = false)) { + readFromDiskCache(language, useFallbackFile = false) + } else { + null + } val collectionAccount = getCollectionAccount() val collectionName = getCollectionName() - if (cachedAvailableAddons != null) { + if (cachedFeaturedAddons != null) { logger.info("Providing cached list of addons for $collectionAccount collection $collectionName") - return cachedAvailableAddons + return cachedFeaturedAddons } else { logger.info("Fetching fresh list of addons for $collectionAccount collection $collectionName") val langParam = if (!language.isNullOrEmpty()) { @@ -169,8 +167,28 @@ class PagedAMOAddonProvider( } } + /** + * Interacts with the search endpoint to provide a list of add-ons for a given list of GUIDs. + * + * See: https://addons-server.readthedocs.io/en/latest/topics/api/addons.html#search + * + * @param guids list of add-on GUIDs to retrieve. + * @param allowCache whether or not the result may be provided from a previously cached response, + * defaults to true. + * @param readTimeoutInSeconds optional timeout in seconds to use when fetching available + * add-ons from a remote endpoint. If not specified [DEFAULT_READ_TIMEOUT_IN_SECONDS] will + * be used. + * @param language indicates in which language the translatable fields should be in, if no + * matching language is found then a fallback translation is returned using the default + * language. When it is null all translations available will be returned. + * @throws IOException if the request failed, or could not be executed due to cancellation, + * a connectivity problem or a timeout. + */ + @Throws(IOException::class) + @Suppress("NestedBlockDepth") override suspend fun getAddonsByGUIDs( guids: List, + allowCache: Boolean, readTimeoutInSeconds: Long?, language: String?, ): List { @@ -179,6 +197,15 @@ class PagedAMOAddonProvider( return emptyList() } + if (allowCache && installedAddons.isNotEmpty()) { + val cachedAddons = installedAddons.findAddonsBy(guids, language ?: Locale.getDefault().language) + // We should only return the cached add-ons when all the requested + // GUIDs have been found in the cache. + if (cachedAddons.size == guids.size) { + return cachedAddons + } + } + val langParam = if (!language.isNullOrEmpty()) { "&lang=$language" } else { @@ -187,7 +214,7 @@ class PagedAMOAddonProvider( client.fetch( Request( - url = "$serverURL/${API_VERSION}/addons/search/?guid=${guids.joinToString(",")}" + langParam, + url = "$serverURL/$API_VERSION/addons/search/?guid=${guids.joinToString(",")}" + langParam, readTimeout = Pair(readTimeoutInSeconds ?: DEFAULT_READ_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS), ), ) @@ -195,7 +222,11 @@ class PagedAMOAddonProvider( if (response.isSuccess) { val responseBody = response.body.string(Charsets.UTF_8) return try { - JSONObject(responseBody).getAddonsFromSearchResults(language) + val addons = JSONObject(responseBody).getAddonsFromSearchResults(language) + addons.forEach { + installedAddons[it.id] = it + } + addons } catch (e: JSONException) { throw IOException(e) } @@ -239,11 +270,7 @@ class PagedAMOAddonProvider( throw IOException(errorMessage) } - val currentResponse = try { - JSONObject(response.body.string(Charsets.UTF_8)) - } catch (e: JSONException) { - throw IOException(e) - } + val currentResponse = JSONObject(response.body.string(Charsets.UTF_8)) if (compiledResponse == null) { compiledResponse = currentResponse } else { @@ -402,6 +429,19 @@ class PagedAMOAddonProvider( } } +internal fun Map.findAddonsBy( + guids: List, + language: String, +): List { + return if (isNotEmpty()) { + filter { + guids.contains(it.key) && it.value.translatableName.containsKey(language) + }.map { it.value } + } else { + emptyList() + } +} + internal fun JSONObject.getAddonsFromSearchResults(language: String? = null): List { val addonsJson = getJSONArray("results") return (0 until addonsJson.length()).map { index -> diff --git a/app/src/main/java/io/github/forkmaintainers/iceraven/components/PagedAddonsManagerAdapter.kt b/app/src/main/java/io/github/forkmaintainers/iceraven/components/PagedAddonsManagerAdapter.kt index 7362710e0..a92f47d26 100644 --- a/app/src/main/java/io/github/forkmaintainers/iceraven/components/PagedAddonsManagerAdapter.kt +++ b/app/src/main/java/io/github/forkmaintainers/iceraven/components/PagedAddonsManagerAdapter.kt @@ -35,6 +35,7 @@ import mozilla.components.feature.addons.R import mozilla.components.feature.addons.ui.AddonsManagerAdapterDelegate import mozilla.components.feature.addons.ui.CustomViewHolder import mozilla.components.feature.addons.ui.CustomViewHolder.AddonViewHolder +import mozilla.components.feature.addons.ui.CustomViewHolder.FooterViewHolder import mozilla.components.feature.addons.ui.CustomViewHolder.SectionViewHolder import mozilla.components.feature.addons.ui.CustomViewHolder.UnsupportedSectionViewHolder import mozilla.components.feature.addons.ui.translateName @@ -49,15 +50,16 @@ import mozilla.components.ui.icons.R as iconsR private const val VIEW_HOLDER_TYPE_SECTION = 0 private const val VIEW_HOLDER_TYPE_NOT_YET_SUPPORTED_SECTION = 1 private const val VIEW_HOLDER_TYPE_ADDON = 2 +private const val VIEW_HOLDER_TYPE_FOOTER = 3 /** * An adapter for displaying add-on items. This will display information related to the state of * an add-on such as recommended, unsupported or installed. In addition, it will perform actions * such as installing an add-on. * - * @property addonsProvider Provider of AMO collection API. + * @property addonsProvider An add-ons provider. * @property addonsManagerDelegate Delegate that will provides method for handling the add-on items. - * @param addons The list of add-on based on the AMO store. + * @param addons The list of add-ons to display. * @property style Indicates how items should look like. */ @Suppress("LargeClass") @@ -87,6 +89,7 @@ class PagedAddonsManagerAdapter( VIEW_HOLDER_TYPE_ADDON -> createAddonViewHolder(parent) VIEW_HOLDER_TYPE_SECTION -> createSectionViewHolder(parent) VIEW_HOLDER_TYPE_NOT_YET_SUPPORTED_SECTION -> createUnsupportedSectionViewHolder(parent) + VIEW_HOLDER_TYPE_FOOTER -> createFooterSectionViewHolder(parent) else -> throw IllegalArgumentException("Unrecognized viewType") } } @@ -100,6 +103,17 @@ class PagedAddonsManagerAdapter( return SectionViewHolder(view, titleView, divider) } + private fun createFooterSectionViewHolder(parent: ViewGroup): CustomViewHolder { + val context = parent.context + val inflater = LayoutInflater.from(context) + val view = inflater.inflate( + R.layout.mozac_feature_addons_footer_section_item, + parent, + false, + ) + return FooterViewHolder(view) + } + private fun createUnsupportedSectionViewHolder(parent: ViewGroup): CustomViewHolder { val context = parent.context val inflater = LayoutInflater.from(context) @@ -144,6 +158,7 @@ class PagedAddonsManagerAdapter( is Addon -> VIEW_HOLDER_TYPE_ADDON is Section -> VIEW_HOLDER_TYPE_SECTION is NotYetSupportedSection -> VIEW_HOLDER_TYPE_NOT_YET_SUPPORTED_SECTION + is FooterSection -> VIEW_HOLDER_TYPE_FOOTER else -> throw IllegalArgumentException("items[position] has unrecognized type") } } @@ -158,6 +173,7 @@ class PagedAddonsManagerAdapter( holder, item as NotYetSupportedSection, ) + is FooterViewHolder -> bindFooterButton(holder) } } @@ -196,6 +212,15 @@ class PagedAddonsManagerAdapter( } } + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun bindFooterButton( + holder: FooterViewHolder, + ) { + holder.itemView.setOnClickListener { + addonsManagerDelegate.onFindMoreAddonsButtonClicked() + } + } + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal fun bindAddon(holder: AddonViewHolder, addon: Addon) { val context = holder.itemView.context @@ -330,6 +355,10 @@ class PagedAddonsManagerAdapter( itemsWithSections.add(NotYetSupportedSection(R.string.mozac_feature_addons_unavailable_section)) } + if (addonsManagerDelegate.shouldShowFindMoreAddonsButton()) { + itemsWithSections.add(FooterSection) + } + return itemsWithSections } @@ -339,6 +368,9 @@ class PagedAddonsManagerAdapter( @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal data class NotYetSupportedSection(@StringRes val title: Int) + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal object FooterSection + /** * Allows to customize how items should look like. */ diff --git a/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt b/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt index 4cccac237..ad24c252b 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt @@ -40,7 +40,6 @@ import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.feature.addons.ui.AddonsManagerAdapter -import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.Config diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckContent.kt b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckContent.kt deleted file mode 100644 index 78663ad03..000000000 --- a/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckContent.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* 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.shopping.ui - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.unit.dp -import org.mozilla.fenix.R -import org.mozilla.fenix.compose.BottomSheetHandle -import org.mozilla.fenix.compose.annotation.LightDarkPreview -import org.mozilla.fenix.theme.FirefoxTheme - -private val bottomSheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) -private const val BOTTOM_SHEET_HANDLE_WIDTH_PERCENT = 0.1f - -/** - * Top-level UI for the Review Quality Check feature. - * - * @param onRequestDismiss Invoked when a user actions requests dismissal of the bottom sheet. - * @param modifier The modifier to be applied to the Composable. - */ -@Composable -fun ReviewQualityCheckContent( - onRequestDismiss: () -> Unit, - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier - .background( - color = FirefoxTheme.colors.layer1, - shape = bottomSheetShape, - ) - .padding(16.dp), - ) { - BottomSheetHandle( - onRequestDismiss = onRequestDismiss, - contentDescription = stringResource(R.string.browser_menu_review_quality_check_close), - modifier = Modifier - .fillMaxWidth(BOTTOM_SHEET_HANDLE_WIDTH_PERCENT) - .align(Alignment.CenterHorizontally), - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Header() - - Spacer(modifier = Modifier.height(16.dp)) - } -} - -@Composable -private fun Header() { - Row( - modifier = Modifier.semantics(mergeDescendants = true) {}, - verticalAlignment = Alignment.CenterVertically, - ) { - Image( - painter = painterResource(id = R.drawable.ic_firefox), - contentDescription = null, - modifier = Modifier.size(24.dp), - ) - - Spacer(modifier = Modifier.width(10.dp)) - - Text( - text = stringResource(R.string.review_quality_check), - color = FirefoxTheme.colors.textPrimary, - style = FirefoxTheme.typography.headline6, - ) - } -} - -@Composable -@LightDarkPreview -private fun ReviewQualityCheckContentPreview() { - FirefoxTheme { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.BottomCenter, - ) { - ReviewQualityCheckContent( - onRequestDismiss = {}, - modifier = Modifier.fillMaxWidth(), - ) - } - } -}