Skip to content

Conversation

@anikiki
Copy link
Contributor

@anikiki anikiki commented Dec 11, 2025

Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1212329205023394?focus=true

Description

Implemented a new Modal Coordinator system that manages when and how modals are shown in the app. This system:

  • Creates a priority-based evaluation mechanism where evaluators run in order (lower number = higher priority)
  • Implements a 24-hour cooldown period to prevent multiple modals from appearing too frequently
  • Migrates existing modal-like components (RemoteMessageModalSurfaceEvaluator and AdditionalDefaultBrowserPromptsImpl) to use the new system
  • Ensures only one modal can be shown at a time, improving user experience

Steps to test this PR

See detailed testing scenarios and help in the Testing section in the description of https://app.asana.com/1/137249556945/project/1200581511062568/task/1212329205023394?focus=true

Modal Coordinator System

  • Verify that modals appear in priority order (RemoteMessageModal has priority 1, DefaultBrowserPrompts has priority 2)
  • Confirm that after a modal is shown, no other modals appear for 24 hours
  • Check that migrated components (remote messages, default browser prompts) still function correctly

Default Browser Prompts

  • Verify the default browser prompts still appear at appropriate times
  • Confirm the prompts respect the modal coordination system's rules

Remote Message Modals

  • Verify remote message modals still appear when expected
  • Confirm they respect the priority system and cooldown period

Note

Adds a prioritized modal coordination system with 24‑hour cooldown and migrates Remote Message modal and Default Browser prompts to it.

  • Modal Coordination (new)
    • Add modal-coordinator-api with ModalEvaluator and ModalEvaluatorCompletionStore.
    • Add modal-coordinator-impl with ModalEvaluatorCoordinator (priority-based, single-run per resume, 24h cooldown) and DataStore-backed completion store.
  • Integrations
    • Wire modules in app/build.gradle and depend on API from remote-messaging-impl.
  • Migrations
    • RemoteMessageModalSurfaceEvaluator -> implements ModalEvaluator (priority 1), enforces 4h background threshold via completion store, launches modal and clears timestamp.
    • AdditionalDefaultBrowserPromptsImpl -> implements ModalEvaluator (priority 2), refactors evaluate to return EvaluationResult; removes lifecycle observer hooks; minor IO dispatching tweak.
  • Tests
    • Add unit tests for coordinator behavior and completion store.
    • Update/add tests for remote messaging modal evaluator and default browser prompts evaluator.

Written by Cursor Bugbot for commit 661ffa2. This will update automatically on new commits. Configure here.

