StageUp
Mobile SDKDeep Link

Android

AdStage DeepLink Integration Guide (Android)

Table of Contents

  1. Overview
  2. Project Setup
  3. AndroidManifest.xml Configuration
  4. Application Class Setup
  5. MainActivity Setup
  6. Handling Deep Link Reception
  7. Creating Deep Links
  8. Advanced Features
  9. Troubleshooting

Overview

The AdStage DeepLink SDK provides the following features:

  • Real-time Deep Links: Immediate processing via URL Scheme and App Links
  • Deferred Deep Links: Restoration on first launch after app installation
  • Dynamic Deep Link Creation: Trackable link generation through server API
  • Attribution Tracking: Marketing analysis based on UTM parameters

Project Setup

Adding Gradle Dependencies

settings.gradle.kts

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://repo.nbase.io/repository/nbase-releases") }
    }
}

build.gradle.kts (Module: app)

dependencies {
    implementation("io.nbase:adapter-adstage:3.0.+")
    
    // Required dependencies
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
    implementation("com.squareup.okhttp3:okhttp:4.11.0")
}

AndroidManifest.xml Configuration

1. Permission Setup

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    
    <!-- Required permissions -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    
    <!-- Install Referrer permission (for deferred deep links) -->
    <uses-permission android:name="com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE" />
    
    <application>
        <!-- ... -->
    </application>
