Android Mobile SDK

Introduction

Arkose Labs' Mobile SDKs let you wrap our solution with Android native function calls. This guarantees seamless integration of your mobile apps with Arkose's full interactive challenges on detection and enforcement and does so without the extended wait times for separate mobile solutions.

This page covers the Mobile SDK for Android. If you are developing in iOS, see the Mobile SDK for iOS page.

The Arkose Mobile SDK:

  • Wraps Arkose's Advanced Enforcement Challenge in native Android OS “web views”.
  • Has 1-to-1 feature availability between web and mobile solutions.
  • Integrates with your apps through native functions.
  • Handles errors through callback events.
  • Complies with Arkose Internal Security guidelines.
  • Complies with Google Play Store guidelines for ease of integration.
  • Is fully compatible with new Enforcement Challenge-API (EC-API) releases.
  • Supports minimum version Android OS 5.0

Mobile SDK High Level Design

Mobile SDK Builds Availability

The Arkose Labs Mobile SDKs are available via the Mobile SDK's Support page. Please talk with your CSM (Customer Success Manager) about your intended usage and to request access.

Compatibility

The Arkose Labs Mobile SDK for Android works with Android 5.0 and up.

All existing detection and challenge features on our web solutions are also available on the Mobile SDKs. All new ones are automatically added; you don't need to update your application every time we release a new Web platform. All challenge updates can be done without updating the SDKs or releasing a new version of your application.

DeviceMinimum OS VersionTarget OS VersionsNote
Android PhoneAndroid 5.0+ (API 21)Android 5, 6, 7, 8, 9, 10, 11, 12, 13, 14+SDK is tested for OS versions 5 through 14+ on emulators and BrowserStack devices
Android TabletAndroid 5.0+ (API 21)Android 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16SDK is tested for OS versions 5.1 through 14+ on emulators and BrowserStack devices
Android TVAndroid 5.1+ (API 22)Android 5, 6, 7, 8, 9, 10, 11, 12, 13, 14+SDK is tested for OS versions 12 through 14+ on emulators
Fire TV/Fire OSFire OS 5+Fire OS 5, 6, 7, 8Fire OS 5 - Based on Android 5.1 Fire OS 6 - Based on Android 7.1 Fire OS 7 - Based on Android 9 Fire OS 8 - Based on Android 11 Vega OS (formerly Kepler OS) - New Linux-based OS that Amazon released Sept 2025 is not supported by us yet. SDK is tested for Fire OS v6.7.1.1 on real TV device.

Technical Requirements

ComponentRequirement
Minimum Android API21 (Android 5.0 Lollipop)
Target Android API34 (Android 14)+
Java Version17
Gradle8.4+
Kotlin1.7.20+ (see the Kotlin-Compose compatibility table below)
Android System WebView39+

Kotlin-Compose Compatibility Reference

Kotlin v1.7.20+ is now supported in our Android XML and Jetpack Compose SDKs, ensuring wider integration capabilities.

Kotlin VersionCompose Compiler Extension Version
1.7.201.3.2
1.8.01.4.0
1.8.101.4.4
1.9.01.5.1
1.9.231.5.11
2.0.0+Use compose plugin

For the most up-to-date Kotlin-Compose compatibility information, refer to the official Jetpack Compose compiler documentation.

Troubleshooting Build Issues

Common Error Messages:

  1. Module was compiled with an incompatible version of Kotlin: Ensure your Kotlin version and Compose compiler version are compatible according to the tables above.
  2. Compose compiler errors: Double-check your kotlinCompilerExtensionVersion matches the recommendations for your Kotlin version.

Security

The Arkose Labs Mobile SDKs are Arkose Labs Security reviewed and comply with Google Play Store guidelines.

Performance

We created the Arkose Labs Mobile SDKs with stability and performance in mind. Their use has no significant impact on the host application’s performance.

Installation

Follow the steps below to set up Arkose Labs Mobile SDK for Android in Android Studio in your host application. This applies to both our detection and enforcement components.

Prerequisites

  • A host Android application. You must be able to build and run this application.
  • For the full end-to-end Arkose setup, you must also complete the standard Arkose Server-Side setup instructions.
