Skip to content

AI Implementation Guide - Android

How to use this page: Copy everything below the horizontal line and paste it into your AI coding assistant (Claude, ChatGPT, GitHub Copilot, Cursor, etc.) to get accurate MovableInk SDK implementation help.

Sample Prompts

After pasting the context below into your AI assistant, try these prompts:

  • "Implement MovableInk deeplinking in my Android app using the context I provided"
  • "Help me configure App Links for MovableInk"
  • "Implement behavior events to capture when users view products"
  • "Add orderCompleted event tracking to my checkout flow"
  • "Help me test my MovableInk integration"
  • "Debug why my MovableInk deeplinks aren't working"
  • "Convert this Java implementation to Kotlin"

Context for AI Assistant

Copy everything inside the code block below and paste into your AI assistant:

# MovableInk Android SDK Implementation Reference

## Overview

### What is MovableInk?
MovableInk is a marketing personalization platform that helps brands create dynamic, personalized content for email, mobile, and web. The Android SDK enables two key capabilities:

1. **Deeplinking**: Users tap links in marketing emails → your app opens to specific content
2. **Behavior Events**: Capture user interactions (product views, purchases) to power personalization

### SDK Information
- **Current Version**: 2.3.0
- **Minimum Android**: Android 7.0 (API 24 / Nougat)
- **Language**: Kotlin (Java compatible)
- **Package**: `com.movableink.sdk:inked`
- **GitHub**: https://github.com/movableink/android-sdk

## Key Terminology

- **MIU (MovableInk User ID)**: Unique, non-PII identifier linking app users to marketing profiles (typically a UUID, NOT an email)
- **MovableInk Link**: URL format `https://mi.yourcompany.com/p/...` in marketing emails
- **App Links**: Android deep linking using https URLs (requires Digital Asset Links file)
- **assetlinks.json**: Digital Asset Links file at `https://mi.yourcompany.com/.well-known/assetlinks.json` (configured by MovableInk team)
- **Intent Filter**: AndroidManifest.xml configuration that allows your app to handle specific URL patterns

## Installation

Add the MovableInk SDK to your app's `build.gradle` file:

```kotlin
dependencies {
    implementation 'com.movableink.sdk:inked:2.3.0'
}
```

Then sync your project.

## Configuration for Deeplinking

### Step 1: Add Intent Filters to AndroidManifest.xml

Add these intent filters to your main activity in `AndroidManifest.xml`:

```xml
<activity
    android:name=".MainActivity"
    android:exported="true">

    <!-- Existing intent filters -->
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

    <!-- Add these intent filters for MovableInk App Links -->
    <intent-filter android:autoVerify="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="mi.yourcompany.com"
            android:pathPrefix="/p/cpm" />
    </intent-filter>

    <intent-filter>
        <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="mi.yourcompany.com"
            android:pathPrefix="/p/rpm" />
    </intent-filter>

    <intent-filter>
        <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="mi.yourcompany.com"
            android:pathPrefix="/p/gom" />
    </intent-filter>
</activity>
```

**Important**: 
- Replace `mi.yourcompany.com` with actual MovableInk subdomain
- `android:autoVerify="true"` is required for App Links verification
- `android:exported="true"` is required for Android 12+

## Deeplinking Implementation

### Kotlin Implementation

```kotlin
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.movableink.sdk.MIClient

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Handle intent
        handleIntent(intent)
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        handleIntent(intent)
    }

    private fun handleIntent(intent: Intent?) {
        val action = intent?.action
        val data: Uri? = intent?.data

        if (action == Intent.ACTION_VIEW && data != null) {
            // This is a deeplink
            resolveMovableInkLink(data)
        }
    }

    private fun resolveMovableInkLink(url: Uri) {
        // Asynchronous approach (recommended)
        MIClient.resolveUrlAsync(url.toString()) { resolvedUrl ->
            if (resolvedUrl != null) {
                // Navigate to the resolved URL in your app
                println("Deeplink resolved to: $resolvedUrl")
                handleDeeplink(resolvedUrl)
            } else {
                println("Failed to resolve link")
            }
        }
    }

    private fun handleDeeplink(url: String) {
        // Parse URL and navigate to appropriate screen
        val uri = Uri.parse(url)

        when {
            uri.path?.contains("/products/") == true -> {
                val productId = uri.lastPathSegment
                // Navigate to product detail
            }
            uri.path?.contains("/categories/") == true -> {
                // Navigate to category screen
            }
            else -> {
                // Handle other URLs or fallback
            }
        }
    }
}
```

