diff --git a/app/src/main/kotlin/org/fdroid/settings/OnboardingManager.kt b/app/src/main/kotlin/org/fdroid/settings/OnboardingManager.kt index 1dd31d95e..2f7b946d1 100644 --- a/app/src/main/kotlin/org/fdroid/settings/OnboardingManager.kt +++ b/app/src/main/kotlin/org/fdroid/settings/OnboardingManager.kt @@ -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) { diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AntiFeatures.kt b/app/src/main/kotlin/org/fdroid/ui/details/AntiFeatures.kt index d58fe2f46..270d69347 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AntiFeatures.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AntiFeatures.kt @@ -30,11 +30,11 @@ import org.fdroid.ui.utils.testApp @Composable @OptIn(ExperimentalMaterial3ExpressiveApi::class) -fun AntiFeatures(antiFeatures: List) { +fun AntiFeatures(antiFeatures: List, 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), diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt index d6bc28fea..135266d8f 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt @@ -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()) { diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt index 53bcfb4a0..b3956eb27 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt @@ -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? = 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, diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt index f1cde9358..bee95654d 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt @@ -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 diff --git a/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt b/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt index d32ced754..a07c4734a 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt @@ -47,6 +47,7 @@ fun DetailsPresenter( viewModel: AppDetailsViewModel, packageInfoFlow: StateFlow, currentRepoIdFlow: StateFlow, + showAntiFeaturesOnboardingFlow: StateFlow, appsWithIssuesFlow: StateFlow?>, networkStateFlow: StateFlow, ): 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, diff --git a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt index 8f40097e1..f324bd2b0 100644 --- a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt +++ b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt @@ -257,6 +257,7 @@ fun getAppDetailsActions() = onRepoChanged = {}, onPreferredRepoChanged = {}, allowBetaVersions = {}, + onAntiFeaturesOnboardingSeen = {}, ignoreAllUpdates = {}, ignoreThisUpdate = {}, shareApk = Intent(), diff --git a/app/src/main/kotlin/org/fdroid/ui/utils/UiUtils.kt b/app/src/main/kotlin/org/fdroid/ui/utils/UiUtils.kt index caea0d7d7..cdad68279 100644 --- a/app/src/main/kotlin/org/fdroid/ui/utils/UiUtils.kt +++ b/app/src/main/kotlin/org/fdroid/ui/utils/UiUtils.kt @@ -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 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8609bf3c6..9a34ca5bf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -226,6 +226,10 @@ This often occurs with apps installed via Google Play or other sources, if they This might also be interesting to you: More apps by %1$s + What are Anti-Features? + 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. + About F-Droid About F-Droid Basic 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.