Show onboarding for anti-features in app details

This commit is contained in:
Torsten Grote committed 2026-04-28 08:58:41 -03:00
1 parent 28668ea80c
commit 73137f49cb
9 files changed
+71 -5

No files matched your search

@@ -23,6 +23,7 @@ constructor(@param:ApplicationContext private val context: Context) {
const val KEY_REPO_LIST = "repoList"
const val KEY_REPO_DETAILS = "repoDetails"
const val KEY_APP_ISSUE_HINT = "appIssueHint"
const val KEY_ANTI_FEATURES = "antiFeatures"
}
private val prefs = context.getSharedPreferences("onboarding", MODE_PRIVATE)
@@ -42,6 +43,9 @@ constructor(@param:ApplicationContext private val context: Context) {
private val _showAppIssueHint = Onboarding(KEY_APP_ISSUE_HINT, prefs)
val showAppIssueHint = _showAppIssueHint.flow
private val _showAntiFeaturesOnboarding = Onboarding(KEY_ANTI_FEATURES, prefs)
val showAntiFeaturesOnboarding = _showAntiFeaturesOnboarding.flow
fun onMyAppsUpdateHintSeen() {
_showMyAppsUpdatesHint.onSeen(prefs)
}
@@ -61,6 +65,10 @@ constructor(@param:ApplicationContext private val context: Context) {
fun onAppIssueHintSeen() {
_showAppIssueHint.onSeen(prefs)
}
fun onAntiFeaturesOnboardingSeen() {
_showAntiFeaturesOnboarding.onSeen(prefs)
}
}
private data class Onboarding(val key: String, private val _flow: MutableStateFlow<Boolean>) {
@@ -30,11 +30,11 @@ import org.fdroid.ui.utils.testApp
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
fun AntiFeatures(antiFeatures: List<AntiFeature>) {
fun AntiFeatures(antiFeatures: List<AntiFeature>, modifier: Modifier = Modifier) {
ElevatedCard(
colors =
CardDefaults.elevatedCardColors(containerColor = MaterialTheme.colorScheme.inverseSurface),
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
modifier = modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
) {
ExpandableSection(
icon = rememberVectorPainter(Icons.Default.WarningAmber),
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@@ -41,12 +42,14 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.LiveRegionMode
@@ -66,6 +69,12 @@ import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.core.os.LocaleListCompat
import com.viktormykhailiv.compose.hints.hintAnchor
import com.viktormykhailiv.compose.hints.rememberHint
import com.viktormykhailiv.compose.hints.rememberHintAnchorState
import com.viktormykhailiv.compose.hints.rememberHintController
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.fdroid.LocaleChooser.getBestLocale
import org.fdroid.R
import org.fdroid.install.InstallState
@@ -78,6 +87,8 @@ import org.fdroid.ui.lists.AppListType
import org.fdroid.ui.navigation.NavigationKey
import org.fdroid.ui.utils.BigLoadingIndicator
import org.fdroid.ui.utils.ExpandableSection
import org.fdroid.ui.utils.OnboardingPopupCard
import org.fdroid.ui.utils.getHintOverlayColor
import org.fdroid.ui.utils.testApp
@Composable
@@ -107,6 +118,21 @@ fun AppDetails(
showInstallError = true
}
}
// onboarding hint plumbing
val hintController = rememberHintController(overlay = getHintOverlayColor())
val hint = rememberHint {
OnboardingPopupCard(
title = stringResource(R.string.app_details_anti_features_title),
message = stringResource(R.string.app_details_anti_features_text),
modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp),
onGotIt = {
item.actions.onAntiFeaturesOnboardingSeen()
hintController.dismiss()
},
)
}
val hintAnchor = rememberHintAnchorState(hint)
val coroutineScope = rememberCoroutineScope()
val scrollState = rememberScrollState()
var size by remember { mutableStateOf(IntSize.Zero) }
Column(
@@ -217,7 +243,19 @@ fun AppDetails(
}
// Anti-features
if (!item.antiFeatures.isNullOrEmpty()) {
AntiFeatures(item.antiFeatures)
AntiFeatures(
antiFeatures = item.antiFeatures,
modifier =
Modifier.hintAnchor(state = hintAnchor, shape = RoundedCornerShape(16.dp)).onPlaced {
if (item.showAntiFeaturesOnboarding) {
coroutineScope.launch {
delay(500) // we still need a delay to not highlight the wrong place
hintController.show(hintAnchor)
item.actions.onAntiFeaturesOnboardingSeen()
}
}
},
)
}
// Screenshots
if (item.phoneScreenshots.isNotEmpty()) {
@@ -21,8 +21,8 @@ import org.fdroid.index.RELEASE_CHANNEL_BETA
import org.fdroid.index.v2.PackageVersion
import org.fdroid.install.InstallState
import org.fdroid.install.SessionInstallManager
import org.fdroid.ui.categories.CategoryItem
import org.fdroid.search.SearchHelper.removeZeroWhiteSpace
import org.fdroid.ui.categories.CategoryItem
data class AppDetailsItem(
val app: AppMetadata,
@@ -62,6 +62,7 @@ data class AppDetailsItem(
val appPrefs: AppPrefs? = null,
val whatsNew: String? = null,
val antiFeatures: List<AntiFeature>? = null,
val showAntiFeaturesOnboarding: Boolean = false,
val issue: AppIssue? = null,
val authorHasMoreThanOneApp: Boolean = false,
val proxy: ProxyConfig? = null,
@@ -82,6 +83,7 @@ data class AppDetailsItem(
suggestedVersion: AppVersion?,
possibleUpdate: AppVersion?,
appPrefs: AppPrefs?,
showAntiFeaturesOnboarding: Boolean,
issue: AppIssue?,
authorHasMoreThanOneApp: Boolean,
localeList: LocaleListCompat,
@@ -131,6 +133,7 @@ data class AppDetailsItem(
localeList = localeList,
proxy = proxy,
),
showAntiFeaturesOnboarding = showAntiFeaturesOnboarding,
issue = issue,
authorHasMoreThanOneApp = authorHasMoreThanOneApp,
proxy = proxy,
@@ -240,6 +243,7 @@ data class AppDetailsActions(
val onRepoChanged: (Long) -> Unit,
val onPreferredRepoChanged: (Long) -> Unit,
val allowBetaVersions: () -> Unit,
val onAntiFeaturesOnboardingSeen: () -> Unit,
val ignoreAllUpdates: (() -> Unit)? = null,
val ignoreThisUpdate: (() -> Unit)? = null,
val shareApk: Intent? = null,
@@ -42,6 +42,7 @@ import org.fdroid.install.AppInstallManager
import org.fdroid.install.InstallState
import org.fdroid.install.InstalledAppsCache
import org.fdroid.repo.RepoPreLoader
import org.fdroid.settings.OnboardingManager
import org.fdroid.settings.SettingsManager
import org.fdroid.updates.UpdatesManager
import org.fdroid.utils.IoDispatcher
@@ -61,6 +62,7 @@ constructor(
private val updatesManager: UpdatesManager,
private val networkMonitor: NetworkMonitor,
private val settingsManager: SettingsManager,
private val onboardingManager: OnboardingManager,
private val appInstallManager: AppInstallManager,
private val installedAppsCache: InstalledAppsCache,
) : AndroidViewModel(app) {
@@ -84,6 +86,7 @@ constructor(
viewModel = this,
packageInfoFlow = packageInfoFlow,
currentRepoIdFlow = currentRepoIdFlow,
showAntiFeaturesOnboardingFlow = onboardingManager.showAntiFeaturesOnboarding,
appsWithIssuesFlow = updatesManager.appsWithIssues,
networkStateFlow = networkMonitor.networkState,
)
@@ -227,6 +230,11 @@ constructor(
}
}
@UiThread
fun onAntiFeaturesOnboardingSeen() {
onboardingManager.onAntiFeaturesOnboardingSeen()
}
@AssistedFactory
interface Factory {
fun create(packageName: String): AppDetailsViewModel
@@ -47,6 +47,7 @@ fun DetailsPresenter(
viewModel: AppDetailsViewModel,
packageInfoFlow: StateFlow<AppInfo?>,
currentRepoIdFlow: StateFlow<Long?>,
showAntiFeaturesOnboardingFlow: StateFlow<Boolean>,
appsWithIssuesFlow: StateFlow<List<AppWithIssueItem>?>,
networkStateFlow: StateFlow<NetworkState>,
): AppDetailsItem? {
@@ -206,6 +207,7 @@ fun DetailsPresenter(
onRepoChanged = viewModel::onRepoChanged,
onPreferredRepoChanged = viewModel::onPreferredRepoChanged,
allowBetaVersions = viewModel::allowBetaUpdates,
onAntiFeaturesOnboardingSeen = viewModel::onAntiFeaturesOnboardingSeen,
ignoreAllUpdates =
if (installedVersionCode == null) {
null
@@ -265,6 +267,7 @@ fun DetailsPresenter(
suggestedVersion = suggestedVersion,
possibleUpdate = possibleUpdate,
appPrefs = appPrefs,
showAntiFeaturesOnboarding = showAntiFeaturesOnboardingFlow.collectAsState().value,
issue = issue,
authorHasMoreThanOneApp = authorHasMoreThanOneApp,
localeList = locales,
@@ -257,6 +257,7 @@ fun getAppDetailsActions() =
onRepoChanged = {},
onPreferredRepoChanged = {},
allowBetaVersions = {},
onAntiFeaturesOnboardingSeen = {},
ignoreAllUpdates = {},
ignoreThisUpdate = {},
shareApk = Intent(),
@@ -23,7 +23,7 @@ import kotlinx.coroutines.withContext
import org.fdroid.database.Repository
@Composable
fun getHintOverlayColor() = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.8f)
fun getHintOverlayColor() = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f)
fun Context.startActivitySafe(i: Intent?) {
if (i == null) return
+4
View File
@@ -226,6 +226,10 @@ This often occurs with apps installed via Google Play or other sources, if they
<string name="app_details_more_apps_interesting">This might also be interesting to you:</string>
<string name="app_details_more_apps_by_author">More apps by %1$s</string>
<string name="app_details_anti_features_title">What are Anti-Features?</string>
<string name="app_details_anti_features_text">Some apps come with behaviors you didn’t ask for — and might not want. Things like ads, tracking, or dependence on proprietary services. We call these Anti-Features.
\n\nApps in F-Droid are clearly labeled when they contain Anti-Features, so you can make an informed choice.</string>
<string name="about_title_full">About F-Droid</string>
<string name="about_title_basic">About F-Droid Basic</string>
<string name="about_text">Apps on F-Droid are free and open source software (FOSS), they respect your privacy, and they\'re available for anyone to see the source code, build on and improve.\n\nApp distribution is transparent, privacy-respecting, and accountable, setting a standard that challenges the mobile ecosystem to do better.\n\nF-Droid\'s impact is measured not just in apps published, but in the freedom, trust, and digital rights it helps protect.\n\nHelp F-Droid to protect community-powered innovation and continue our mission of openness, privacy, and user empowerment.</string>