### Kotlin with Coroutines (Synchronous)

```kotlin
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.movableink.sdk.MIClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        handleIntent(intent)
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        handleIntent(intent)
    }

    private fun handleIntent(intent: Intent?) {
        val action = intent?.action
        val data: Uri? = intent?.data

        if (action == Intent.ACTION_VIEW && data != null) {
            resolveMovableInkLink(data)
        }
    }

    private fun resolveMovableInkLink(url: Uri) {
        lifecycleScope.launch {
            val resolvedUrl = withContext(Dispatchers.IO) {
                MIClient.resolveUrl(url.toString())
            }

            resolvedUrl?.let {
                println("Deeplink resolved to: $it")
                handleDeeplink(it)
            }
        }
    }

    private fun handleDeeplink(url: String) {
        // Navigate to appropriate screen based on URL
    }
}
```

### Java Implementation

```java
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import com.movableink.sdk.MIClient;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        handleIntent(getIntent());
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        handleIntent(intent);
    }

    private void handleIntent(Intent intent) {
        String action = intent.getAction();
        Uri data = intent.getData();

        if (Intent.ACTION_VIEW.equals(action) && data != null) {
            resolveMovableInkLink(data);
        }
    }

    private void resolveMovableInkLink(Uri url) {
        MIClient.resolveUrlAsync(url.toString(), resolvedUrl -> {
            if (resolvedUrl != null) {
                System.out.println("Deeplink resolved to: " + resolvedUrl);
                handleDeeplink(resolvedUrl);
            } else {
                System.out.println("Failed to resolve link");
            }
            return null;
        });
    }

    private void handleDeeplink(String url) {
        // Parse the URL and navigate to the appropriate screen
        Uri uri = Uri.parse(url);

        if (uri.getPath() != null && uri.getPath().contains("/products/")) {
            String productId = uri.getLastPathSegment();
            // Navigate to product detail
        } else if (uri.getPath() != null && uri.getPath().contains("/categories/")) {
            // Navigate to category screen
        }
    }
}
```

## Behavior Events Implementation (Optional)

### Setup

#### Step 1: Add API Key to local.properties

In your project's `local.properties` file:

```properties
MOVABLE_INK_SDK_API_KEY=YOUR_API_KEY
```

#### Step 2: Update AndroidManifest.xml

Add this meta-data tag within the `<application>` tag:

```xml
<application>
    <!-- Other configuration -->

    <meta-data
        android:name="com.movableink.inked.API_KEY"
        android:value="${MOVABLE_INK_SDK_API_KEY}" />
</application>
```

#### Step 3: Update build.gradle

In your app-level `build.gradle` (or `build.gradle.kts`):

```kotlin
android {
    defaultConfig {
        // Other configuration

        // Load API key from local.properties
        manifestPlaceholders["MOVABLE_INK_SDK_API_KEY"] = 
            project.properties['MOVABLE_INK_SDK_API_KEY'] ?: ""
    }
}
```

#### Step 4: Configure Region (Optional)

For EU compliance or specific regional requirements:

```kotlin
android {
    defaultConfig {
        // Set your region (default is US, use EU for European clients)
        buildConfigField("String", "MOVABLE_INK_SDK_REGION", "\"EU\"")
    }
}
```

**Important**: If you don't specify a region, the SDK defaults to US.

### Set User ID (MIU)

Set the MIU as soon as user is identified (typically after login):

```kotlin
MIClient.setMIU("USER_UNIQUE_ID")
```

**Important**: 
- MIU must be non-PII (use UUID, NOT email address)
- Must match what marketing team uses in email campaigns
- Should be URL-friendly string

