diff --git a/app/build.gradle b/app/build.gradle index 820cb941c3d8c2a89045b58e787e0abb572d05da..90d0f8717ec184f8d7d9ff9399f43e77f51e3c23 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -153,7 +153,7 @@ dependencies { api files('libs/splitinstall-lib.jar') implementation 'foundation.e.lib:telemetry:0.0.11-alpha' - implementation "foundation.e:gplayapi:3.2.10-1" + implementation "foundation.e:gplayapi:3.2.10-2" implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.fragment:fragment-ktx:1.5.6' diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml index 59db6f3315a6c9afa962f295b4d6f0dddf99aa87..19dfb1935430d6fc1e7148dbe74bd0b24c07a85d 100644 --- a/app/detekt-baseline.xml +++ b/app/detekt-baseline.xml @@ -30,7 +30,7 @@ InstanceOfCheckForException:GPlayHttpClient.kt$GPlayHttpClient$e is SocketTimeoutException InvalidPackageDeclaration:Trackers.kt$package foundation.e.apps.data.exodus LargeClass:ApplicationFragment.kt$ApplicationFragment : TimeoutFragment - LongParameterList:ApplicationDialogFragment.kt$ApplicationDialogFragment$( drawable: Int = -1, title: String, message: String, positiveButtonText: String = "", positiveButtonAction: (() -> Unit)? = null, cancelButtonText: String = "", cancelButtonAction: (() -> Unit)? = null, cancelable: Boolean = true, onDismissListener: (() -> Unit)? = null, ) + LongParameterList:ApplicationDialogFragment.kt$ApplicationDialogFragment$( title: String, message: String, @DrawableRes drawableResId: Int = -1, drawable: Drawable? = null, positiveButtonText: String = "", positiveButtonAction: (() -> Unit)? = null, cancelButtonText: String = "", cancelButtonAction: (() -> Unit)? = null, cancelable: Boolean = true, onDismissListener: (() -> Unit)? = null, ) LongParameterList:ApplicationListRVAdapter.kt$ApplicationListRVAdapter$( private val applicationInstaller: ApplicationInstaller, private val privacyInfoViewModel: PrivacyInfoViewModel, private val appInfoFetchViewModel: AppInfoFetchViewModel, private val mainActivityViewModel: MainActivityViewModel, private val currentDestinationId: Int, private var lifecycleOwner: LifecycleOwner?, private var paidAppHandler: ((Application) -> Unit)? = null ) LongParameterList:ApplicationViewModel.kt$ApplicationViewModel$( id: String, packageName: String, origin: Origin, isFdroidLink: Boolean, authObjectList: List<AuthObject>, retryBlock: (failedObjects: List<AuthObject>) -> Boolean, ) LongParameterList:CleanApkRetrofit.kt$CleanApkRetrofit$( @Query("keyword") keyword: String, @Query("source") source: String = APP_SOURCE_FOSS, @Query("type") type: String = APP_TYPE_ANY, @Query("nres") nres: Int = 20, @Query("page") page: Int = 1, @Query("by") by: String? = null, ) diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index 7b2b1217cad46ddb26e61bc8fb7a71899a33cfb2..dfc6b776b2bd9a6574db9e88f546b25d9d5bfb87 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -413,7 +413,7 @@ class MainActivity : AppCompatActivity() { ApplicationDialogFragment( title = getString(R.string.account_unavailable), message = getString(R.string.too_many_requests_desc), - drawable = R.drawable.ic_warning, + drawableResId = R.drawable.ic_warning, positiveButtonText = getString(R.string.refresh_session), positiveButtonAction = { refreshSession() diff --git a/app/src/main/java/foundation/e/apps/data/application/data/Application.kt b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt index ffc19d94c345ea9feab2075a17e53e8d0de97707..dd32f1a5d7cebbef84d56f431118a9be4fefd2a4 100644 --- a/app/src/main/java/foundation/e/apps/data/application/data/Application.kt +++ b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt @@ -21,6 +21,7 @@ package foundation.e.apps.data.application.data import android.content.Context import android.net.Uri import com.aurora.gplayapi.Constants.Restriction +import com.aurora.gplayapi.data.models.ContentRating import com.google.gson.annotations.SerializedName import foundation.e.apps.R import foundation.e.apps.data.enums.FilterLevel @@ -100,7 +101,8 @@ data class Application( var filterLevel: FilterLevel = FilterLevel.UNKNOWN, var isGplayReplaced: Boolean = false, @SerializedName(value = "on_fdroid") - val isFDroidApp: Boolean = false + val isFDroidApp: Boolean = false, + val contentRating: ContentRating = ContentRating() ) { fun updateType() { this.type = if (this.is_pwa) PWA else NATIVE diff --git a/app/src/main/java/foundation/e/apps/data/application/utils/GplayApiExtensions.kt b/app/src/main/java/foundation/e/apps/data/application/utils/GplayApiExtensions.kt index be26c5669b9ad838ef0353104bd290fb94fb07e2..a9ffd87cfaa624a3492ae619e20fab8d3d06c3c8 100644 --- a/app/src/main/java/foundation/e/apps/data/application/utils/GplayApiExtensions.kt +++ b/app/src/main/java/foundation/e/apps/data/application/utils/GplayApiExtensions.kt @@ -1,6 +1,5 @@ /* - * Copyright MURENA SAS 2023 - * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2024 MURENA SAS * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,6 +13,7 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . + * */ package foundation.e.apps.data.application.utils @@ -58,6 +58,7 @@ fun App.toApplication(context: Context): Application { isFree = this.isFree, price = this.price, restriction = this.restriction, + contentRating = this.contentRating ) return app } diff --git a/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt b/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt index a38538b11b35c2e3f4c874f4109e2e0756ec542f..d71d6def54bc50c53f44a07174826322ea67a2cb 100644 --- a/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt @@ -42,7 +42,11 @@ import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager +import coil.ImageLoader import coil.load +import coil.request.ImageRequest +import coil.request.SuccessResult +import com.aurora.gplayapi.data.models.ContentRating import com.google.android.material.button.MaterialButton import com.google.android.material.snackbar.Snackbar import com.google.android.material.textview.MaterialTextView @@ -72,9 +76,11 @@ import foundation.e.apps.ui.application.ShareButtonVisibilityState.Visible import foundation.e.apps.ui.application.model.ApplicationScreenshotsRVAdapter import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment import foundation.e.apps.ui.parentFragment.TimeoutFragment +import foundation.e.apps.utils.isValid import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber import java.util.Locale import javax.inject.Inject @@ -226,7 +232,12 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { observeDownloadStatus(binding.root) stopLoadingUI() + collectState() + } + + private fun collectState() { collectShareVisibilityState() + collectAppContentRatingState() } private fun showWarningMessage(it: Application) { @@ -265,9 +276,9 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { binding.privacyInclude.apply { appPermissions.setOnClickListener { _ -> ApplicationDialogFragment( - R.drawable.ic_perm, - getString(R.string.permissions), - getPermissionListString() + drawableResId = R.drawable.ic_perm, + title = getString(R.string.permissions), + message = getPermissionListString() ).show(childFragmentManager, TAG) } appTrackers.setOnClickListener { @@ -276,9 +287,9 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { buildTrackersString(fusedApp) ApplicationDialogFragment( - R.drawable.ic_tracker, - getString(R.string.trackers_title), - trackers + drawableResId = R.drawable.ic_tracker, + title = getString(R.string.trackers_title), + message = trackers ).show(childFragmentManager, TAG) } } @@ -335,15 +346,18 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { ) appRating.setCompoundDrawablesWithIntrinsicBounds( - null, null, getRatingDrawable(rating), null + ContextCompat.getDrawable(requireContext(), R.drawable.ic_star_blank), + null, + getRatingDrawable(rating), + null ) appRating.compoundDrawablePadding = 15 } appRatingLayout.setOnClickListener { ApplicationDialogFragment( - R.drawable.ic_star, - getString(R.string.rating), - getString(R.string.rating_description) + drawableResId = R.drawable.ic_star_blank, + title = getString(R.string.rating), + message = getString(R.string.rating_description) ).show(childFragmentManager, TAG) } @@ -360,15 +374,15 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { private fun showRequestExodusReportDialog() { ApplicationDialogFragment( - R.drawable.ic_lock, - getString(R.string.request_exodus_report), - getRequestExodusReportDialogDetailsText(), - getString(R.string.ok), - { + drawableResId = R.drawable.ic_lock, + title = getString(R.string.request_exodus_report), + message = getRequestExodusReportDialogDetailsText(), + positiveButtonText = getString(R.string.ok), + positiveButtonAction = { shouldReloadPrivacyInfo = true openRequestExodusReportUrl() }, - getString(R.string.cancel) + cancelButtonText = getString(R.string.cancel) ).show(childFragmentManager, TAG) } @@ -387,9 +401,9 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { private fun showPrivacyScoreCalculationLoginDialog() { ApplicationDialogFragment( - R.drawable.ic_lock, - getString(R.string.privacy_score), - getString( + drawableResId = R.drawable.ic_lock, + title = getString(R.string.privacy_score), + message = getString( R.string.privacy_description, PRIVACY_SCORE_SOURCE_CODE_URL, generateExodusUrl(), @@ -477,6 +491,69 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { } } + private fun collectAppContentRatingState() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + applicationViewModel.appContentRating.collectLatest(::updateContentRatingUi) + } + } + } + + private fun updateContentRatingUi(contentRating: ContentRating) { + fun loadContentRating(contentRating: ContentRating) { + lifecycleScope.launch { + val drawable = loadContentRatingDrawable(contentRating.artwork.url) + displayRating(contentRating, drawable) + } + } + + fun hideContentRating() { + binding.ratingsInclude.appContentRatingLayout.visibility = View.GONE + } + + if (contentRating.isValid()) { + loadContentRating(contentRating) + } else { + hideContentRating() + } + } + + private fun displayRating(contentRating: ContentRating, drawable: Drawable?) { + binding.ratingsInclude.apply { + appContentRatingTitle.text = contentRating.title + + if (drawable != null) { + appContentRatingProgress.visibility = View.GONE + appContentRatingIcon.setImageDrawable(drawable) + } + + appContentRatingLayout.apply { + visibility = View.VISIBLE + + setOnClickListener { + openContentRatingDialog( + contentRating.title, + drawable, + contentRating.description + ) + } + } + } + } + + private suspend fun loadContentRatingDrawable(url: String): Drawable? { + return withContext(Dispatchers.IO) { + val imageRequest = ImageRequest.Builder(requireContext()).data(url).build() + val result = ImageLoader.invoke(requireContext()).execute(imageRequest) + if (result is SuccessResult) result.drawable else null + } + } + + private fun openContentRatingDialog(title: String, iconUrl: Drawable?, recommendation: String) { + ApplicationDialogFragment(drawable = iconUrl, title = title, message = recommendation) + .show(childFragmentManager, TAG) + } + override fun loadData(authObjectList: List) { if (isDetailsLoaded) return /* Show the loading bar. */ @@ -831,7 +908,8 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { val progressPercentage = ((progressResult.second / progressResult.first.toDouble()) * 100f).toInt() binding.downloadInclude.appInstallPB.progress = progressPercentage - binding.downloadInclude.percentage.text = String.format("%d%%", progressPercentage) + binding.downloadInclude.percentage.text = + String.format(Locale.getDefault(), "%d%%", progressPercentage) binding.downloadInclude.downloadedSize.text = downloadedSize } @@ -890,7 +968,10 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { ) appPrivacyScore.setCompoundDrawablesRelativeWithIntrinsicBounds( - null, null, getPrivacyDrawable(privacyScore.toString()), null + ContextCompat.getDrawable(requireContext(), R.drawable.ic_lock_blank), + null, + getPrivacyDrawable(privacyScore.toString()), + null ) appPrivacyScore.compoundDrawablePadding = 15 } diff --git a/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt b/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt index 2148be2f55bdbe16c7201fcf81b84bb3f2442790..8d1815588b215f8ff99a433f278ccda6b24f4890 100644 --- a/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt @@ -21,6 +21,7 @@ package foundation.e.apps.ui.application import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.data.models.ContentRating import com.aurora.gplayapi.exceptions.ApiException import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.R @@ -43,6 +44,7 @@ import foundation.e.apps.ui.parentFragment.LoadingViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -62,6 +64,9 @@ class ApplicationViewModel @Inject constructor( private val _shareButtonVisibilityState = MutableStateFlow(Hidden) val shareButtonVisibilityState = _shareButtonVisibilityState.asStateFlow() + private val _appContentRating = MutableStateFlow(ContentRating()) + val appContentRating = _appContentRating.asStateFlow() + fun loadData( id: String, packageName: String, @@ -114,6 +119,7 @@ class ApplicationViewModel @Inject constructor( application.postValue(appData) updateShareVisibilityState(appData.first.shareUri.toString()) + updateAppContentRatingState(appData.first.contentRating) val status = appData.second @@ -140,6 +146,10 @@ class ApplicationViewModel @Inject constructor( } } + private fun updateAppContentRatingState(value: ContentRating) { + _appContentRating.update { value } + } + private fun updateShareVisibilityState(shareUri: String) { val isValidUri = shareUri.isNotBlank() _shareButtonVisibilityState.value = if (isValidUri) Visible else Hidden diff --git a/app/src/main/java/foundation/e/apps/ui/application/subFrags/ApplicationDialogFragment.kt b/app/src/main/java/foundation/e/apps/ui/application/subFrags/ApplicationDialogFragment.kt index 7237822701ab6063183e6c98f60be13cd1f26dad..597c437e5394e9234139a9fd73f6bfbe77760610 100644 --- a/app/src/main/java/foundation/e/apps/ui/application/subFrags/ApplicationDialogFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/application/subFrags/ApplicationDialogFragment.kt @@ -1,6 +1,5 @@ /* - * Apps Quickly and easily install Android apps onto your device! - * Copyright (C) 2021 E FOUNDATION + * Copyright (C) 2021-2024 MURENA SAS * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,12 +13,14 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . + * */ package foundation.e.apps.ui.application.subFrags import android.app.Dialog import android.content.DialogInterface +import android.graphics.drawable.Drawable import android.os.Bundle import android.text.Html import android.text.SpannableString @@ -27,6 +28,7 @@ import android.text.TextPaint import android.text.method.LinkMovementMethod import android.text.style.URLSpan import android.widget.TextView +import androidx.annotation.DrawableRes import androidx.fragment.app.DialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint @@ -35,9 +37,10 @@ import foundation.e.apps.R @AndroidEntryPoint class ApplicationDialogFragment() : DialogFragment() { - private var drawable: Int = -1 private var title: String? = null private var message: String? = null + @DrawableRes private var drawableResId: Int = -1 + private var drawable: Drawable? = null private var positiveButtonText: String? = null private var positiveButtonAction: (() -> Unit)? = null private var cancelButtonText: String? = null @@ -46,9 +49,10 @@ class ApplicationDialogFragment() : DialogFragment() { private var onDismissListener: (() -> Unit)? = null constructor( - drawable: Int = -1, title: String, message: String, + @DrawableRes drawableResId: Int = -1, + drawable: Drawable? = null, positiveButtonText: String = "", positiveButtonAction: (() -> Unit)? = null, cancelButtonText: String = "", @@ -56,9 +60,10 @@ class ApplicationDialogFragment() : DialogFragment() { cancelable: Boolean = true, onDismissListener: (() -> Unit)? = null, ) : this() { - this.drawable = drawable this.title = title this.message = message + this.drawableResId = drawableResId + this.drawable = drawable this.positiveButtonText = positiveButtonText this.positiveButtonAction = positiveButtonAction this.cancelButtonText = cancelButtonText @@ -84,9 +89,14 @@ class ApplicationDialogFragment() : DialogFragment() { this.dismiss() } } - if (drawable != -1) { + if (drawableResId != -1) { + materialAlertDialogBuilder.setIcon(drawableResId) + } + + if (drawableResId == -1 && drawable != null) { materialAlertDialogBuilder.setIcon(drawable) } + return materialAlertDialogBuilder.create() } diff --git a/app/src/main/java/foundation/e/apps/utils/Extensions.kt b/app/src/main/java/foundation/e/apps/utils/Extensions.kt index aa763f6a816a0d2f6d8d73b135c7e6a1528c247d..07f652e9cac50eaa129638380a6fb6e88dc4852d 100644 --- a/app/src/main/java/foundation/e/apps/utils/Extensions.kt +++ b/app/src/main/java/foundation/e/apps/utils/Extensions.kt @@ -1,9 +1,28 @@ +/* + * Copyright (C) 2024 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + package foundation.e.apps.utils import android.content.Context import android.net.ConnectivityManager import android.net.NetworkCapabilities import androidx.appcompat.app.AlertDialog +import com.aurora.gplayapi.data.models.ContentRating import foundation.e.apps.R import java.text.SimpleDateFormat import java.util.Date @@ -42,3 +61,5 @@ fun Context.isNetworkAvailable(): Boolean { return false } + +fun ContentRating.isValid() = title.isNotBlank() && artwork.url.isNotBlank() diff --git a/app/src/main/res/drawable/ic_lock_blank.xml b/app/src/main/res/drawable/ic_lock_blank.xml new file mode 100644 index 0000000000000000000000000000000000000000..8d37663af8d2b228649b0e796855e51f835b83ad --- /dev/null +++ b/app/src/main/res/drawable/ic_lock_blank.xml @@ -0,0 +1,28 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_rating_privacy_circle.xml b/app/src/main/res/drawable/ic_rating_privacy_circle.xml index dd96d8abea9178fa800fab510d9f18ec5cad3007..188591202f1fb68d27a1da1d56b75d10f65c3af1 100644 --- a/app/src/main/res/drawable/ic_rating_privacy_circle.xml +++ b/app/src/main/res/drawable/ic_rating_privacy_circle.xml @@ -1,6 +1,5 @@ diff --git a/app/src/main/res/drawable/ic_star_blank.xml b/app/src/main/res/drawable/ic_star_blank.xml new file mode 100644 index 0000000000000000000000000000000000000000..21444f178aa18eecc337b27489698ef627d70cb6 --- /dev/null +++ b/app/src/main/res/drawable/ic_star_blank.xml @@ -0,0 +1,29 @@ + + + + + diff --git a/app/src/main/res/layout/fragment_application_ratings.xml b/app/src/main/res/layout/fragment_application_ratings.xml index b211c8a308e9b57a4f5735e21328247890a72cba..dd02f2ba61621d7cebe869f2c50b153525d33b3e 100644 --- a/app/src/main/res/layout/fragment_application_ratings.xml +++ b/app/src/main/res/layout/fragment_application_ratings.xml @@ -1,6 +1,5 @@ @@ -43,6 +44,7 @@ android:id="@+id/appRatingLayout" android:layout_width="0dp" android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" android:layout_weight="1" android:background="?android:attr/selectableItemBackground" android:clickable="true" @@ -56,23 +58,23 @@ android:layout_height="wrap_content" android:text="@string/not_available" android:textColor="?android:textColorPrimary" - android:textSize="25sp" /> + android:textSize="24sp" + tools:drawableStart="@drawable/ic_star_blank" /> + android:textSize="14sp" /> + app:layout_constraintTop_toTopOf="@+id/appPrivacyScore" + tools:visibility="gone" /> + app:layout_constraintTop_toTopOf="parent" + tools:drawableStart="@drawable/ic_lock_blank" + tools:text="3/10" + tools:visibility="visible" /> + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_application_title.xml b/app/src/main/res/layout/fragment_application_title.xml index a94b2af8c038a37ec2c59c19f4bf8df06d91db66..96ec06c1f3a50de7f8ac28f892ff122d7158f22d 100644 --- a/app/src/main/res/layout/fragment_application_title.xml +++ b/app/src/main/res/layout/fragment_application_title.xml @@ -1,6 +1,5 @@ @@ -71,6 +71,7 @@ android:paddingEnd="2dp" android:textColor="@color/app_info_text_color_grey" android:textSize="16sp" /> + @@ -87,6 +88,7 @@ android:id="@+id/sourceTag" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginEnd="10dp" android:background="@drawable/bg_tag_rounded" android:paddingStart="10dp" android:paddingTop="4dp" @@ -94,7 +96,6 @@ android:paddingBottom="4dp" android:textAllCaps="false" android:textColor="#626262" - android:layout_marginEnd="10dp" android:textSize="14sp" android:visibility="gone" tools:text="Open Source" /> @@ -103,6 +104,7 @@ android:id="@+id/categoryTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginEnd="20dp" android:background="@drawable/bg_tag_rounded" android:paddingStart="10dp" android:paddingTop="4dp" @@ -110,9 +112,8 @@ android:paddingBottom="4dp" android:textAllCaps="false" android:textColor="#626262" - android:textSize="14sp" - android:layout_marginEnd="20dp" android:textIsSelectable="false" + android:textSize="14sp" tools:text="Racing" />