</manifest>
<activity
    android:name=".MainActivity"
    android:exported="true"
    android:launchMode="singleTask">
    
    <!-- Default launcher -->
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    
    <!-- URL Scheme deep link (e.g., myapp://promo/summer) -->
    <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="myapp"
            android:host="promo" />
    </intent-filter>
    
    <!-- App Link (https://go.myapp.com/...) -->
    <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="go.myapp.com" />
    </intent-filter>
    
    <!-- AdStage deep link domain (e.g., https://go.adstage.net/...) -->
    <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="go.adstage.net" />
    </intent-filter>
</activity>

3. Important launchMode Settings

<!-- Recommended: singleTask -->
android:launchMode="singleTask"
 
<!-- When using singleTask:
     - Reuses existing Activity if available
     - onNewIntent() is called
     - Brings to top of Task stack
-->

Application Class Setup

MyApplication.kt

package com.example.myapp
 
import android.app.Application
import io.nbase.adapter.adstage.AdStage
 
class MyApplication : Application() {
    
    override fun onCreate() {
        super.onCreate()
        
        // Initialize AdStage SDK
        AdStage.initialize(
            context = this,
            apiKey = "your-api-key-here",
            serverUrl = "https://api.adstage.app" // Optional, default can be used
        )
        
        // Setup deep link listener
        setupDeeplinkListener()
    }
    
    private fun setupDeeplinkListener() {
        AdStage.setDeeplinkListener(object : io.nbase.adapter.adstage.models.DeeplinkListener {
            override fun onDeeplinkReceived(data: io.nbase.adapter.adstage.models.DeeplinkData) {
                android.util.Log.d("AdStage", """
                    ✅ Deep link received
                    - Short Path: ${data.shortPath}
                    - Link ID: ${data.linkId}
                    - Source: ${data.source}
                    - Parameters: ${data.parameters}
                """.trimIndent())
                
                // Handle globally or pass through event bus
                handleGlobalDeeplink(data)
            }
            
            override fun onDeeplinkFailed(error: String, shortPath: String?) {
                android.util.Log.e("AdStage", """
                    ❌ Deep link failed
                    - Error: $error
                    - Short Path: $shortPath
                """.trimIndent())
            }
        })
    }
    
    private fun handleGlobalDeeplink(data: io.nbase.adapter.adstage.models.DeeplinkData) {
        // Pass to current Activity via global event bus or SharedFlow
        // Or store in deep link manager for Activity to retrieve
    }
}

Register Application in AndroidManifest.xml

<application
    android:name=".MyApplication"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/Theme.MyApp">
    <!-- ... -->
</application>

MainActivity Setup

MainActivity.kt

package com.example.myapp
 
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import io.nbase.adapter.adstage.AdStage
import io.nbase.adapter.adstage.models.DeeplinkData
import io.nbase.adapter.adstage.models.DeeplinkListener
 
class MainActivity : AppCompatActivity() {
    
    companion object {
        private const val TAG = "MainActivity"
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // Handle deep link (when app is first launched or launched from background)
        handleIntent(intent)
        
        // Setup local deep link listener (optional)
        setupLocalDeeplinkListener()
    }
    
    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        setIntent(intent) // Important: Replace with new Intent
        
        Log.d(TAG, "onNewIntent called")
        
        // Handle deep link (when new deep link received while app is running)
        handleIntent(intent)
    }
    
    private fun handleIntent(intent: Intent?) {
        if (intent == null) {
            Log.w(TAG, "Intent is null")
            return
        }
        
        Log.d(TAG, """
            Intent received:
            - Action: ${intent.action}
            - Data: ${intent.data}
            - Extras: ${intent.extras?.keySet()?.joinToString()}
        """.trimIndent())
        
        // Pass Intent to AdStage SDK
        val handled = AdStage.handleIntent(this, intent)
        
        if (handled) {
            Log.i(TAG, "✅ AdStage handled the deep link")
        } else {
            Log.d(TAG, "ℹ️ Not an AdStage deep link")
            
            // Handle regular Intent
            handleRegularIntent(intent)
        }
    }
    
    private fun handleRegularIntent(intent: Intent) {
        when (intent.action) {
            Intent.ACTION_VIEW -> {
                // Handle regular web links or custom schemes
                val uri = intent.data
                Log.d(TAG, "Regular deep link: $uri")
            }
            // Handle other actions...
        }
    }
    
    /**
     * Local deep link listener (Activity-only handling)
     * Optional if global listener is set in Application
     */
    private fun setupLocalDeeplinkListener() {
        AdStage.setDeeplinkListener(object : DeeplinkListener {
            override fun onDeeplinkReceived(data: DeeplinkData) {
                Log.d(TAG, "📱 Deep link received in Activity: ${data.shortPath}")
                
                // Handle business logic
                when {
                    data.parameters.containsKey("campaign") -> {
                        handleCampaignDeeplink(data)
                    }
                    data.parameters.containsKey("promo") -> {
                        handlePromoDeeplink(data)
                    }
                    else -> {
                        handleDefaultDeeplink(data)
                    }
                }
            }
            
            override fun onDeeplinkFailed(error: String, shortPath: String?) {
                Log.e(TAG, "❌ Deep link failed: $error")
                // Show error UI
            }
        })
    }
    
    private fun handleCampaignDeeplink(data: DeeplinkData) {
        val campaign = data.parameters["campaign"]
        val channel = data.parameters["channel"]
        
        Log.d(TAG, """
            🎯 Campaign deep link handling
            - Campaign: $campaign
            - Channel: $channel
        """.trimIndent())
        
        // Navigate to campaign screen
        // startActivity(Intent(this, CampaignActivity::class.java).apply {
        //     putExtra("campaign", campaign)
        // })
    }
    
    private fun handlePromoDeeplink(data: DeeplinkData) {
        val promoCode = data.parameters["promo"]
        
        Log.d(TAG, "🎁 Promo code: $promoCode")
        
        // Apply promotion
        // applyPromoCode(promoCode)
    }
    
    private fun handleDefaultDeeplink(data: DeeplinkData) {
        Log.d(TAG, "📋 Default deep link handling: ${data.shortPath}")
        
        // Stay on main screen or navigate to specific screen
    }
    
    override fun onDestroy() {
        super.onDestroy()
        
        // Clean up listener (prevent memory leaks)
        // Don't clear here if using global listener
        // AdStage.clearDeeplinkListener()
    }
}

class MyActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        AdStage.setDeeplinkListener(object : DeeplinkListener {
            override fun onDeeplinkReceived(data: DeeplinkData) {
                // data.shortPath: "SDGWNBB"
                // data.linkId: "507f1f77bcf86cd799439011"
                // data.source: REALTIME or DEFERRED
                // data.parameters: Map<String, String>
                
                // Extract UTM parameters
                val utmSource = data.parameters["utmSource"]
                val utmMedium = data.parameters["utmMedium"]
                val utmCampaign = data.parameters["utmCampaign"]
                
                // Extract custom parameters
                val customParam = data.parameters["customKey"]
                
                // Navigate to screen
                navigateToScreen(data)
            }
            
            override fun onDeeplinkFailed(error: String, shortPath: String?) {
                // Handle error
                showErrorDialog(error)
            }
        })
    }
}
// Automatically handled!
// Install Referrer is automatically queried at AdStage.initialize()
// If saved deep link exists, onDeeplinkReceived is automatically called
 