### Product Searched

```kotlin
MIClient.productSearched(
    ProductSearched(
        query = "running shoes",
        url = "https://yourapp.com/search?q=running+shoes",
        meta = mapOf(
            "resultsCount" to 42
        )
    )
)
```

### Product Viewed

```kotlin
MIClient.productViewed(
    ProductProperties(
        id = "PROD-12345",
        title = "Running Shoes",
        price = "89.99",
        currency = Currency.USD,
        url = "https://yourapp.com/products/12345",
        categories = listOf(
            Category(
                id = "shoes",
                title = "Shoes",
                url = "https://yourapp.com/categories/shoes"
            ),
            Category(
                id = "running",
                title = "Running",
                url = "https://yourapp.com/categories/running"
            )
        ),
        meta = mapOf(
            "inStock" to true,
            "rating" to 4.5
        )
    )
)
```

**Important**: Product ID must not be empty string

### Product Added (to Cart)

```kotlin
MIClient.productAdded(
    ProductProperties(
        id = "PROD-12345",
        title = "Running Shoes",
        price = "89.99",
        currency = Currency.USD,
        url = "https://yourapp.com/products/12345",
        categories = listOf(
            Category(id = "shoes"),
            Category(id = "running")
        ),
        meta = mapOf(
            "size" to "10",
            "color" to "blue"
        )
    )
)
```

### Product Removed (from Cart)

```kotlin
MIClient.productRemoved(
    ProductProperties(
        id = "PROD-12345",
        title = "Running Shoes",
        price = "89.99",
        currency = Currency.USD,
        url = "https://yourapp.com/products/12345"
    )
)
```

### Category Viewed

```kotlin
MIClient.categoryViewed(
    Category(
        id = "shoes",
        title = "Shoes",
        url = "https://yourapp.com/categories/shoes",
        meta = mapOf(
            "itemCount" to 156
        )
    )
)
```

### Order Completed

```kotlin
MIClient.orderCompleted(
    OrderCompletedProperties(
        id = "ORDER-789",
        revenue = "179.98",
        currency = Currency.USD,
        orderProducts = listOf(
            OrderProduct(
                id = "PROD-12345",
                title = "Running Shoes",
                url = "https://yourapp.com/products/12345",
                price = "89.99",
                quantity = 2,
                meta = mapOf(
                    "size" to "10",
                    "color" to "blue"
                )
            )
        )
    )
)
```

**Important**: 
- Order ID and Product IDs must not be empty strings
- Revenue and price should be in dollars and cents (e.g., "15.99")
- Can include up to 100 products per order

### Currency Support

Available currency values:
- `Currency.USD`, `Currency.EUR`, `Currency.GBP`, `Currency.CAD`, `Currency.AUD`, `Currency.JPY`, etc.
- Default is `Currency.USD` if not specified
- Introduced in SDK v2

### Java Examples

#### Product Viewed (Java)

```java
import com.movableink.sdk.MIClient;
import com.movableink.sdk.ProductProperties;
import com.movableink.sdk.Category;
import com.movableink.sdk.Currency;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

// Product Viewed
Map<String, Object> meta = new HashMap<>();
meta.put("inStock", true);
meta.put("rating", 4.5);

ProductProperties properties = new ProductProperties(
    "PROD-12345",
    "Running Shoes",
    "89.99",
    Currency.USD,
    "https://yourapp.com/products/12345",
    Arrays.asList(
        new Category("shoes", "Shoes", "https://yourapp.com/categories/shoes"),
        new Category("running", "Running", "https://yourapp.com/categories/running")
    ),
    meta
);

MIClient.productViewed(properties);
```

#### Order Completed (Java)

```java
import com.movableink.sdk.OrderCompletedProperties;
import com.movableink.sdk.OrderProduct;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

Map<String, Object> productMeta = new HashMap<>();
productMeta.put("size", "10");
productMeta.put("color", "blue");

OrderProduct product = new OrderProduct(
    "PROD-12345",
    "Running Shoes",
    "https://yourapp.com/products/12345",
    "89.99",
    2,
    productMeta
);

OrderCompletedProperties orderProperties = new OrderCompletedProperties(
    "ORDER-789",
    "179.98",
    Currency.USD,
    Collections.singletonList(product)
);

MIClient.orderCompleted(orderProperties);
```