📘

This page walks through integration in a Jetpack Compose application. Integrating in an XML/View-based application? Refer to Android SDK Integration in XML-Based Apps. For the full list of configuration parameters and callback listeners, see the Android SDK Common API Reference.

Jetpack Compose Apps

Integrating the Arkose Mobile SDKs into Jetpack Compose apps is straightforward, eliminating the need for extra configuration and allowing developers to incorporate SDK features into composable objects with ease.

For optimal app and SDK performance, it's advised to utilize baseline profiles, which leverage Jetpack Compose's just-in-time (JIT) compilation. This approach, especially when creating baseline profiles for critical user actions like showing the Enforcement Challenge, prompts Android Runtime to optimize execution through ahead-of-time (AOT) compilation, significantly boosting performance. For guidance on crafting a Baseline Profile, consult the Android Documentation.

Steps for integration in a Compose-based application

Dependency Management:

Include the dependencies using the Authenticated Package Manager(APM)

Prerequisites:

  1. You have received the Arkose provided credentials (username and token) from the Arkose Self Service portal.

  2. Store these credentials securely on your CI environment and in your project's local.properties for fetching locally.

  3. Declare Credentials:

    maven {
        url = uri("https://maven.pkg.github.com/ArkoseLabs/alsdk-android-packages")
        credentials{
            username = "TODO: Insert Arkose provided username here"
            password = "TODO: Insert Arkose provided token here"
        }
    }
⚠️

Ensure these credentials are stored securely and not shared outside authorised usage.

  1. Add the SDK Dependency to Your Project:

    • To integrate an SDK package into your project, use the following format for the dependency:

      implementation 'ArkoseLabs.sdk:android:x.xx.xxxxx'
    • To support Compose-based application, add the following additional dependency:

      implementation 'ArkoseLabs.sdk:android-compose:x.xx.xxxxx'
  2. Sync the Project with Gradle Files

    • After adding the dependency, sync your project with the Gradle files to ensure all dependencies are correctly resolved. Use the Sync Project with Gradle Files option in Android Studio or run:

      ./gradlew sync

Legacy Support: Include the dependencies using manual AAR file

  1. Under src/main, create a libs folder.

  2. Copy your .aar files to src/main/libs

    1. ArkoseLabsSDK.aar

    2. ArkoseLabsSDKCompose.aar

      • NOTE: this companion framework file is intended for Jetpack Compose integrations.
  3. In the app level build.gradle file, add this line to its dependencies block.

    implementation fileTree(dir: "libs", include: ["*.aar"])

Additional Dependencies:

The manual-AAR dependency requirements for Compose are the same as for XML: see Additional Dependencies in the XML integration page for the full list and the v2.22.0 migration note. APM/Maven integrations resolve all dependencies automatically.

In addition, the Compose module (ArkoseLabsSDKCompose.aar) requires the Jetpack Compose runtime. Most Compose apps already include these; ensure they are present:

implementation platform('androidx.compose:compose-bom:2024.03.00')
implementation 'androidx.compose.material3:material3'
implementation 'androidx.activity:activity-compose:1.9.0'
implementation 'androidx.core:core-ktx:1.12.0'

ProGuard rules:

Update the Android ProGuard or R8 configuration with the following rule sets:

-dontwarn com.google.android.play.core.integrity.IntegrityManagerFactory
-dontwarn com.google.android.play.core.integrity.StandardIntegrityManager
-dontwarn okhttp3.OkHttpClient$Builder
-dontwarn okhttp3.OkHttpClient
-dontwarn okhttp3.logging.HttpLoggingInterceptor$Level
-dontwarn okhttp3.logging.HttpLoggingInterceptor$Logger
-dontwarn okhttp3.logging.HttpLoggingInterceptor
-dontwarn retrofit2.Converter$Factory
-dontwarn retrofit2.Retrofit$Builder
-dontwarn retrofit2.Retrofit
-dontwarn retrofit2.converter.gson.GsonConverterFactory

In the toolbar, click on File. Then in its menu click Sync Project with Gradle Files.

