From 94786e89fd5cb14e3f8184f5ec9316fcacaa2d95 Mon Sep 17 00:00:00 2001 From: Marvin Date: Sun, 9 Aug 2026 23:51:15 +0530 Subject: [PATCH 1/2] feat: make support prompts less intrusive --- .../compose/extensions/ActivityExtensions.kt | 29 +++--- .../fossify/commons/dialogs/DonateDialog.kt | 18 +++- .../fossify/commons/dialogs/RateAppDialog.kt | 31 ++++++ .../fossify/commons/extensions/Activity.kt | 16 +--- .../commons/extensions/SupportPrompt.kt | 95 +++++++++++++++++++ .../org/fossify/commons/helpers/BaseConfig.kt | 24 +++++ .../org/fossify/commons/helpers/Constants.kt | 6 ++ commons/src/main/res/values/strings.xml | 3 +- .../samples/activities/MainActivity.kt | 30 +----- 9 files changed, 190 insertions(+), 62 deletions(-) create mode 100644 commons/src/main/kotlin/org/fossify/commons/dialogs/RateAppDialog.kt create mode 100644 commons/src/main/kotlin/org/fossify/commons/extensions/SupportPrompt.kt diff --git a/commons/src/main/kotlin/org/fossify/commons/compose/extensions/ActivityExtensions.kt b/commons/src/main/kotlin/org/fossify/commons/compose/extensions/ActivityExtensions.kt index af4496b3a7..55fb633c17 100644 --- a/commons/src/main/kotlin/org/fossify/commons/compose/extensions/ActivityExtensions.kt +++ b/commons/src/main/kotlin/org/fossify/commons/compose/extensions/ActivityExtensions.kt @@ -13,13 +13,11 @@ import org.fossify.commons.R import org.fossify.commons.extensions.baseConfig import org.fossify.commons.extensions.checkAppIconColor import org.fossify.commons.extensions.getAppIconColors -import org.fossify.commons.extensions.getCanAppBeUpgraded import org.fossify.commons.extensions.getInternalStoragePath -import org.fossify.commons.extensions.isAProApp import org.fossify.commons.extensions.isAppInstalledOnSDCard -import org.fossify.commons.extensions.isOrWasThankYouInstalled import org.fossify.commons.extensions.launchViewIntent import org.fossify.commons.extensions.random +import org.fossify.commons.extensions.showAutomaticSupportPromptIfEligible import org.fossify.commons.extensions.toggleAppIconColor import org.fossify.commons.extensions.updateSDCardPath import org.fossify.commons.helpers.isOreoMr1Plus @@ -27,8 +25,6 @@ import org.fossify.commons.models.Release fun ComponentActivity.appLaunchedCompose( appId: String, - showUpgradeDialog: () -> Unit, - showDonateDialog: () -> Unit, ) { baseConfig.internalStoragePath = getInternalStoragePath() updateSDCardPath() @@ -64,15 +60,20 @@ fun ComponentActivity.appLaunchedCompose( } baseConfig.appRunCount++ - if (baseConfig.appRunCount % 30 == 0 && !isAProApp()) { - if (!resources.getBoolean(R.bool.hide_google_relations)) { - if (getCanAppBeUpgraded()) { - showUpgradeDialog() - } else if (!isOrWasThankYouInstalled()) { - showDonateDialog() - } - } - } + showAutomaticSupportPromptIfEligible() +} + +@Deprecated( + message = "Automatic support prompts are now handled by Commons.", + replaceWith = ReplaceWith("appLaunchedCompose(appId)"), +) +@Suppress("UNUSED_PARAMETER") +fun ComponentActivity.appLaunchedCompose( + appId: String, + showUpgradeDialog: () -> Unit, + showDonateDialog: () -> Unit, +) { + appLaunchedCompose(appId) } fun ComponentActivity.checkWhatsNewCompose(releases: List, currVersion: Int, showWhatsNewDialog: (List) -> Unit) { diff --git a/commons/src/main/kotlin/org/fossify/commons/dialogs/DonateDialog.kt b/commons/src/main/kotlin/org/fossify/commons/dialogs/DonateDialog.kt index f63090aa9c..9c62cfe391 100644 --- a/commons/src/main/kotlin/org/fossify/commons/dialogs/DonateDialog.kt +++ b/commons/src/main/kotlin/org/fossify/commons/dialogs/DonateDialog.kt @@ -33,19 +33,31 @@ import org.fossify.commons.databinding.DialogDonateBinding import org.fossify.commons.extensions.* class DonateDialog(val activity: Activity) { + private var onPurchase: () -> Unit = { activity.launchViewIntent(R.string.thank_you_url) } + private var onLater: () -> Unit = {} + + internal constructor( + activity: Activity, + onPurchase: () -> Unit, + onLater: () -> Unit, + ) : this(activity) { + this.onPurchase = onPurchase + this.onLater = onLater + } + init { val view = DialogDonateBinding.inflate(activity.layoutInflater, null, false).apply { dialogDonateImage.applyColorFilter(activity.getProperTextColor()) dialogDonateText.text = Html.fromHtml(activity.getString(R.string.donate_short)) dialogDonateText.movementMethod = LinkMovementMethod.getInstance() dialogDonateImage.setOnClickListener { - activity.launchViewIntent(R.string.thank_you_url) + onPurchase() } } activity.getAlertDialogBuilder() - .setPositiveButton(R.string.purchase) { _, _ -> activity.launchViewIntent(R.string.thank_you_url) } - .setNegativeButton(R.string.later, null) + .setPositiveButton(R.string.purchase) { _, _ -> onPurchase() } + .setNegativeButton(R.string.later) { _, _ -> onLater() } .apply { activity.setupDialogStuff(view.root, this, cancelOnTouchOutside = false) } diff --git a/commons/src/main/kotlin/org/fossify/commons/dialogs/RateAppDialog.kt b/commons/src/main/kotlin/org/fossify/commons/dialogs/RateAppDialog.kt new file mode 100644 index 0000000000..167c988f14 --- /dev/null +++ b/commons/src/main/kotlin/org/fossify/commons/dialogs/RateAppDialog.kt @@ -0,0 +1,31 @@ +package org.fossify.commons.dialogs + +import android.app.Activity +import org.fossify.commons.R +import org.fossify.commons.databinding.DialogMessageBinding +import org.fossify.commons.extensions.getAlertDialogBuilder +import org.fossify.commons.extensions.setupDialogStuff + +internal class RateAppDialog( + activity: Activity, + onRate: () -> Unit, + onLater: () -> Unit, +) { + init { + val view = DialogMessageBinding.inflate(activity.layoutInflater, null, false).apply { + message.setText(R.string.rate_app_prompt) + } + + activity.getAlertDialogBuilder() + .setPositiveButton(R.string.rate_this_app) { _, _ -> onRate() } + .setNegativeButton(R.string.later) { _, _ -> onLater() } + .apply { + activity.setupDialogStuff( + view = view.root, + dialog = this, + titleId = R.string.rate_this_app, + cancelOnTouchOutside = false, + ) + } + } +} diff --git a/commons/src/main/kotlin/org/fossify/commons/extensions/Activity.kt b/commons/src/main/kotlin/org/fossify/commons/extensions/Activity.kt index 3acdac26a8..b5fe88db78 100644 --- a/commons/src/main/kotlin/org/fossify/commons/extensions/Activity.kt +++ b/commons/src/main/kotlin/org/fossify/commons/extensions/Activity.kt @@ -49,10 +49,8 @@ import org.fossify.commons.dialogs.AppSideloadedDialog import org.fossify.commons.dialogs.ConfirmationAdvancedDialog import org.fossify.commons.dialogs.ConfirmationDialog import org.fossify.commons.dialogs.CustomIntervalPickerDialog -import org.fossify.commons.dialogs.DonateDialog import org.fossify.commons.dialogs.RadioGroupDialog import org.fossify.commons.dialogs.SecurityDialog -import org.fossify.commons.dialogs.UpgradeToProDialog import org.fossify.commons.dialogs.WhatsNewDialog import org.fossify.commons.dialogs.WritePermissionDialog import org.fossify.commons.dialogs.WritePermissionDialog.WritePermissionDialogMode @@ -127,19 +125,7 @@ fun Activity.appLaunched(appId: String) { } baseConfig.appRunCount++ - if (baseConfig.appRunCount % 30 == 0 && !isAProApp()) { - if (!resources.getBoolean(R.bool.hide_google_relations)) { - showDonateOrUpgradeDialog() - } - } -} - -fun Activity.showDonateOrUpgradeDialog() { - if (getCanAppBeUpgraded()) { - UpgradeToProDialog(this) - } else if (!isOrWasThankYouInstalled()) { - DonateDialog(this) - } + showAutomaticSupportPromptIfEligible() } fun Activity.isAppInstalledOnSDCard(): Boolean = try { diff --git a/commons/src/main/kotlin/org/fossify/commons/extensions/SupportPrompt.kt b/commons/src/main/kotlin/org/fossify/commons/extensions/SupportPrompt.kt new file mode 100644 index 0000000000..af7ae8a9f0 --- /dev/null +++ b/commons/src/main/kotlin/org/fossify/commons/extensions/SupportPrompt.kt @@ -0,0 +1,95 @@ +package org.fossify.commons.extensions + +import android.app.Activity +import org.fossify.commons.R +import org.fossify.commons.dialogs.DonateDialog +import org.fossify.commons.dialogs.RateAppDialog +import org.fossify.commons.helpers.BaseConfig + +private const val FIRST_PROMPT_MIN_LAUNCHES = 10 +private const val NEXT_PROMPT_MIN_LAUNCHES = 5 +private const val MAX_PROMPT_DISMISSALS = 3 +private const val DAY_MILLIS = 24L * 60L * 60L * 1000L +private const val FIRST_PROMPT_DELAY_MILLIS = 7L * DAY_MILLIS +private const val NEXT_PROMPT_DELAY_MILLIS = 30L * DAY_MILLIS + +private enum class SupportPromptType(val value: Int) { + RateApp(0), + ThankYou(1); + + fun alternate() = when (this) { + RateApp -> ThankYou + ThankYou -> RateApp + } + + companion object { + fun fromValue(value: Int) = entries.firstOrNull { it.value == value } ?: RateApp + } +} + +internal fun Activity.showAutomaticSupportPromptIfEligible() { + val config = baseConfig + val now = System.currentTimeMillis() + if (config.supportPromptFirstLaunchTimestamp == 0L) { + config.supportPromptFirstLaunchTimestamp = now + return + } + + if (resources.getBoolean(R.bool.hide_google_relations) || config.supportPromptDismissalCount >= MAX_PROMPT_DISMISSALS) { + return + } + + val hasEnoughInitialUse = config.appRunCount >= FIRST_PROMPT_MIN_LAUNCHES + val hasEnoughInitialTime = now - config.supportPromptFirstLaunchTimestamp >= FIRST_PROMPT_DELAY_MILLIS + if (!hasEnoughInitialUse || !hasEnoughInitialTime) { + return + } + + if (config.supportPromptLastShownTimestamp != 0L) { + val hasEnoughFurtherUse = config.appRunCount - config.supportPromptLastShownAppRunCount >= NEXT_PROMPT_MIN_LAUNCHES + val hasEnoughFurtherTime = now - config.supportPromptLastShownTimestamp >= NEXT_PROMPT_DELAY_MILLIS + if (!hasEnoughFurtherUse || !hasEnoughFurtherTime) { + return + } + } + + val promptType = selectSupportPrompt(config) ?: return + config.supportPromptLastShownTimestamp = now + config.supportPromptLastShownAppRunCount = config.appRunCount + config.supportPromptNextType = promptType.alternate().value + + val onLater = { + config.supportPromptDismissalCount = (config.supportPromptDismissalCount + 1).coerceAtMost(MAX_PROMPT_DISMISSALS) + } + + when (promptType) { + SupportPromptType.RateApp -> RateAppDialog( + activity = this, + onRate = { + config.wasRatePromptAccepted = true + launchAppRatingPage() + }, + onLater = onLater, + ) + + SupportPromptType.ThankYou -> DonateDialog( + activity = this, + onPurchase = { launchPurchaseThankYouIntent() }, + onLater = onLater, + ) + } +} + +private fun Activity.selectSupportPrompt(config: BaseConfig): SupportPromptType? { + val canShowRatePrompt = !config.wasRatePromptAccepted + val canShowThankYouPrompt = !isOrWasThankYouInstalled() + val preferredPrompt = SupportPromptType.fromValue(config.supportPromptNextType) + + return when { + preferredPrompt == SupportPromptType.RateApp && canShowRatePrompt -> SupportPromptType.RateApp + preferredPrompt == SupportPromptType.ThankYou && canShowThankYouPrompt -> SupportPromptType.ThankYou + canShowRatePrompt -> SupportPromptType.RateApp + canShowThankYouPrompt -> SupportPromptType.ThankYou + else -> null + } +} diff --git a/commons/src/main/kotlin/org/fossify/commons/helpers/BaseConfig.kt b/commons/src/main/kotlin/org/fossify/commons/helpers/BaseConfig.kt index 66dfae5d42..84be23bf71 100644 --- a/commons/src/main/kotlin/org/fossify/commons/helpers/BaseConfig.kt +++ b/commons/src/main/kotlin/org/fossify/commons/helpers/BaseConfig.kt @@ -29,6 +29,30 @@ open class BaseConfig(val context: Context) { get() = prefs.getInt(APP_RUN_COUNT, 0) set(appRunCount) = prefs.edit().putInt(APP_RUN_COUNT, appRunCount).apply() + var supportPromptFirstLaunchTimestamp: Long + get() = prefs.getLong(SUPPORT_PROMPT_FIRST_LAUNCH_TIMESTAMP, 0L) + set(timestamp) = prefs.edit().putLong(SUPPORT_PROMPT_FIRST_LAUNCH_TIMESTAMP, timestamp).apply() + + var supportPromptLastShownTimestamp: Long + get() = prefs.getLong(SUPPORT_PROMPT_LAST_SHOWN_TIMESTAMP, 0L) + set(timestamp) = prefs.edit().putLong(SUPPORT_PROMPT_LAST_SHOWN_TIMESTAMP, timestamp).apply() + + var supportPromptLastShownAppRunCount: Int + get() = prefs.getInt(SUPPORT_PROMPT_LAST_SHOWN_APP_RUN_COUNT, 0) + set(appRunCount) = prefs.edit().putInt(SUPPORT_PROMPT_LAST_SHOWN_APP_RUN_COUNT, appRunCount).apply() + + var supportPromptDismissalCount: Int + get() = prefs.getInt(SUPPORT_PROMPT_DISMISSAL_COUNT, 0) + set(dismissalCount) = prefs.edit().putInt(SUPPORT_PROMPT_DISMISSAL_COUNT, dismissalCount).apply() + + var supportPromptNextType: Int + get() = prefs.getInt(SUPPORT_PROMPT_NEXT_TYPE, 0) + set(promptType) = prefs.edit().putInt(SUPPORT_PROMPT_NEXT_TYPE, promptType).apply() + + var wasRatePromptAccepted: Boolean + get() = prefs.getBoolean(WAS_RATE_PROMPT_ACCEPTED, false) + set(wasAccepted) = prefs.edit().putBoolean(WAS_RATE_PROMPT_ACCEPTED, wasAccepted).apply() + var lastVersion: Int get() = prefs.getInt(LAST_VERSION, 0) set(lastVersion) = prefs.edit().putInt(LAST_VERSION, lastVersion).apply() diff --git a/commons/src/main/kotlin/org/fossify/commons/helpers/Constants.kt b/commons/src/main/kotlin/org/fossify/commons/helpers/Constants.kt index 4aef023e30..f31a1e73ff 100644 --- a/commons/src/main/kotlin/org/fossify/commons/helpers/Constants.kt +++ b/commons/src/main/kotlin/org/fossify/commons/helpers/Constants.kt @@ -81,6 +81,12 @@ const val YEAR_SECONDS = YEAR_MINUTES * 60 // shared preferences const val PREFS_KEY = "Prefs" const val APP_RUN_COUNT = "app_run_count" +const val SUPPORT_PROMPT_FIRST_LAUNCH_TIMESTAMP = "support_prompt_first_launch_timestamp" +const val SUPPORT_PROMPT_LAST_SHOWN_TIMESTAMP = "support_prompt_last_shown_timestamp" +const val SUPPORT_PROMPT_LAST_SHOWN_APP_RUN_COUNT = "support_prompt_last_shown_app_run_count" +const val SUPPORT_PROMPT_DISMISSAL_COUNT = "support_prompt_dismissal_count" +const val SUPPORT_PROMPT_NEXT_TYPE = "support_prompt_next_type" +const val WAS_RATE_PROMPT_ACCEPTED = "was_rate_prompt_accepted" const val LAST_VERSION = "last_version" const val SD_TREE_URI = "tree_uri_2" const val PRIMARY_ANDROID_DATA_TREE_URI = "primary_android_data_tree_uri_2" diff --git a/commons/src/main/res/values/strings.xml b/commons/src/main/res/values/strings.xml index f3edff4241..118a468026 100644 --- a/commons/src/main/res/values/strings.xml +++ b/commons/src/main/res/values/strings.xml @@ -921,6 +921,7 @@ Hey, come check out %1$s at %2$s Invite via Rate this app + Enjoying the app? Please rate it on Google Play. Donate Follow us v %1$s\nCopyright © Fossify %2$d @@ -943,7 +944,7 @@ ]]> Hello,<br><br>hope you are enjoying the app. It contains no ads and we aren\'t collecting your data either, please support its development by purchasing <a href=https://play.google.com/store/apps/details?id=org.fossify.thankyou>Fossify Thank You</a>. You will also have all app features including color customization unlocked.<br><br>Thank you! - Support us by purchasing <a href=https://play.google.com/store/apps/details?id=org.fossify.thankyou>Fossify Thank You</a> please, it will also unlock all app features including color customization. + Support us by purchasing <a href=https://play.google.com/store/apps/details?id=org.fossify.thankyou>Fossify Thank You</a>. It also enables some additional customization. Purchase Please update Fossify Thank You to the latest version Before you ask a question, please check the app settings and read the Frequently Asked Questions first. Maybe the solution is there. diff --git a/samples/src/main/kotlin/org/fossify/commons/samples/activities/MainActivity.kt b/samples/src/main/kotlin/org/fossify/commons/samples/activities/MainActivity.kt index eccf848e71..69972d8192 100644 --- a/samples/src/main/kotlin/org/fossify/commons/samples/activities/MainActivity.kt +++ b/samples/src/main/kotlin/org/fossify/commons/samples/activities/MainActivity.kt @@ -3,12 +3,8 @@ package org.fossify.commons.samples.activities import android.content.Intent import android.os.Bundle import androidx.activity.compose.setContent -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import org.fossify.commons.activities.BaseSimpleActivity import org.fossify.commons.activities.ManageBlockedNumbersActivity -import org.fossify.commons.compose.alert_dialog.AlertDialogState -import org.fossify.commons.compose.alert_dialog.rememberAlertDialogState import org.fossify.commons.compose.extensions.DEVELOPER_PLAY_STORE_URL import org.fossify.commons.compose.extensions.FAKE_VERSION_APP_LABEL import org.fossify.commons.compose.extensions.appLaunchedCompose @@ -16,8 +12,6 @@ import org.fossify.commons.compose.extensions.enableEdgeToEdgeSimple import org.fossify.commons.compose.extensions.onEventValue import org.fossify.commons.compose.theme.AppThemeSurface import org.fossify.commons.dialogs.ConfirmationDialog -import org.fossify.commons.dialogs.DonateAlertDialog -import org.fossify.commons.extensions.appLaunched import org.fossify.commons.extensions.launchMoreAppsFromUsIntent import org.fossify.commons.extensions.launchViewIntent import org.fossify.commons.helpers.LICENSE_AUTOFITTEXTVIEW @@ -30,7 +24,7 @@ class MainActivity : BaseSimpleActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - appLaunched(BuildConfig.APPLICATION_ID) + appLaunchedCompose(BuildConfig.APPLICATION_ID) enableEdgeToEdgeSimple() setContent { AppThemeSurface { @@ -58,32 +52,10 @@ class MainActivity : BaseSimpleActivity() { openAbout = ::launchAbout, moreAppsFromUs = ::launchMoreAppsFromUsIntent ) - AppLaunched() } } } - @Composable - private fun AppLaunched( - donateAlertDialogState: AlertDialogState = getDonateAlertDialogState(), - ) { - LaunchedEffect(Unit) { - appLaunchedCompose( - appId = BuildConfig.APPLICATION_ID, - showDonateDialog = donateAlertDialogState::show, - showUpgradeDialog = {} - ) - } - } - - @Composable - private fun getDonateAlertDialogState() = - rememberAlertDialogState().apply { - DialogMember { - DonateAlertDialog(alertDialogState = this) - } - } - private fun launchAbout() { val licenses = LICENSE_AUTOFITTEXTVIEW From 2dd9494b796a228d61550714d8dfdbbb6c66cf3e Mon Sep 17 00:00:00 2001 From: Marvin <295208132+marvinctl@users.noreply.github.com> Date: Mon, 10 Aug 2026 00:27:13 +0530 Subject: [PATCH 2/2] fix: satisfy support prompt checks --- .../fossify/commons/extensions/SupportPrompt.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/commons/src/main/kotlin/org/fossify/commons/extensions/SupportPrompt.kt b/commons/src/main/kotlin/org/fossify/commons/extensions/SupportPrompt.kt index af7ae8a9f0..8b4e795ca4 100644 --- a/commons/src/main/kotlin/org/fossify/commons/extensions/SupportPrompt.kt +++ b/commons/src/main/kotlin/org/fossify/commons/extensions/SupportPrompt.kt @@ -35,18 +35,19 @@ internal fun Activity.showAutomaticSupportPromptIfEligible() { return } - if (resources.getBoolean(R.bool.hide_google_relations) || config.supportPromptDismissalCount >= MAX_PROMPT_DISMISSALS) { - return - } - val hasEnoughInitialUse = config.appRunCount >= FIRST_PROMPT_MIN_LAUNCHES val hasEnoughInitialTime = now - config.supportPromptFirstLaunchTimestamp >= FIRST_PROMPT_DELAY_MILLIS - if (!hasEnoughInitialUse || !hasEnoughInitialTime) { + val isSupportPromptDisabled = + resources.getBoolean(R.bool.hide_google_relations) || + config.supportPromptDismissalCount >= MAX_PROMPT_DISMISSALS + val lacksInitialEligibility = !hasEnoughInitialUse || !hasEnoughInitialTime + if (isSupportPromptDisabled || lacksInitialEligibility) { return } if (config.supportPromptLastShownTimestamp != 0L) { - val hasEnoughFurtherUse = config.appRunCount - config.supportPromptLastShownAppRunCount >= NEXT_PROMPT_MIN_LAUNCHES + val hasEnoughFurtherUse = + config.appRunCount - config.supportPromptLastShownAppRunCount >= NEXT_PROMPT_MIN_LAUNCHES val hasEnoughFurtherTime = now - config.supportPromptLastShownTimestamp >= NEXT_PROMPT_DELAY_MILLIS if (!hasEnoughFurtherUse || !hasEnoughFurtherTime) { return @@ -59,7 +60,8 @@ internal fun Activity.showAutomaticSupportPromptIfEligible() { config.supportPromptNextType = promptType.alternate().value val onLater = { - config.supportPromptDismissalCount = (config.supportPromptDismissalCount + 1).coerceAtMost(MAX_PROMPT_DISMISSALS) + config.supportPromptDismissalCount = + (config.supportPromptDismissalCount + 1).coerceAtMost(MAX_PROMPT_DISMISSALS) } when (promptType) {