## Testing

### Test Deeplinking

1. Build and run your app on device or emulator (API 24+)
2. Put app in background or close it
3. Open a MovableInk test link in Chrome: `https://mi.yourcompany.com/p/rp/test`
4. App should open automatically
5. Check Logcat for: `Deeplink resolved to: <final URL>`

### Test with ADB

You can also test using ADB:

```bash
adb shell am start -a android.intent.action.VIEW \
  -d "https://mi.yourcompany.com/p/rp/test" \
  com.yourcompany.yourapp
```

Replace `com.yourcompany.yourapp` with your app's package name.

### Test Behavior Events

1. Set MIU to test value: `MIClient.setMIU("00000000-0000-0000-0000-000000000000")`
2. Trigger an event (e.g., view a product)
3. Check Logcat for event logs (filter by "MovableInk" or "MI SDK")
4. You should see success message with event name
5. Coordinate with MovableInk team to verify events are received

## Common Issues and Solutions

### Deeplinks Not Opening App

**Issue**: Clicking MovableInk links doesn't open the app

**Solutions**:
- Verify assetlinks.json is accessible: `https://mi.yourcompany.com/.well-known/assetlinks.json`
- Check package name and SHA-256 fingerprint match what's in assetlinks.json
- Ensure `android:autoVerify="true"` is set on at least one intent filter
- Verify testing on Android 7.0 (API 24) or later
- Check intent filters match the URL pattern exactly (scheme, host, pathPrefix)
- Try clearing app data and reinstalling
- Wait 24-48 hours after assetlinks.json changes for Google to re-verify

### Get SHA-256 Fingerprint

To get your app's SHA-256 fingerprint for assetlinks.json:

```bash
# For debug keystore
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android

# For release keystore
keytool -list -v -keystore /path/to/release.keystore -alias your_alias
```

### Events Not Sending

**Issue**: Behavior events not appearing in MovableInk platform

**Solutions**:
- Verify API key is set in `local.properties` and `AndroidManifest.xml`
- Ensure MIU is set before sending events: `MIClient.setMIU("...")`
- Check Logcat for SDK logs showing success/failure
- Verify region is correct if using EU region
- Confirm API key is for correct environment (staging/production)
- Check network connectivity

### MIU Mismatch

**Issue**: Events not associating with correct users in campaigns

**Solutions**:
- Verify MIU matches exactly what marketing team uses in ESP
- Ensure MIU is set after user logs in
- MIU should be UUID format, not email address
- Call `MIClient.identifyUser()` after setting MIU for guest users who log in

### Build Errors

**Issue**: SDK not found or import errors

**Solutions**:
- Ensure `mavenCentral()` is in your repositories
- Run Gradle sync after adding dependency
- Check minimum SDK version is API 24 or higher
- Clean and rebuild project
- Check for conflicting dependencies

## Important Notes

- **Replace placeholders**: Always replace `mi.yourcompany.com` with actual MovableInk subdomain
- **MIU requirements**: Must be non-PII, URL-friendly, match marketing team's identifier
- **API Key**: Required only for behavior events, NOT for deeplinking
- **Deeplinking works without API key**: Basic deeplinking functionality requires only App Links configuration
- **Region compliance**: Use `"EU"` region for European users if required for data compliance
- **Price format**: Always use string format for prices ("15.99", not 15.99)
- **Product IDs**: Must not be empty strings - will cause event to fail
- **Testing**: Use UUID `00000000-0000-0000-0000-000000000000` for testing with MovableInk team
- **Guest users**: Call `identifyUser()` after setting MIU for users who made actions while logged out
- **Kotlin vs Java**: SDK supports both languages; examples provided for both

## Full Example App

Minimal working example:

