Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .claude/docs/navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,19 @@ class MyActivity : DuckDuckGoActivity() {
}
```

Use `screenName` to opt into deeplink support:
Use `deeplinkScreenName` to opt into deeplink support:

```kotlin
@ContributeToActivityStarter(MyScreenParams::class, screenName = "myScreen")
@ContributeToActivityStarter(MyScreenParams::class, deeplinkScreenName = "myScreen")
```

Only declare one for screens that are a sensible entry point — settings screens, feature landing
screens. Do not declare one for screens in the middle of a flow, screens that need caller context
(a tab, a credential), screens that load a caller-provided URL, or screens in `*-internal` modules
and internal build variants. Names follow `<feature>.<subScreen>` with each segment camelCase
(`vpn.geoswitching`), or a single segment when the screen has no parent feature (`bookmarks`), and
must be unique: when two mappers claim the same name the winner is undefined.

## Choosing the overload

For `ActivityParams`:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import kotlinx.coroutines.flow.onEach
import javax.inject.Inject

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(DuckPlayerSettingsNoParams::class)
@ContributeToActivityStarter(DuckPlayerSettingsNoParams::class, deeplinkScreenName = "duckplayer.settings")
class DuckPlayerSettingsActivity : DuckDuckGoActivity() {

private val viewModel: DuckPlayerSettingsViewModel by bindViewModel()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@ import kotlin.reflect.KClass

/**
* Anvil annotation to generate and contribute the Map<ActivityParams, Class<ActivityParams>> to the activity starter.
* It is also possible to define a [screenName], that can be used to deeplink to a screen from RMF.
* It is also possible to define a [deeplinkScreenName], that can be used to deeplink to a screen from RMF.
*
* The [screenName] should be named as [feature].<screenName>. For instance, for the VPN feature has many sub-screens, eg. main, settings and so
* they could be named "vpn.main", "vpn.settings" etc.
* Not all screens will have a parent feature, for instance the main settings screen would be named just "settings"
* The [deeplinkScreenName] should be named as <feature>.<subScreen>, each segment camelCase. For instance the VPN feature has many
* sub-screens, eg. "vpn.main", "vpn.settings", "vpn.geoswitching". Not all screens will have a parent feature, for instance the main
* settings screen is named just "settings".
* The name must be unique across the app: several mappers claiming the same name are resolved in an undefined order.
*
* Only screens that make sense as a deeplink entry point should declare one. Screens in the middle of a flow, screens that need caller
* context, screens loading a caller-provided URL, and screens in internal/dev modules must not be deeplinkable.
*
* Usage:
* ```kotlin
* @ContributeToActivityStarter(ExampleActivityParams::class, screenName = "example")
* @ContributeToActivityStarter(ExampleActivityParams::class, deeplinkScreenName = "example")
* class MyActivity {
*
* }
Expand All @@ -43,5 +47,5 @@ annotation class ContributeToActivityStarter(
/** The type of the input parameters received by the Activity */
val paramsType: KClass<*>,
/** Declares the deeplink name for the Activity */
val screenName: String = "",
val deeplinkScreenName: String = "",
)
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,15 @@ class ContributeToActivityStarterProcessor(
return
}
val paramsClassName = paramsType.toClassName()
val screenName = annotation.getArgumentString("screenName").orEmpty()
val deeplinkScreenName = annotation.getArgumentString("deeplinkScreenName").orEmpty()

val mapperClassName = "${className}_${paramsClassName.simpleName}_Mapper"

val typeSpec = createMapperClass(
mapperClassName = mapperClassName,
activityClassName = activityClassName,
paramsClassName = paramsClassName,
screenName = screenName,
deeplinkScreenName = deeplinkScreenName,
)

