Added unhandled actions handling

commit_hash:f7b6e386eb6f815b56a0fadd8644c44c84e21cf5
This commit is contained in:
pkurchatov
2026-04-03 17:46:34 +03:00
parent 172c2aa44d
commit fe4a87d23d
11 changed files with 145 additions and 60 deletions
+2 -1
View File
@@ -614,7 +614,8 @@
"client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivActionData.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivActionData.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivActionHandler.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivActionHandler.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivActionHandlingContext.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivActionHandlingContext.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivCustomActionHandler.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivCustomActionHandler.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivCustomActionData.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivCustomActionData.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivExternalActionHandler.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivExternalActionHandler.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/SetVariableActionHandler.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/SetVariableActionHandler.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/UpdateStructureActionHandler.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/UpdateStructureActionHandler.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivLocalContext.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivLocalContext.kt",
@@ -5,9 +5,7 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import coil3.ImageLoader
import coil3.request.allowHardware
import com.yandex.div.compose.actions.DivActionData
import com.yandex.div.compose.actions.DivActionHandlingContext
import com.yandex.div.compose.actions.DivCustomActionHandler
import com.yandex.div.compose.actions.DivExternalActionHandler
import com.yandex.div.compose.dagger.Names
import com.yandex.div.compose.dagger.`Yatagan$DivContextComponent`
import com.yandex.div.compose.internal.ImageLoaderProvider
@@ -35,7 +33,7 @@ import javax.inject.Named
@PublicApi
class DivComposeConfiguration(
@get:Provides
val customActionHandler: DivCustomActionHandler = defaultCustomActionHandler,
val actionHandler: DivExternalActionHandler = defaultActionHandler,
@get:Provides
val fontFamilyProvider: DivFontFamilyProvider = defaultFontFamilyProvider,
@@ -66,12 +64,7 @@ private val defaultImageLoaderProvider = ImageLoaderProvider { context ->
.build()
}
private val defaultCustomActionHandler = object : DivCustomActionHandler {
override fun handle(
context: DivActionHandlingContext,
action: DivActionData
) = Unit
}
private val defaultActionHandler = object : DivExternalActionHandler {}
private val defaultFontFamilyProvider = object : DivFontFamilyProvider {
override fun getFontFamily(fontFamilyName: String?, weight: FontWeight): FontFamily {
@@ -5,10 +5,11 @@ import com.yandex.div.core.annotations.PublicApi
import org.json.JSONObject
/**
* Data associated with a custom DivKit action.
* Data associated with a DivKit action.
*/
@PublicApi
class DivActionData(
data class DivActionData(
val id: String,
val payload: JSONObject?,
val url: Uri?
)
@@ -3,13 +3,14 @@ package com.yandex.div.compose.actions
import com.yandex.div.compose.DivReporter
import com.yandex.div.compose.dagger.DivContextScope
import com.yandex.div.internal.actions.DivUntypedAction
import com.yandex.div.internal.actions.isDivAction
import com.yandex.div2.DivAction
import com.yandex.div2.DivActionTyped
import javax.inject.Inject
@DivContextScope
internal class DivActionHandler @Inject constructor(
private val customActionHandler: DivCustomActionHandler,
private val externalActionHandler: DivExternalActionHandler,
private val reporter: DivReporter,
private val arrayActionsHandler: ArrayActionsHandler,
private val dictSetValueActionHandler: DictSetValueActionHandler,
@@ -18,14 +19,30 @@ internal class DivActionHandler @Inject constructor(
) {
fun handle(context: DivActionHandlingContext, action: DivAction) {
val expressionResolver = context.expressionResolver
if (!action.isEnabled.evaluate(expressionResolver)) {
return
}
action.typed?.let {
handle(context = context, action = it, baseAction = action)
return
}
val uri = action.url?.evaluate(context.expressionResolver) ?: return
DivUntypedAction.parse(uri)?.let {
handle(context = context, action = it)
val url = action.url?.evaluate(expressionResolver) ?: return
if (url.isDivAction) {
DivUntypedAction.parse(url)?.let {
handle(context = context, action = it)
}
} else {
externalActionHandler.handle(
context = context,
action = DivActionData(
id = action.logId.evaluate(expressionResolver),
payload = action.payload,
url = action.url?.evaluate(expressionResolver)
)
)
}
}
@@ -49,11 +66,11 @@ internal class DivActionHandler @Inject constructor(
is DivActionTyped.ClearFocus -> notSupported()
is DivActionTyped.CopyToClipboard -> notSupported()
is DivActionTyped.Custom ->
customActionHandler.handle(
externalActionHandler.handleCustomAction(
context = context,
action = DivActionData(
payload = baseAction.payload,
url = baseAction.url?.evaluate(context.expressionResolver)
action = DivCustomActionData(
id = baseAction.logId.evaluate(context.expressionResolver),
payload = baseAction.payload
)
)
@@ -0,0 +1,13 @@
package com.yandex.div.compose.actions
import com.yandex.div.core.annotations.PublicApi
import org.json.JSONObject
/**
* Data associated with a custom DivKit action (action with `"type": "custom"`).
*/
@PublicApi
data class DivCustomActionData(
val id: String,
val payload: JSONObject?
)
@@ -1,19 +0,0 @@
package com.yandex.div.compose.actions
import com.yandex.div.core.annotations.PublicApi
/**
* Handler for custom DivKit actions (actions with `"type": "custom"`).
*
* Implement this interface to handle application-specific actions.
*
* @see com.yandex.div.compose.DivComposeConfiguration
*/
@PublicApi
interface DivCustomActionHandler {
/**
* Called when a custom action is triggered.
*/
fun handle(context: DivActionHandlingContext, action: DivActionData)
}
@@ -0,0 +1,27 @@
package com.yandex.div.compose.actions
import com.yandex.div.core.annotations.PublicApi
/**
* Handler for actions that DivKit does not handle internally.
*
* Implement this interface to handle application-specific actions.
*
* @see com.yandex.div.compose.DivComposeConfiguration
*/
@PublicApi
interface DivExternalActionHandler {
/**
* Called when an action that does not handled by DivKit is triggered.
*
* DivKit handles actions with `typed` parameter and actions with `url` that starts with
* `div-action:` only.
*/
fun handle(context: DivActionHandlingContext, action: DivActionData) = Unit
/**
* Called when a custom action (action with `"type": "custom"`) is triggered.
*/
fun handleCustomAction(context: DivActionHandlingContext, action: DivCustomActionData) = Unit
}
@@ -19,14 +19,14 @@ internal class ActionHandlerEnvironment {
)
fun createActionHandler(
customActionHandler: DivCustomActionHandler = mock(),
externalActionHandler: DivExternalActionHandler = mock(),
arrayActionsHandler: ArrayActionsHandler = mock(),
dictSetValueActionHandler: DictSetValueActionHandler = mock(),
setVariableActionHandler: SetVariableActionHandler = mock(),
updateStructureActionHandler: UpdateStructureActionHandler = mock()
): DivActionHandler {
return DivActionHandler(
customActionHandler = customActionHandler,
externalActionHandler = externalActionHandler,
reporter = reporter,
arrayActionsHandler = arrayActionsHandler,
dictSetValueActionHandler = dictSetValueActionHandler,
@@ -1,38 +1,73 @@
package com.yandex.div.compose.actions
import androidx.core.net.toUri
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.yandex.div.test.data.action
import com.yandex.div.test.data.customAction
import com.yandex.div2.DivAction
import com.yandex.div2.DivActionCustom
import com.yandex.div2.DivActionTyped
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class DivActionHandlerTest {
private val actionHandlerEnvironment = ActionHandlerEnvironment()
private val customActionHandler = CustomActionHandler()
private val externalActionHandler = TestExternalActionHandler()
private val actionHandler = actionHandlerEnvironment.createActionHandler(
customActionHandler = customActionHandler
externalActionHandler = externalActionHandler
)
@Test
fun `handle custom action`() {
val payload = JSONObject().apply {
put("key", "value")
}
fun `not enabled action is not handled`() {
handle(action(isEnabled = false, typed = customAction()))
assertNull(externalActionHandler.lastCustomAction)
}
@Test
fun `unhandled action is passed to the external action handler`() {
val payload = JSONObject(mapOf("key" to "value"))
handle(
action(
id = "test",
payload = payload,
typed = DivActionTyped.Custom(DivActionCustom())
url = "custom://url"
)
)
assertEquals(payload, customActionHandler.lastAction?.payload)
assertEquals(
DivActionData(
id = "test",
payload = payload,
url = "custom://url".toUri()
),
externalActionHandler.lastAction
)
}
@Test
fun `custom action is passed to the external action handler`() {
val payload = JSONObject(mapOf("key" to "value"))
handle(
action(
id = "test",
payload = payload,
typed = customAction()
)
)
assertEquals(
DivCustomActionData(
id = "test",
payload = payload
),
externalActionHandler.lastCustomAction
)
}
private fun handle(action: DivAction) {
@@ -40,14 +75,21 @@ class DivActionHandlerTest {
}
}
private class CustomActionHandler : DivCustomActionHandler {
private class TestExternalActionHandler : DivExternalActionHandler {
var lastAction: DivActionData? = null
private set
override fun handle(
context: DivActionHandlingContext,
action: DivActionData
) {
var lastCustomAction: DivCustomActionData? = null
private set
override fun handle(context: DivActionHandlingContext, action: DivActionData) {
lastAction = action
}
override fun handleCustomAction(
context: DivActionHandlingContext,
action: DivCustomActionData
) {
lastCustomAction = action
}
}
@@ -47,7 +47,7 @@ sealed class DivUntypedAction {
@JvmStatic
fun parse(uri: Uri): DivUntypedAction? {
if (uri.scheme != "div-action") {
if (!uri.isDivAction) {
return null
}
@@ -111,3 +111,7 @@ sealed class DivUntypedAction {
}
}
}
@InternalApi
val Uri.isDivAction: Boolean
get() = scheme == "div-action"
@@ -5,6 +5,7 @@ import com.yandex.div2.DivAction
import com.yandex.div2.DivActionArrayInsertValue
import com.yandex.div2.DivActionArrayRemoveValue
import com.yandex.div2.DivActionArraySetValue
import com.yandex.div2.DivActionCustom
import com.yandex.div2.DivActionDictSetValue
import com.yandex.div2.DivActionSetVariable
import com.yandex.div2.DivActionTyped
@@ -13,12 +14,15 @@ import com.yandex.div2.DivTypedValue
import org.json.JSONObject
fun action(
typed: DivActionTyped? = null,
id: String = "test",
isEnabled: Boolean = true,
payload: JSONObject? = null,
typed: DivActionTyped? = null,
url: String? = null,
): DivAction {
return DivAction(
logId = constant("test"),
isEnabled = constant(isEnabled),
logId = constant(id),
payload = payload,
typed = typed,
url = url?.let { constant(it.toUri()) }
@@ -65,6 +69,8 @@ fun arraySetValueAction(
)
}
fun customAction(): DivActionTyped = DivActionTyped.Custom(DivActionCustom())
fun dictSetValueAction(
name: String,
key: String,