Brand Concierge Implementation Guide (Android)

The Brand Concierge extension embeds a conversational chat UI into your host app. It uses AEP SDK shared state from Mobile Core, Edge, and Edge Identity to configure and run a session.

The Brand Concierge UI has two integration approaches:

Both approaches are available for Compose and XML/Views-based apps.

Prerequisites

Required SDK modules

Your app needs the following Experience Platform SDKs to be available and registered:

Android version

Permissions for speech to text (optional)

Speech to text uses Android Speech Recognition APIs and microphone APIs for voice input functionality. Add this permission to your app AndroidManifest.xml:

<uses-permission android:name="android.permission.RECORD_AUDIO" />

The SDK handles permission requests internally when users interact with the microphone button.

Installation

Add the dependencies to your app module's build.gradle.kts:

dependencies {
    implementation("com.adobe.marketing.mobile:concierge:3.+")
    implementation("com.adobe.marketing.mobile:core:3.5.0")
    implementation("com.adobe.marketing.mobile:edgeidentity:3.0.0")
}

Then sync your project with the Gradle files.

Configuration

Step 1: Register the Brand Concierge extension

Import and register the extensions in your Application class onCreate():

import com.adobe.marketing.mobile.MobileCore
import com.adobe.marketing.mobile.Concierge
import com.adobe.marketing.mobile.edge.identity.Identity as EdgeIdentity
import android.app.Application

class MainApp : Application() {
    override fun onCreate() {
        super.onCreate()
        MobileCore.setApplication(this)
        MobileCore.initialize(this, "YOUR_APP_ID")
    }
}

Replace YOUR_APP_ID with your mobile property App ID from Adobe Data Collection. For full setup instructions see the Adobe Experience Platform Mobile SDK getting started guide.

Step 2: Validate the Brand Concierge configuration keys exist

If you set the Adobe Experience Platform SDK log level to trace:

MobileCore.setLogLevel(LoggingMode.VERBOSE)

you can then inspect the app logs to confirm that extension shared states are being set with the expected values.

Brand Concierge expects the following keys to be present in the Configuration shared state:

The ECID is read from the Edge Identity shared state.

Another option for validation is to use Adobe Assurance. Refer to the Mobile SDK validation guide for more information.

Integration

Managed Integration

Use this when you want to provide a drop-in entry point to the chat interface and let the Brand Concierge extension automatically manage it.

This mode:

Jetpack Compose

The ConciergeChat composable can be configured with a UI element (button, floating action button, or any custom element) of your choice to act as a trigger to launch the chat interface. Pass the list of surface URLs via surfaces.

@Composable
fun MyScreen() {
    val viewModel = viewModel<ConciergeChatViewModel>()
    val surfaces = listOf("web://example.com/your-surface.html")
    
    // Your app content
    // ... other views ...
    
    ConciergeChat(
        viewModel = viewModel,
        surfaces = surfaces
    ) { showChat ->
        // Your trigger button/view that launches ConciergeChat
        MyTriggerButton(onClick = { showChat() }) 
    }
}

XML/Views

For non-Compose apps, the SDK provides ConciergeChatView that wraps the Compose chat UI and can be included in XML layouts.

Note: The activity hosting ConciergeChatView must have android:windowSoftInputMode="adjustResize" added in the app's AndroidManifest.xml to ensure the chat input field remains visible when the keyboard is shown:

<activity
    android:name=".YourActivity"
    android:windowSoftInputMode="adjustResize" />

Step 1: Add the view to your XML layout

<com.adobe.marketing.mobile.concierge.ui.chat.ConciergeChatView
    android:id="@+id/concierge_chat"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

Step 2: Bind with a trigger view in your Activity/Fragment

class XmlActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // Load optional theme
        val theme = ConciergeThemeLoader.load(this, "myTheme.json")
        
        // Create a trigger button of your choice
        val triggerButton = Button(this).apply {
            text = "Start Chat"
            textSize = 18f
            setPadding(32, 16, 32, 16)
        }
        
        // Obtain the ConciergeChatView and bind the triggerButton
        val chatView = findViewById<ConciergeChatView>(R.id.concierge_chat)
        val surfaces = listOf("web://example.com/your-surface.html")
        chatView.bind(
            lifecycleOwner = this,
            viewModelStoreOwner = this,
            surfaces = surfaces,
            theme = theme,  // Optional: apply custom theme
            triggerView = triggerButton
        )
    }
}

Custom Integration

Use this when you want to embed the chat interface directly into your app's view hierarchy for more flexibility. This is useful for dedicated chat screens or custom layouts.

In this mode, you should:

Jetpack Compose

Set surfaces via surfaces and ensure the extension is ready (configuration, ECID, and surfaces) before showing the chat.