fileSpecBuilder.addType(typeSpec)
Expand All @@ -152,7 +152,7 @@ class ContributeToActivityStarterProcessor(
mapperClassName: String,
activityClassName: ClassName,
paramsClassName: ClassName,
screenName: String,
deeplinkScreenName: String,
): TypeSpec {
val constructor = FunSpec.constructorBuilder()
.addAnnotation(INJECT_CLASS)
Expand Down Expand Up @@ -194,11 +194,12 @@ class ContributeToActivityStarterProcessor(
.addProperty(moshiProperty)
.addFunction(mapActivityParamsFun)
.apply {
if (screenName.isBlank()) {
if (deeplinkScreenName.isBlank()) {
addFunction(emptyDeeplinkMapper())
} else {
addFunction(createDeeplinkMapper(paramsClassName, screenName))
addFunction(createDeeplinkMapper(paramsClassName, deeplinkScreenName))
addFunction(createTryCreateObjectInstance())
addFunction(createTryCreateDefaultParams())
addFunction(createTryCreateActivityParams())
}
}
Expand All @@ -214,7 +215,7 @@ class ContributeToActivityStarterProcessor(
.build()
}

private fun createDeeplinkMapper(paramsClassName: ClassName, screenName: String): FunSpec {
private fun createDeeplinkMapper(paramsClassName: ClassName, deeplinkScreenName: String): FunSpec {
return FunSpec.builder("map")
.addModifiers(KModifier.OVERRIDE)
.addParameter("deeplinkActivityParams", DEEPLINK_ACTIVITY_PARAMS_CLASS)
Expand All @@ -237,13 +238,16 @@ class ContributeToActivityStarterProcessor(
if (instance != null) {
return instance
}
val defaultParams = tryCreateDefaultParams(%T::class.java)
return defaultParams
}
tryCreateActivityParams(%T::class.java, deeplinkActivityParams)
} else {
null
}
""".trimIndent(),
screenName,
deeplinkScreenName,
paramsClassName,
paramsClassName,
paramsClassName,
)
Expand Down Expand Up @@ -275,6 +279,30 @@ class ContributeToActivityStarterProcessor(
.build()
}

private fun createTryCreateDefaultParams(): FunSpec {
// Lets params types whose values all have defaults be deeplinked without a payload
return FunSpec.builder("tryCreateDefaultParams")
.addModifiers(KModifier.PRIVATE)
.addParameter(
"clazz",
Class::class.asClassName().parameterizedBy(
WildcardTypeName.producerOf(ACTIVITY_PARAMS_CLASS),
),
)
.returns(ACTIVITY_PARAMS_CLASS.copy(nullable = true))
.addCode(
CodeBlock.builder()
.add(
"""
return kotlin.runCatching {
moshi.adapter(clazz).fromJson("{}")
}.getOrNull()
""".trimIndent(),
).build(),
)
.build()
}

private fun createTryCreateActivityParams(): FunSpec {
return FunSpec.builder("tryCreateActivityParams")
.addModifiers(KModifier.PRIVATE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ class ContributeToActivityStarterProcessorTest {
)

@Test
fun `basic mapper without screenName generates correct code`() {
fun `basic mapper without deeplinkScreenName generates correct code`() {
val source = SourceFile.kotlin(
"TestActivity.kt",
"""
Expand All @@ -131,12 +131,12 @@ class ContributeToActivityStarterProcessorTest {

val result = compile(source, *commonStubs)
val generated = result.findGeneratedSource("TestActivity_ActivityMapper.kt")
val golden = loadGolden("ActivityMapper_NoScreenName.kt")
val golden = loadGolden("ActivityMapper_NoDeeplinkScreenName.kt")
assertEquals(golden, generated)
}

@Test
fun `mapper with screenName generates deeplink handling code`() {
fun `mapper with deeplinkScreenName generates deeplink handling code`() {
val source = SourceFile.kotlin(
"TestActivity.kt",
"""
Expand All @@ -147,14 +147,14 @@ class ContributeToActivityStarterProcessorTest {

data class TestParams(val id: String) : ActivityParams

@ContributeToActivityStarter(TestParams::class, screenName = "example")
@ContributeToActivityStarter(TestParams::class, deeplinkScreenName = "example")
class TestActivity : DuckDuckGoActivity()
""".trimIndent(),
)

val result = compile(source, *commonStubs)
val generated = result.findGeneratedSource("TestActivity_ActivityMapper.kt")
val golden = loadGolden("ActivityMapper_WithScreenName.kt")
val golden = loadGolden("ActivityMapper_WithDeeplinkScreenName.kt")
assertEquals(golden, generated)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public class TestActivity_TestParams_Mapper @Inject constructor() :
if (instance != null) {
return instance
}
val defaultParams = tryCreateDefaultParams(TestParams::class.java)
return defaultParams
}
tryCreateActivityParams(TestParams::class.java, deeplinkActivityParams)
} else {
Expand All @@ -52,6 +54,11 @@ public class TestActivity_TestParams_Mapper @Inject constructor() :
Types.getRawType(clazz).kotlin.objectInstance as GlobalActivityStarter.ActivityParams
}.getOrNull()

private fun tryCreateDefaultParams(clazz: Class<out GlobalActivityStarter.ActivityParams>):
GlobalActivityStarter.ActivityParams? = kotlin.runCatching {
moshi.adapter(clazz).fromJson("{}")
}.getOrNull()

private fun tryCreateActivityParams(clazz: Class<out GlobalActivityStarter.ActivityParams>,
deeplinkActivityParams: GlobalActivityStarter.DeeplinkActivityParams):
GlobalActivityStarter.ActivityParams? = kotlin.runCatching {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ import kotlinx.coroutines.launch
import javax.inject.Inject

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(AppTrackerOnboardingActivityWithEmptyParamsParams::class)
@ContributeToActivityStarter(AppTrackerOnboardingActivityWithEmptyParamsParams::class, deeplinkScreenName = "apptp.onboarding")
class VpnOnboardingActivity : DuckDuckGoActivity() {

@Inject
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ import javax.inject.Inject
import javax.inject.Provider

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(AppTrackerActivityWithEmptyParams::class, screenName = "apptp.main")
@ContributeToActivityStarter(AppTrackerActivityWithEmptyParams::class, deeplinkScreenName = "apptp.main")
class DeviceShieldTrackerActivity :
DuckDuckGoActivity(),
DeviceShieldActivityFeedFragment.DeviceShieldActivityFeedListener {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ import kotlinx.coroutines.flow.onEach
import javax.inject.Inject

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(AboutScreenNoParams::class)
@ContributeToActivityStarter(AboutScreenNoParams::class, deeplinkScreenName = "about")
class AboutDuckDuckGoActivity : DuckDuckGoActivity() {

private val viewModel: AboutDuckDuckGoViewModel by bindViewModel()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ import java.text.NumberFormat
import javax.inject.Inject

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(Default::class, screenName = "accessibility")
@ContributeToActivityStarter(HighlightedItem::class, screenName = "accessibility")
@ContributeToActivityStarter(Default::class, deeplinkScreenName = "accessibility")
@ContributeToActivityStarter(HighlightedItem::class, deeplinkScreenName = "accessibility")
class AccessibilityActivity : DuckDuckGoActivity() {

@Inject
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ import javax.inject.Inject
import com.duckduckgo.mobile.android.R as CommonR

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(Default::class, screenName = "appearance")
@ContributeToActivityStarter(HighlightedItem::class, screenName = "appearance")
@ContributeToActivityStarter(Default::class, deeplinkScreenName = "appearance")
@ContributeToActivityStarter(HighlightedItem::class, deeplinkScreenName = "appearance")
class AppearanceActivity : DuckDuckGoActivity() {
@Inject
lateinit var appTheme: AppTheme
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import javax.inject.Inject
import com.duckduckgo.mobile.android.R as CommonR

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(ThreatProtectionSettingsNoParams::class)
@ContributeToActivityStarter(ThreatProtectionSettingsNoParams::class, deeplinkScreenName = "threatProtection")
class ThreatProtectionSettingsActivity : DuckDuckGoActivity() {

private val viewModel: ThreatProtectionSettingsViewModel by bindViewModel()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import logcat.logcat
import javax.inject.Inject

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(GetDesktopBrowserActivityParams::class, screenName = "getDesktopBrowser")
@ContributeToActivityStarter(GetDesktopBrowserActivityParams::class, deeplinkScreenName = "getDesktopBrowser")
class GetDesktopBrowserActivity : DuckDuckGoActivity() {

@Inject
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import kotlinx.coroutines.launch
import javax.inject.Inject

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(DataClearingSettingsScreenNoParams::class)
@ContributeToActivityStarter(DataClearingSettingsScreenNoParams::class, deeplinkScreenName = "dataClearing")
class DataClearingSettingsActivity : DuckDuckGoActivity() {

@Inject
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import kotlinx.coroutines.flow.onEach
import javax.inject.Inject

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(GeneralSettingsScreenNoParams::class, screenName = "settingsGeneral")
@ContributeToActivityStarter(GeneralSettingsScreenNoParams::class, deeplinkScreenName = "settingsGeneral")
class GeneralSettingsActivity : DuckDuckGoActivity() {

@Inject
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import kotlinx.coroutines.flow.onEach
import javax.inject.Inject

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(ShowOnAppLaunchScreenNoParams::class, screenName = "settingsAfterInactivity")
@ContributeToActivityStarter(ShowOnAppLaunchScreenNoParams::class, deeplinkScreenName = "settingsAfterInactivity")
class ShowOnAppLaunchActivity : DuckDuckGoActivity() {

private val viewModel: ShowOnAppLaunchViewModel by bindViewModel()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import kotlinx.coroutines.flow.onEach
import javax.inject.Inject

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(PermissionsScreenNoParams::class)
@ContributeToActivityStarter(PermissionsScreenNoParams::class, deeplinkScreenName = "permissions")
class PermissionsActivity : DuckDuckGoActivity() {

@Inject
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import kotlinx.coroutines.flow.onEach
import javax.inject.Inject

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(PrivateSearchScreenNoParams::class)
@ContributeToActivityStarter(PrivateSearchScreenNoParams::class, deeplinkScreenName = "privateSearch")
class PrivateSearchActivity : DuckDuckGoActivity() {
@Inject
lateinit var globalActivityStarter: GlobalActivityStarter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ import javax.inject.Inject
private const val OTHER_PLATFORMS_URL = "https://duckduckgo.com/app"

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(SettingsScreenNoParams::class, screenName = "settings")
@ContributeToActivityStarter(SettingsScreenNoParams::class, deeplinkScreenName = "settings")
class SettingsActivity : DuckDuckGoActivity() {

private val viewModel: SettingsViewModel by bindViewModel()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ import kotlin.math.max
import com.duckduckgo.mobile.android.R as CommonR

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(TabSwitcherScreenNoParams::class, screenName = "tabSwitcher")
@ContributeToActivityStarter(TabSwitcherScreenWithParams::class, screenName = "tabSwitcherWithParams")
@ContributeToActivityStarter(TabSwitcherScreenNoParams::class, deeplinkScreenName = "tabSwitcher")
@ContributeToActivityStarter(TabSwitcherScreenWithParams::class, deeplinkScreenName = "tabSwitcherWithParams")
class TabSwitcherActivity :
DuckDuckGoActivity(),
TabSwitcherListener,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import javax.inject.Inject
import com.duckduckgo.mobile.android.R as CommonR

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(WebTrackingProtectionScreenNoParams::class)
@ContributeToActivityStarter(WebTrackingProtectionScreenNoParams::class, deeplinkScreenName = "webTrackingProtection")
class WebTrackingProtectionActivity : DuckDuckGoActivity() {

@Inject
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import org.junit.Test
* resolves the deeplink through [GlobalActivityStarter], which delegates to the mapper generated here.
*
* The mapper under test is generated by [com.duckduckgo.anvil.ksp.ContributeToActivityStarterProcessor]
* from the `@ContributeToActivityStarter(TabSwitcherScreenWithParams::class, screenName = ...)` annotation on
* from the `@ContributeToActivityStarter(TabSwitcherScreenWithParams::class, deeplinkScreenName = ...)` annotation on
* [TabSwitcherActivity].
*/
class TabSwitcherScreenDeeplinkMapperTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import javax.inject.Inject
data class ImportBookmarksViaGoogleTakeoutScreen(val launchSource: String) : GlobalActivityStarter.ActivityParams

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(ImportBookmarksViaGoogleTakeoutScreen::class, screenName = "importGoogleBookmarks")
@ContributeToActivityStarter(ImportBookmarksViaGoogleTakeoutScreen::class, deeplinkScreenName = "importGoogleBookmarks")
class ImportGoogleBookmarksWebFlowActivity :
DuckDuckGoActivity(),
ImportGoogleBookmarksWebFlowFragment.WebViewVisibilityListener {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ import logcat.logcat
import javax.inject.Inject

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(AutofillPasswordsManagementScreen::class)
@ContributeToActivityStarter(AutofillPasswordsManagementScreen::class, deeplinkScreenName = "passwords")
@ContributeToActivityStarter(AutofillPasswordsManagementScreenWithSuggestions::class)
@ContributeToActivityStarter(AutofillPasswordsManagementViewCredential::class)
class AutofillManagementActivity : DuckDuckGoActivity(), PasswordsScreenPromotionPlugin.Callback {
Expand Down
Loading
Loading