```kotlin
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.movableink.sdk.MIClient
import com.movableink.sdk.ProductProperties
import com.movableink.sdk.Currency

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Set MIU when user is identified
        val userId = getUserId() // Your method to get user ID
        if (userId != null) {
            MIClient.setMIU(userId)
        }

        // Handle deeplink
        handleIntent(intent)
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        handleIntent(intent)
    }

    private fun handleIntent(intent: Intent?) {
        val action = intent?.action
        val data: Uri? = intent?.data

        if (action == Intent.ACTION_VIEW && data != null) {
            MIClient.resolveUrlAsync(data.toString()) { resolvedUrl ->
                resolvedUrl?.let {
                    println("Opening: $it")
                    navigateToContent(it)
                }
            }
        }
    }

    private fun navigateToContent(url: String) {
        val uri = Uri.parse(url)
        when {
            uri.path?.contains("/products/") == true -> {
                val productId = uri.lastPathSegment
                // Navigate to product detail
            }
        }
    }

    // Example: Capture product view
    fun onProductViewed(product: Product) {
        MIClient.productViewed(
            ProductProperties(
                id = product.id,
                title = product.name,
                price = product.price,
                currency = Currency.USD,
                url = "https://yourapp.com/products/${product.id}"
            )
        )
    }

    private fun getUserId(): String? {
        // Your logic to get user ID
        return null
    }
}

data class Product(
    val id: String,
    val name: String,
    val price: String
)
```

## Push Notification Analytics (Optional)

### Overview

Track when users open MovableInk push notifications for campaign attribution and conversion measurement. This feature is particularly useful for DaVinci customers.

### Prerequisites

This assumes you have already set up Firebase Cloud Messaging (FCM) in your Android app. If you haven't, see the [Firebase documentation](https://firebase.google.com/docs/cloud-messaging/android/client).

### Kotlin Implementation

Handle notification opens in both `onCreate` (cold starts) and `onNewIntent` (warm starts):

```kotlin
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import com.movableink.inked.MIClient

class MainActivity : ComponentActivity() {

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

        // Check if app was opened from a notification
        checkIntentExtras()

        // Your app setup
        setContent {
            YourApp()
        }
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        this.intent = intent
        checkIntentExtras()
    }

    private fun checkIntentExtras() {
        intent?.extras?.let { bundle ->
            // Optional: Log extras for debugging
            for (key in bundle.keySet()) {
                bundle.getString(key)?.let { value ->
                    Log.d("MovableInk", "Intent Extra - Key: $key, Value: $value")
                }
            }

            // Track the notification open with MovableInk
            MIClient.handlePushNotificationOpened(bundle)
        }
    }
}
```

### Manual Tracking Without SDK

Extract the tracking URL from the notification payload and make a request:

```kotlin
import android.os.Bundle
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.net.URL

private fun checkIntentExtras() {
    intent?.extras?.let { bundle ->
        trackMovableInkNotification(bundle)
    }
}

private fun trackMovableInkNotification(bundle: Bundle) {
    // Extract the MovableInk tracking URL from the payload
    val miUrl = bundle.getString("mi_url")

    if (!miUrl.isNullOrEmpty()) {
        CoroutineScope(Dispatchers.IO).launch {
            try {
                URL(miUrl).openConnection().apply {
                    connect()
                    getInputStream().close()
                }
            } catch (e: Exception) {
                // Handle error
            }
        }
    }
}
```

### Payload Format

MovableInk push notifications sent via FCM have this structure:

```json
{
  "notification": {
    "title": "Special Offer Just for You!",
    "body": "Open to unlock exclusive savings."
  },
  "data": {
    "mi_url": "https://mi.example.com/deeplink/abc123",
    "mi_source": "davinci"
  }
}
```

The `mi_url` in the `data` object is the tracking URL that the SDK uses to record the notification open event.

## Additional Resources

- Full Documentation: https://sdk-mobile.movableink.com/
- GitHub Repository: https://github.com/movableink/android-sdk
- Contact MovableInk team for: API keys, assetlinks.json configuration, MIU strategy, testing support