fix build

pull/700/head iceraven-2.11.1
akliuxingyuan 8 months ago
parent 17cd0de4cc
commit 51754f194a

@ -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<String, Addon>()
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<String>,
allowCache: Boolean,
readTimeoutInSeconds: Long?,
language: String?,
): List<Addon> {
@ -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<String, Addon>.findAddonsBy(
guids: List<String>,
language: String,
): List<Addon> {
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<Addon> {
val addonsJson = getJSONArray("results")
return (0 until addonsJson.length()).map { index ->

@ -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.
*/

@ -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

@ -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(),
)
}
}
}
Loading…
Cancel
Save