Import and add Arkose code to your application

  1. Import the necessary SDK classes. Add the below code in your Activity class, before your class definition statements.

    import com.arkoselabs.compose.sdk.ArkoseConfigCompose
    import com.arkoselabs.compose.sdk.EnforcementChallenge
  2. Initialize the mutableStateOf state object to show/hide Enforcement Challenge.

    val showEc = remember { mutableStateOf(false) }
  3. To show the Enforcement Challenge dialog from the onClick lambda function of a Login Button, update the mutableStateOf that controls the visibility of the EnforcementChallenge() composable.

    var sessionResetTime by remember { mutableIntStateOf(0) } //in milliseconds
    ...
    ...
    ...
    when {
                showEc.value -> EnforcementChallenge(
                        onDismiss = {
                            showEc.value = false
                        },
                        sessionResetTime = sessionResetTime, //optional
                        config = ArkoseConfigCompose(
                                apiKey = apiKeyText,
                                enableBackButton = true, //optional
                                noSuppress = true, //optional
                                blobData = "", //optional
                                loading = true, //optional
                                language = "", //optional
                                dismissChallengeOnTouchOutside = false, //optional
                                clientAPIRetryCount = 0, //optional
                                styleTheme = "", //optional
                                timeoutInSecondsUntilReady = 0, //optional
                                accessibilityTextScaleCap = 0 //optional
                        ),
                        onCompleted = {
                            "Arkose Enforcement Challenge onCompleted -> ${it.response}".log()
                        },
                        onFailure = {
                            "Arkose Enforcement Challenge OnFailure -> ${it.response}".log()
                        },
                        onError = {
                            "Arkose Enforcement Challenge OnError -> ${it.response}".log()
                        },
                        onWarning = {
                            "Arkose Enforcement Challenge OnWarning".log()
                        },
                        onResize = {
                            "Arkose Enforcement Challenge OnResize -> ${it.response}".log()
                        },
                        onViewFramePosition = {
                            "Arkose Enforcement Challenge OnViewFramePosition -> ${it.response}".log()
                        },
                        onReady = {
                            "Arkose Enforcement Challenge OnReady".log()
                        },
                        onShow = {
                            "Arkose Enforcement Challenge OnShow".log()
                        },
                        onShown = {
                            "Arkose Enforcement Challenge OnShown-> ${it?.response}".log()
                        },
                        onHide = {
                            "Arkose Enforcement Challenge OnHide-> ${it?.response}".log()
                        },
                        onReset = {
                            "Arkose Enforcement Challenge OnReset".log()
                        },
                        onSuppress = {
                            "Arkose Enforcement Challenge OnSuppress-> ${it?.response}".log()
                        }
                )
            }

Alternative: Embed the Enforcement Challenge inline

  1. To embed the Enforcement Challenge in an Activity, create a new InlineActivity.

    import com.arkoselabs.compose.sdk.ArkoseConfigCompose
    import com.arkoselabs.compose.sdk.EnforcementChallengeInline
    
    class InlineActivity : ComponentActivity() {
  2. Use the inline composable object EnforcementChallengeInline(), which will be tied to the lifecycle of InlineActivity.

    @Composable
    fun InlineScreen(onFinish: () -> Unit) {
    
    val isVisible = remember { mutableStateOf(true) }
    var sessionResetTime by remember { mutableIntStateOf(0) } //in milliseconds
    ...
    ...
    Column {
                    EnforcementChallengeInline(
                        onDismiss = {
                            onFinish()
                        },
                        sessionResetTime = sessionResetTime, //optional
                        isVisible = isVisible,  //optional
                        config = ArkoseConfigCompose(
                            apiKey = AppConfig.API_KEY,
                            enableBackButton = true, //optional
                            noSuppress = false //optional
                        ),
                        onCompleted = {
                            "Arkose Enforcement Challenge onCompleted -> ${it.response}".log()
                        },
                        onFailure = {
                            "Arkose Enforcement Challenge OnFailure -> ${it.response}".log()
                        },
                        onError = {
                            "Arkose Enforcement Challenge OnError -> ${it.response}".log()
                        },
                        onWarning = {
                            "Arkose Enforcement Challenge OnWarning".log()
                        },
                        onResize = {
                            "Arkose Enforcement Challenge OnResize -> ${it.response}".log()
                        },
                        onViewFramePosition = {
                            "Arkose Enforcement Challenge OnViewFramePosition -> ${it.response}".log()
                        },
                        onReady = {
                            "Arkose Enforcement Challenge OnReady".log()
                        },
                        onShow = {
                            "Arkose Enforcement Challenge OnShow".log()
                        },
                        onShown = {
                            "Arkose Enforcement Challenge OnShown".log()
                        },
                        onHide = {
                            "Arkose Enforcement Challenge OnHide".log()
                        },
                        onReset = {
                            "Arkose Enforcement Challenge OnReset".log()
                        },
                        onSuppress = {
                            "Arkose Enforcement Challenge OnSuppress".log()
                        }
                    )
                }
  3. Invoke finish() on InlineActivity when exiting the screen to prevent memory leaks.

    class InlineActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
    
            setContent {
            ...
            ...
            InlineScreen(onFinish = { finish() })
            ...
            ...
            }

Resetting the Arkose Session

Arkose sessions in Jetpack Compose can be reset dynamically by modifying the properties of a mutable ArkoseConfigCompose instance. Since Jetpack Compose automatically recomposes when a state changes, updating the config will automatically reset the session.

Create a Mutable ArkoseConfig Instance

Customers should first initialize a mutable ArkoseConfigCompose instance using remember:

// Create a mutable ArkoseConfigCompose instance
var arkoseConfig by remember {
    mutableStateOf(
        ArkoseConfigCompose(
            apiKey = AppConfig.API_KEY,
            enableBackButton = true, // Optional
            noSuppress = false // Optional
        )
    )
}

Pass the Mutable Config to EnforcementChallengeInline

The mutable arkoseConfig should be passed to EnforcementChallengeInline, allowing it to automatically detect and apply any updates.

EnforcementChallengeInline(
    onDismiss = {
        closeButtonState.value = false
        onFinish()
    },
    sessionResetTime = sessionResetTime.intValue,
    isVisible = isVisible,
    config = arkoseConfig // Pass the mutable ArkoseConfig instance
    .
    .
    .
)

Reset the Session by Updating arkoseConfig

Whenever the customer wants to reset the session, they only need to update arkoseConfig properties. The Composable function will automatically apply the new configuration and reset the session.

// Update the arkoseConfig properties to reset the session
arkoseConfig = ArkoseConfigCompose(
    language = "<YOUR_PREFERRED_LANGUAGE>", // Optional: Set preferred language
    styleTheme = "<YOUR_PREFERRED_STYLE_THEME>", // Optional: Set EC style theme
    blobData = "<YOUR_ENCRYPTED_DATA>" // Optional: Encrypted blob data
)

Example Use Case

Here’s an example of resetting the session when a user clicks a button:

Button(onClick = {
    arkoseConfig = ArkoseConfigCompose(
        blobData = "updated_blob_data"
    )
}) {
    Text("Reset Session")
}

Implement Preloading of Challenges to onReady with On-Demand Presentation

Overview:

The inlineRunOnTrigger configuration enables preloading of the enforcement challenge to the onReady state in Jetpack Compose inline mode, giving your application full control over when the challenge is presented to the user. This improves user-perceived latency by warming up the WebView ahead of time. Available from Android SDK v2.20.0 (Jetpack Compose inline mode only).

Configuration:

  1. Set inlineRunOnTrigger = true in your ArkoseConfigCompose object (default: false)
  2. Use EnforcementChallengeInline as the composable. This is the supported format for preloading in Compose
  3. Resume the challenge flow by calling ArkoseManagerCompose.runTriggeredInline() when the challenge should be presented
⚠️

Important Notes:

  • Only supported with EnforcementChallengeInline. Do NOT use with EnforcementChallenge (dialog/overlay mode)
  • Do NOT enable loading = true when using this feature, as it will cause the loader to display indefinitely
  • ArkoseManagerCompose.runTriggeredInline() should only be called after onReady has fired

Implementation Example:

class InlineActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Set log level
        ArkoseManager.setLogLevel(ArkoseManager.INFO)

        setContent {
            AppTheme {
                InlineScreen(onFinish = { finish() })
            }
        }
    }
}

@Composable
fun InlineScreen(onFinish: () -> Unit) {
    val isVisible = remember { mutableStateOf(true) }
    val isOnReadyCalled = remember { mutableStateOf(false) }

    // Step 1: Configure ArkoseConfigCompose with inlineRunOnTrigger enabled
    var arkoseConfig by remember {
        mutableStateOf(
            ArkoseConfigCompose(
                apiKey = "<YOUR_PUBLIC_KEY>",
                enableBackButton = true,   // optional
                noSuppress = false,        // optional
                inlineRunOnTrigger = true  // enables preloading
            )
        )
    }

    Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {

        // Step 2: Add EnforcementChallengeInline. This preloads the challenge on composition
        EnforcementChallengeInline(
            onDismiss = {
                onFinish()
            },
            isVisible = isVisible,
            config = arkoseConfig,
            onCompleted = {
                "Arkose Enforcement Challenge onCompleted -> ${it.response}".log()
            },
            onFailure = {
                "Arkose Enforcement Challenge OnFailure -> ${it.response}".log()
            },
            onError = {
                "Arkose Enforcement Challenge OnError -> ${it.response}".log()
            },
            onWarning = {
                "Arkose Enforcement Challenge OnWarning".log()
            },
            onReady = {
                // Step 3: Mark onReady received. The challenge is preloaded and ready to present
                isOnReadyCalled.value = true
                "Arkose Enforcement Challenge OnReady".log()
            },
            onShown = {
                "Arkose Enforcement Challenge OnShown".log()
            },
            onHide = {
                "Arkose Enforcement Challenge OnHide".log()
            },
            onReset = {
                "Arkose Enforcement Challenge OnReset".log()
            },
            onSuppress = {
                "Arkose Enforcement Challenge OnSuppress".log()
            }
        )

        Button(
            onClick = {
                // Step 4: Trigger the preloaded challenge only after onReady (e.g. on button click)
                if (isOnReadyCalled.value) {
                    ArkoseManagerCompose.runTriggeredInline()
                }
            },
            modifier = Modifier.fillMaxWidth().padding(top = 10.dp)
        ) {
            Text("Show Challenge")
        }
    }
}

Build the revised project

  1. Go to the Build menu and click on Clean Project.
  2. Go to the Build menu and click on Rebuild Project.

Run and test the app

  1. Run your Android application.

  2. If running Arkose enforcement component, the view shows an Arkose Labs Enforcement Challenge.

    • If running Arkose detection component, the view runs Arkose detection component.
  3. If running Arkose enforcement component, verify the challenge.

  4. When the verification or detection is successful, the onCompleted event returns a response token.

📘

The Compose samples above show the token arriving in the onCompleted lambda (it.response). For the equivalent listener-based sample in an XML-based integration, see Retrieve the response token. The ArkoseConfig configuration objects, callback listeners, and Enforcement Challenge configuration parameters are documented in the Android SDK Common API Reference.

Programmatic Enforcement Challenge Dismissal

If your application flow requires force dismissal of the Enforcement Challenge (such as during navigation changes), programmatic dismissal can be implemented as follows

@Composable
fun MainScreen() {
    var showEc by remember { mutableStateOf(false) } // a state variable to control the visibility of the challenge

    Button(
        onClick = {
            showEc = false // To dismiss the EC
        }
    ) {
        Text("Go to Details Page")
    }

    when {
        showEc -> EnforcementChallenge(
            config = arkoseConfig,
            // Add callback handlers as needed...
        )
    }
}
📘

For the XML-based and inline Fragment implementations, refer to Programmatic Enforcement Challenge Dismissal on the XML integration page.

In-SDK Cancel / Reset Buttons

Available from Android SDK v2.22.0.

Overview:

The SDK can render an optional action-button strip at the bottom of the Enforcement Challenge modal (matching the Apple SDK), giving the user two built-in controls:

  • Cancel: closes (dismisses) the current challenge.
  • Reset: restarts the challenge session, optionally with an updated configuration.

Each button is configured with an ArkoseActionConfig. A button is shown only when its config is supplied; passing null hides that button. When both are supplied, the on-screen layout is always Reset on the left, Cancel on the right, with a divider between them (identical on XML and Compose).

📘

This feature is modal-only. It applies to the dialog presentation (showEnforcementChallenge(...) / the EnforcementChallenge composable). It is not available for inline presentations (createEnforcementChallenge and EnforcementChallengeInline). These user-facing buttons are distinct from the programmatic session reset (updateConfig(...)) and force-dismissal described in the sections above.

ArkoseActionConfig

Constructor / MethodDescription
ArkoseActionConfig(String title)Creates a config with a visible button label only. title is the user-facing label (e.g. "Cancel" / "Reset"). Application can set the localised text.
ArkoseActionConfig(String title, String accessibilityLabel)Also sets an accessibilityLabel, which is applied as the button's contentDescription and read by TalkBack. When set, it becomes the button's entire spoken announcement, replacing the visible title (it is not additive), so include the button's purpose in the label itself.
String getTitle()Returns the button label.
String getAccessibilityLabel()Returns the accessibility label, or null if none was provided (the visible title is then the accessible name).
📘

Integrating in an XML/View-based application? Refer to In-SDK Cancel / Reset Buttons on the XML integration page for the XML usage example and the reset-with-same/new-configuration instructions.

Usage

The EnforcementChallenge composable exposes the buttons and reset callbacks as (named) parameters:

// e.g.
// The config MUST be Compose state so a reset-with-new-config is observed:
// var arkoseConfig by remember { mutableStateOf(ArkoseConfigCompose(apiKey = apiKeyText)) }
EnforcementChallenge(
    config = arkoseConfig,
    cancelActionConfig = ArkoseActionConfig(
        "Cancel", "Closes the challenge and returns to the previous screen"
    ),
    resetActionConfig = ArkoseActionConfig(
        "Reset", "Restarts the challenge"
    ),
    onReset = {
        "Arkose Enforcement Challenge onReset".log()
    },
    onPrepareForReset = { completion ->
        // Optional: runs before the reset when the user taps Reset.
        // To reset with a NEW config, reassign the config state here; omit to reuse it.
        // e.g.
        arkoseConfig = ArkoseConfigCompose(
            apiKey = apiKeyText,
            language = languageText,
        )
        completion() // ALWAYS call exactly once. The SDK resets once with the current config
    },
    onHide = {
        // Invoked when the user taps Cancel (challenge dismissed).
    },
    onReady = {
        "Arkose Enforcement Challenge onReady".log()
    },
    // ... other callbacks
)

onPrepareForReset defaults to { it() } (continue immediately). cancelActionConfig / resetActionConfig default to null (button hidden).

📘

Compose config update: To reset with a new config, the config must be Compose state (e.g. var arkoseConfig by remember { mutableStateOf(...) }); reassign it inside onPrepareForReset (or leave it unchanged), then call completion() exactly once. The SDK resets a single time with whatever config is current; there is no double reload. See Resetting with the same configuration.

Reset loader (showLoaderOnReset)

During a Reset, the SDK briefly covers the challenge while it reloads. The showLoaderOnReset configuration controls whether a loading spinner is shown over that cover. It defaults to true.

  • true: a loading spinner is shown over the challenge while it reloads.
  • false: the spinner is suppressed, but the challenge is still briefly covered to block interaction during the reload (it never exposes a half-loaded challenge).
// XML
ArkoseConfig config = new ArkoseConfig.Builder()
        .apiKey(<YOUR_PUBLIC_KEY>)
        .showLoaderOnReset(false)   // default is true
        .build();
// Compose
val config = ArkoseConfigCompose(
    apiKey = apiKeyText,
    showLoaderOnReset = false,      // default is true
)
📘

The loader overlays the puzzle in place. It is not a separate screen.

Theming

The Cancel / Reset button strip, the reset cover, and the reset spinner inherit the host app's Material palette (surface / primary / outline colors), in both light and dark mode. Where your theme defines a Material attribute the SDK uses it; where it doesn't, the SDK fills in a Material 3 baseline. No configuration is required, and the behaviour is identical on XML and Compose. If your app supports dark mode, the reset UI follows it automatically.

Each element resolves its color from your app's theme. Set these in your theme to control the strip's appearance:

ElementXML (theme attribute)Compose (MaterialTheme.colorScheme)
Button strip backgroundcolorSurfacesurface
Cancel / Reset button textcolorPrimaryprimary
Dividers (above the strip and between the buttons)colorOutlineoutline
Reset cover backgroundcolorSurfacesurface
Reset spinnercolorPrimaryprimary

Where to set them:

  • XML: define the attributes in your app theme (themes.xml), with values-night/ variants for dark mode. Material 3 and Material 2 themes define colorSurface and colorOutline; AppCompat-only themes define colorPrimary (button text and spinner follow it), while the surface and outline fall back to the SDK's Material 3 baseline.
<style name="Theme.MyApp" parent="Theme.Material3.DayNight">
    <item name="colorSurface">@color/my_surface</item>
    <item name="colorPrimary">@color/my_primary</item>
    <item name="colorOutline">@color/my_outline</item>
</style>
  • Compose: provide the roles in the MaterialTheme your app wraps the challenge in:
MaterialTheme(
    colorScheme = lightColorScheme(
        surface = MySurface,
        primary = MyPrimary,
        outline = MyOutline,
    )
) { /* your app content, including EnforcementChallenge */ }

Any role your theme does not define falls back to the Material 3 baseline (DayNight-aware), so partial theming is safe.

⚠️

Important Notes:

  • The Cancel / Reset button strip is modal-only. It renders with showEnforcementChallenge(...) (XML) and EnforcementChallenge (Compose). It is not applicable for the createEnforcementChallenge and EnforcementChallengeInline api.
  • Passing null for cancelActionConfig or resetActionConfig hides that button. Passing null for both shows no strip. Layout is always Reset (left) / Cancel (right).
  • XML and Compose takes the configs positionally (Cancel first, Reset second).
  • onPrepareForReset is optional (default: proceed immediately). Use it to run async work, or to swap the configuration (XML: updateConfig(...); Compose: reassign the config state). There is no timeout. The SDK waits for completion(), so you must call it exactly once, or the reset won't proceed. While it waits, Reset is disabled (loader shown) and Cancel/back stay available; completion() may be called from any thread.
  • Tapping Cancel dismisses the challenge: onHide is invoked if the challenge was already shown; onError (with a dismiss reason) is invoked if it is dismissed before it becomes visible.

Dismissing the Challenge on Background Transition

The following code snippet demonstrates how to dismiss the enforcement challenge when your application transitions to the background state.

In below code snippet, when the app moves to the background (onPause), a shared state flag (shouldDismissEC) is set to true, triggering the LaunchedEffect to dismiss any active Enforcement Challenge. The flag (shouldDismissEC) resets to false when the app returns to the foreground (onResume).

class MainActivity : ComponentActivity() {
    companion object {
        val shouldDismissEC = mutableStateOf(false) // a shared state variable to control dismissal of the challenge when app goes to background
    }

    override fun onPause() {
        super.onPause()
        shouldDismissEC.value = true
    }

    override fun onResume() {
        super.onResume()
        shouldDismissEC.value = false
    }
}

@Composable
fun MainScreen() {
    var showEc by remember { mutableStateOf(false) } // a state variable to control the visibility of the challenge

    // Monitor activity lifecycle
    LaunchedEffect(MainActivity.shouldDismissEC.value) {
        if (MainActivity.shouldDismissEC.value && showEc) {
            showEc = false
        }
    }

    // Other UI and challenge setup...
}
📘

For the XML-based and inline Fragment implementations, refer to Dismissing the Challenge on Background Transition on the XML integration page.

📘

To capture SDK logs from a device or emulator, see Android SDK Logging and Troubleshooting.