@Composable
fun YourChatScreen() {
    val viewModel = viewModel<ConciergeChatViewModel>()
    val conciergeState by ConciergeStateRepository.instance.state.collectAsStateWithLifecycle()
    val surfaces = listOf("web://example.com/your-surface.html")
    // Set surfaces so they are available for the chat session
    ConciergeStateRepository.instance.setSessionSurfaces(surfaces)
    val ready = conciergeState.configurationReady &&
        conciergeState.experienceCloudId != null &&
        conciergeState.surfaces.isNotEmpty()
    
    if (ready) {
        ConciergeChat(
            viewModel = viewModel,
            onClose = {
                // your logic on close or back press
            }
        )
    } else {
        // Show your intermediate loading state or wait for SDK to be ready
    }
}

XML/Views

Note: The activity hosting ConciergeChatView must have android:windowSoftInputMode="adjustResize" added in the app's AndroidManifest.xml to ensure the chat input field remains visible when the keyboard is shown:

<activity
    android:name=".YourActivity"
    android:windowSoftInputMode="adjustResize" />

Step 1: Add the view to your XML layout

<com.adobe.marketing.mobile.concierge.ui.chat.ConciergeChatView
    android:id="@+id/concierge_chat"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Step 2: Bind in your Activity/Fragment

class XmlActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // Load optional theme
        val theme = ConciergeThemeLoader.load(this, "myTheme.json")
        
        // Obtain the chat view and bind
        val chatView = findViewById<ConciergeChatView>(R.id.concierge_chat)
        val surfaces = listOf("web://example.com/your-surface.html")
        chatView.bind(
            lifecycleOwner = this,
            viewModelStoreOwner = this,
            surfaces = surfaces,
            theme = theme,  // Optional: apply custom theme
            onClose = { finish() }
        )
    }
}

Theme Customization

The Brand Concierge chat interface can be customized by loading the theme file from the assets directory of your app using ConciergeThemeLoader.

@Composable
fun MyScreen() {
    val context = LocalContext.current

    val theme = remember {
        ConciergeThemeLoader.load(context, "myTheme.json")
            ?: ConciergeThemeLoader.default()
    }
    
    ConciergeTheme(theme = theme) {
        ConciergeChat(/* ... */)
    }
}

More information regarding theme customization can be found in the Style guide (Android).

Required manifest entries

1. Register your app as an App Link handler (all API levels)

Add an <intent-filter> with android:autoVerify="true" to the activity in your AndroidManifest.xml that should handle your domain's URLs. This triggers Android's domain verification against your domain's assetlinks.json file, which is what makes your app the verified handler. Without this, the Concierge extension's App Link check will always fall back to the in-app WebView.

<activity android:name=".YourActivity" ...>
    <intent-filter android:autoVerify="true" android:exported="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" android:host="yourdomain.com" />
    </intent-filter>
</activity>

2. Package visibility (Android 11 and higher)

Add the following <queries> block to your AndroidManifest.xml. Without it, the Concierge extension cannot use PackageManager.resolveActivity() to detect the App Link handler on API 30 or higher, and App Links will silently fall back to the in-app WebView.

<!-- Required for PackageManager.resolveActivity() on Android 11+ to detect
     which app handles VIEW intents for http/https URLs. -->
<queries>
    <intent>
        <action android:name="android.intent.action.VIEW" />
        <data android:scheme="https" />
    </intent>
    <intent>
        <action android:name="android.intent.action.VIEW" />
        <data android:scheme="http" />
    </intent>
</queries>

The Concierge extension automatically opens links when your app is the verified handler for the URL's domain. If your app is not the handler, the link opens in an in-app WebView overlay.

Default link handling flow: handleLink callback (if provided) → App Link check → WebView overlay.

To customize this behavior, provide a handleLink callback. Return true if your app handled the link; return false to fall back to the default behavior (App Link check, then WebView overlay).

Compose (ConciergeChat):

val context = LocalContext.current
ConciergeChat(
    viewModel = viewModel,
    onClose = { finish() },
    handleLink = { url ->
        try {
            val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
            context.startActivity(intent)
            true  // Handled
        } catch (e: ActivityNotFoundException) {
            false  // Fall back to WebView overlay
        }
    }
)

XML (ConciergeChatView):

chatView.bind(
    lifecycleOwner = this,
    viewModelStoreOwner = this,
    surfaces = surfaces,
    theme = theme,
    handleLink = { url ->
        try {
            val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
            startActivity(intent)
            true
        } catch (e: ActivityNotFoundException) {
            false
        }
    },
    onClose = { finish() }
)

To close the chat when a deep link is tapped, call viewModel.closeConcierge() inside your handleLink callback before returning true:

ConciergeChat(
    viewModel = viewModel,
    handleLink = { url ->
        if (url.startsWith("myapp://")) {
            viewModel.closeConcierge()
            // navigate to the deeplink destination
            true
        } else {
            false
        }
    }
) { showChat -> ... }

Links clicked inside the in-app WebView overlay (for example, links on a product page) follow their own routing rules, independent of the handleLink callback:

No additional configuration is required for this behavior. App Link forwarding within the WebView uses the same domain verification as chat message link handling.

Next steps