// For manual handling if needed:
AdStage.handleInstallReferrer(context)
override fun onDeeplinkReceived(data: DeeplinkData) {
    when (data.source) {
        DeepLinkSource.REALTIME -> {
            Log.d(TAG, "🔗 Real-time deep link (received while app running)")
            // Can navigate immediately
        }
        DeepLinkSource.INSTALL -> {
            Log.d(TAG, "📦 Deferred deep link (first launch after install)")
            // Navigate after onboarding, etc.
        }
        else -> {
            Log.d(TAG, "❓ Unknown source")
        }
    }
}

1. Request Object Method

import io.nbase.adapter.adstage.AdStage
import io.nbase.adapter.adstage.models.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
 
class DeeplinkManager {
    
    fun createSimpleDeeplink() {
        CoroutineScope(Dispatchers.Main).launch {
            try {
                val request = CreateDeeplinkRequest(
                    name = "Summer Promotion Link",
                    description = "2024 Summer Season Promotion",
                    channel = "google-ads",
                    campaign = "summer2024",
                    parameters = mapOf(
                        "promo" to "SUMMER20",
                        "discount" to 20
                    )
                )
                
                val response = AdStage.createDeeplink(request)
                
                Log.d(TAG, """
                    ✅ Deep link created
                    - Short URL: ${response.shortUrl}
                    - Short Path: ${response.shortPath}
                    - ID: ${response.id}
                """.trimIndent())
                
                // Share generated URL
                shareUrl(response.shortUrl)
                
            } catch (e: Exception) {
                Log.e(TAG, "❌ Deep link creation failed: ${e.message}")
            }
        }
    }
}

2. Builder Pattern (DSL Style)

fun createDeeplinkWithBuilder() {
    CoroutineScope(Dispatchers.Main).launch {
        try {
            val response = AdStage.createDeeplink("Winter Promotion") {
                description("2024-2025 Winter Season Promotion")
                shortPath("WINTER24")  // Custom path
                
                // Tracking parameters
                channel("facebook-ads")
                subChannel("instagram")
                campaign("winter2024")
                adGroup("fashion-lovers")
                creative("banner-001")
                content("hero-image")
                keyword("winter-sale")
                
                // Redirect configuration
                redirectConfig {
                    type(RedirectType.APP)  // STORE, APP, WEB
                    
                    // Android settings
                    android {
                        appScheme("myapp://promo/winter")
                        packageName("com.example.myapp")
                        webUrl("https://example.com/promo/winter")
                    }
                    
                    // iOS settings
                    ios {
                        appScheme("myapp://promo/winter")
                        bundleId("com.example.myapp")
                        appStoreId("123456789")
                        webUrl("https://example.com/promo/winter")
                    }
                    
                    // Desktop settings
                    desktop {
                        webUrl("https://example.com/promo/winter")
                    }
                }
                
                // Custom parameters
                parameter("discount", 30)
                parameter("promoCode", "WINTER30")
                parameter("validUntil", "2025-03-31")
                
                // Status setting
                status(DeeplinkStatus.ACTIVE)
            }
            
            Log.d(TAG, "✅ Short URL: ${response.shortUrl}")
            
        } catch (e: Exception) {
            Log.e(TAG, "❌ Error: ${e.message}")
        }
    }
}

3. Callback Method (Async)

fun createDeeplinkWithCallback() {
    val request = CreateDeeplinkRequest(
        name = "App Invite Link",
        channel = "referral",
        campaign = "invite-friend",
        parameters = mapOf(
            "referrer" to "USER123",
            "bonus" to 5000
        )
    )
    
    // Callback method uses suspend function wrapped in coroutine
    CoroutineScope(Dispatchers.Main).launch {
        try {
            val response = AdStage.createDeeplink(request)
            onSuccess(response)
        } catch (e: Exception) {
            onError(e)
        }
    }
}
 
private fun onSuccess(response: CreateDeeplinkResponse) {
    Log.d(TAG, "Deep link created successfully: ${response.shortUrl}")
    
    // Update UI
    runOnUiThread {
        textView.text = response.shortUrl
        shareButton.isEnabled = true
    }
}
 
private fun onError(error: Exception) {
    Log.e(TAG, "Deep link creation failed: ${error.message}")
    
    runOnUiThread {
        Toast.makeText(this, "Deep link creation failed", Toast.LENGTH_SHORT).show()
    }
}

4. RedirectType Explanation

enum class RedirectType {
    STORE,  // If app not installed → Go to store
            // If app installed → Launch app (deferred deep link)
    
    APP,    // If app not installed → Go to web fallback URL
            // If app installed → Launch app (real-time deep link)
    
    WEB     // Always go to web URL
            // Regardless of app installation
}
fun shareDeeplink(shortUrl: String) {
    val shareIntent = Intent(Intent.ACTION_SEND).apply {
        type = "text/plain"
        putExtra(Intent.EXTRA_TEXT, """
            🎁 Special Promotion Invite!
            
            Sign up through this link and get a $50 discount coupon.
            $shortUrl
        """.trimIndent())
    }
    
    startActivity(Intent.createChooser(shareIntent, "Invite Friends"))
}

Advanced Features

1. Global User Attributes Setup

// Set in Application onCreate()
val userAttributes = UserAttributes(
    gender = "male",
    country = "KR",
    city = "Seoul",
    age = "28",
    language = "ko-KR"
)
AdStage.setUserAttributes(userAttributes)
 
// Automatically included in subsequent trackEvent calls

2. Global Device Info Setup

val deviceInfo = DeviceInfo(
    category = "mobile",
    platform = "Android",
    model = Build.MODEL,
    appVersion = BuildConfig.VERSION_NAME,
    osVersion = Build.VERSION.RELEASE
)
AdStage.setDeviceInfo(deviceInfo)

3. User ID and Session Management

// On login
AdStage.setUserId("user_123456")
 
// On logout
AdStage.setUserId(null)
 
// Start new session
AdStage.startNewSession()
 
// Get current session ID
val sessionId = AdStage.getSessionId()
Log.d(TAG, "Current Session: $sessionId")

4. Promotion Banner Integration

// Get promotion list
CoroutineScope(Dispatchers.Main).launch {
    val params = PromotionListParams.builder()
        .bannerType("NATIVE")
        .region("KR")
        .limit(10)
        .build()
    
    val response = AdStage.getPromotionList(params)
    
    response.promotions.forEach { promotion ->
        Log.d(TAG, """
            Promotion: ${promotion.appName}
            Banner: ${promotion.bannerUrl}
        """.trimIndent())
    }
}
 
// Handle promotion click
CoroutineScope(Dispatchers.Main).launch {
    val result = AdStage.handlePromotionClick(context, promotion)
    
    when (result) {
        is PromotionClickResult.Success -> {
            Log.d(TAG, "Store opened: ${result.storeUrl}")
        }
        is PromotionClickResult.Failure -> {
            Log.e(TAG, "Failed: ${result.error}")
        }
    }
}

Troubleshooting

Checklist:

  • intent-filter correctly configured in AndroidManifest.xml
  • Verify android:exported="true" setting
  • Recommend android:launchMode="singleTask" setting
  • Verify AdStage.initialize() is called
  • Verify setDeeplinkListener() is called
  • Verify handleIntent() is called

Debugging:

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    
    Log.d(TAG, """
        Intent Debug:
        - Action: ${intent?.action}
        - Data: ${intent?.data}
        - Scheme: ${intent?.data?.scheme}
        - Host: ${intent?.data?.host}
        - Path: ${intent?.data?.path}
    """.trimIndent())
    
    handleIntent(intent)
}

Causes:

  • Missing Install Referrer permission
  • Installation not through Google Play Store (direct APK install)
  • Install Referrer API initialization failed

Solution:

// Manually handle Install Referrer
AdStage.handleInstallReferrer(applicationContext)
 
// Check logs
Log.d(TAG, "Install Referrer processed")

Verification Method:

# 1. Check Digital Asset Links file
https://go.myapp.com/.well-known/assetlinks.json
 
# 2. Use verification tool
https://developers.google.com/digital-asset-links/tools/generator
 
# 3. Test with ADB
adb shell am start -a android.intent.action.VIEW -d "https://go.myapp.com/ABCDEF"

assetlinks.json Example:

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.myapp",
    "sha256_cert_fingerprints": [
      "14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"
    ]
  }
}]

4. ProGuard/R8 Obfuscation Issues

proguard-rules.pro:

# AdStage SDK
-keep class io.nbase.adapter.adstage.** { *; }
-keepclassmembers class io.nbase.adapter.adstage.** { *; }

# Model classes
-keep class io.nbase.adapter.adstage.models.** { *; }

# OkHttp
-dontwarn okhttp3.**
-keep class okhttp3.** { *; }

# Kotlin Coroutines
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}

5. Multi-Process Environment

<!-- If you have Activity running in separate process -->
<activity
    android:name=".SomeActivity"
    android:process=":separate">
    <!-- Need separate initialization to handle deep links in this Activity -->
</activity>
// Need to call AdStage.initialize() in each process
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        
        // Initialize in all processes
        AdStage.initialize(this, apiKey)
    }
}

Testing Methods

1. ADB Testing

# URL Scheme test
adb shell am start -a android.intent.action.VIEW -d "myapp://promo/summer"
 
# App Link test
adb shell am start -a android.intent.action.VIEW -d "https://go.myapp.com/ABCDEF"
 
# With parameters
adb shell am start -a android.intent.action.VIEW -d "myapp://promo?campaign=summer&discount=20"

2. Intent Log Verification

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    
    intent?.let {
        Log.d(TAG, "=== Intent Debug ===")
        Log.d(TAG, "Action: ${it.action}")
        Log.d(TAG, "Data: ${it.data}")
        Log.d(TAG, "Extras: ${it.extras?.keySet()?.joinToString()}")
        
        it.data?.let { uri ->
            Log.d(TAG, "URI Scheme: ${uri.scheme}")
            Log.d(TAG, "URI Host: ${uri.host}")
            Log.d(TAG, "URI Path: ${uri.path}")
            Log.d(TAG, "URI Query: ${uri.query}")
        }
    }
}

3. Real Device Testing

// Create test deep link
CoroutineScope(Dispatchers.Main).launch {
    val response = AdStage.createDeeplink("Test Link") {
        channel("test")
        campaign("test-campaign")
        parameter("test", "true")
    }
    
    Log.d(TAG, "Test URL: ${response.shortUrl}")
    
    // Copy URL and test in browser or messenger
}

References


Table of Contents