@anikiki anikiki force-pushed the feaure/ana/android_whats_new_promo_message_update_the_remotemessagemodalsurfaceevaluatorimpl_with_all_conditions branch from 92ec7e1 to a7d6647 Compare December 11, 2025 23:24
@anikiki anikiki force-pushed the feature/ana/android_whats_new_promo_message_prompt_dispatcher_modal_coordinator branch from f80d95b to 4dd0807 Compare December 11, 2025 23:24
@anikiki anikiki force-pushed the feaure/ana/android_whats_new_promo_message_update_the_remotemessagemodalsurfaceevaluatorimpl_with_all_conditions branch from a7d6647 to 05e9c3e Compare December 12, 2025 00:25
@anikiki anikiki force-pushed the feature/ana/android_whats_new_promo_message_prompt_dispatcher_modal_coordinator branch from 4dd0807 to afd2782 Compare December 12, 2025 00:25
@anikiki anikiki force-pushed the feaure/ana/android_whats_new_promo_message_update_the_remotemessagemodalsurfaceevaluatorimpl_with_all_conditions branch from 05e9c3e to 1bfabe0 Compare December 12, 2025 11:05
@anikiki anikiki force-pushed the feature/ana/android_whats_new_promo_message_prompt_dispatcher_modal_coordinator branch from afd2782 to 1874dd5 Compare December 12, 2025 11:05
@anikiki anikiki force-pushed the feaure/ana/android_whats_new_promo_message_update_the_remotemessagemodalsurfaceevaluatorimpl_with_all_conditions branch from 1bfabe0 to 068f409 Compare December 12, 2025 13:25
@anikiki anikiki force-pushed the feature/ana/android_whats_new_promo_message_prompt_dispatcher_modal_coordinator branch from 1874dd5 to 998e90e Compare December 12, 2025 13:25
@anikiki anikiki changed the base branch from feaure/ana/android_whats_new_promo_message_update_the_remotemessagemodalsurfaceevaluatorimpl_with_all_conditions to feature/ana/android_whats_new_promo_message_implement_pixels December 12, 2025 19:04
@anikiki anikiki changed the title Add modal coordinator implementation with priority-based evaluation and 24-hour blocking. [Android] "What’s New" promo message: Prompt dispatcher - modal coordinator Dec 13, 2025
@anikiki anikiki force-pushed the feature/ana/android_whats_new_promo_message_implement_pixels branch from 366bbc1 to b411140 Compare December 13, 2025 21:09
@anikiki anikiki force-pushed the feature/ana/android_whats_new_promo_message_prompt_dispatcher_modal_coordinator branch from 40609a5 to 64ebebb Compare December 13, 2025 21:09
init {
appCoroutineScope.launch {
userStageStore.userAppStageFlow().collect {
evaluate()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this as it's not really needed and created more issues. I wanted to remove it when I worked on the initial project, but forgot.

override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
appCoroutineScope.launch {
evaluate()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now called as part of ModalEvaluatorCoordinator's onResume.

appCoroutineScope.launch {
appCoroutineScope.launch(dispatchers.io()) {
featureSettings = defaultBrowserPromptsFeatureToggles.parseFeatureSettings()
evaluate()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, not needed, removed.

defaultBrowserPromptsDataStore.storeShowSetAsDefaultMessageState(action.showMessage)

return@withLock if (action.showMessageDialog) {
ModalEvaluator.EvaluationResult.CompletedWithAction
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only place where a modal is shown, hence the only place that returns CompletedWithAction.

override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
appCoroutineScope.launch {
evaluate()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to ModalEvaluatorCoordinator onResume


intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
applicationContext.startActivity(intent)
return@withContext ModalEvaluator.EvaluationResult.CompletedWithAction
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in the other evaluator, this is the place a modal is shown, hence returning CompletedWithAction

@anikiki anikiki force-pushed the feature/ana/android_whats_new_promo_message_implement_pixels branch from b411140 to 9c0c2a1 Compare December 15, 2025 16:07
@anikiki anikiki force-pushed the feature/ana/android_whats_new_promo_message_prompt_dispatcher_modal_coordinator branch from 08e3793 to 091a355 Compare December 15, 2025 16:07
@anikiki anikiki marked this pull request as ready for review December 15, 2025 17:28
@anikiki anikiki requested a review from cmonfortep December 15, 2025 17:28
@anikiki anikiki force-pushed the feature/ana/android_whats_new_promo_message_implement_pixels branch 2 times, most recently from 12c6b27 to 507fc69 Compare December 16, 2025 11:08
@anikiki anikiki force-pushed the feature/ana/android_whats_new_promo_message_prompt_dispatcher_modal_coordinator branch from 2549ea7 to f4be73c Compare December 16, 2025 11:08
@anikiki anikiki changed the base branch from feature/ana/android_whats_new_promo_message_implement_pixels to graphite-base/7343 December 16, 2025 11:34
@anikiki anikiki force-pushed the feature/ana/android_whats_new_promo_message_prompt_dispatcher_modal_coordinator branch from f4be73c to 1045448 Compare December 16, 2025 11:35
@graphite-app graphite-app bot changed the base branch from graphite-base/7343 to develop December 16, 2025 11:35
@anikiki anikiki force-pushed the feature/ana/android_whats_new_promo_message_prompt_dispatcher_modal_coordinator branch from 1045448 to 51a045a Compare December 16, 2025 11:35
@anikiki anikiki force-pushed the feature/ana/android_whats_new_promo_message_prompt_dispatcher_modal_coordinator branch from 51a045a to 661ffa2 Compare December 16, 2025 17:24
Copy link
Contributor

@cmonfortep cmonfortep left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code wise, looks good.

I have some concerns with behavior we introduce, and implicit behavior of the modal. Left some comments about that.

My main concern on the behavior: After a modal is shown, if we restart the app, the modal doesn't show, but it will show again after 24h.

My expectation will be: After the modal is shown, even if we restart the app, it shows again if the modal needs to show (that logic belongs to the modal). Coordinator only makes sure that after the modal is gone, we do a 24h cooldown, if that's the requirement.


package com.duckduckgo.modalcoordinator.api

interface ModalEvaluatorCompletionStore {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we missed the api proposal for this api. From a quick look, not all methods are used outside this api. Exposing the Store in the api also feels not ideal.

If the purpose of modal-coordinator is to avoid multiple modals showing at the same time, the logic around cooldown period or background checks might not need to be exposed. If we need that, we should probably include it when implementing the plugin.

logcat { "ModalEvaluatorCoordinator: Evaluating '${evaluator.evaluatorId}' (priority ${evaluator.priority})" }

// Start evaluation
when (evaluator.evaluate()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here relies the issue I mentioned on MM about modals blocking themselves if the app restarts, and appearing again after 24h.

Few things I see:

  • when evaluate returns CompletedWithAction and we record completion.
  • completion is confusing, or at least I don't think it matches the definition of completion on every modal. Right now we know (or expect) they have been shown.
  • I think the modal itself should track their own state (e.g: RMF will own his own state on the database like scheduled, dismissed, done)
  • Here I would just track timestamp and id

One proposal here (not sure if it will work) will be to update the logic so we store the time the modal is shown (like you do now) and the id. If within the 24h threshold, we only call evaluate for the plugin with that Id. That way we allow only the modal that was last shown to appear within that 24h window. Others will need to wait for the cool down to finish.

return@withContext ModalEvaluator.EvaluationResult.Skipped
}

if (!hasMetBackgroundTimeThreshold()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't fully understand why this logic. So the app needs to be in the background for more than 4h in order to show. I would do that if the event is onResume, but if it's an app fresh start, why do we need that? is that a requirement?

sealed class EvaluationResult {

/** Evaluation completed and modal was shown/triggered */
object CompletedWithAction : EvaluationResult()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if this is to represent shown we should rename to something that makes that clear.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants