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 Configuration
  5. MainActivity Configuration
  6. Handling Incoming Deep Links
  7. Creating Deep Links
  8. Advanced Features
  9. Troubleshooting

Overview

The AdStage DeepLink SDK provides the following features:

  • Real-time deep links: Instant handling via URL Scheme and App Link
  • Deferred deep links: Restored on the first launch after app installation
  • Dynamic deep link creation: Trackable link generation through the server API
  • Attribution tracking: Marketing analytics 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:nbase-adapter-adstage:3.0.9")
    
    // 필수 의존성
    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">
    
    <!-- 필수 권한 -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    
    <!-- Install Referrer 권한 (디퍼드 딥링크용) -->
    <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">
    
    <!-- 기본 런처 -->
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    
    <!-- URL Scheme 딥링크 (예: 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 딥링크 도메인 (예: 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 Notes on launchMode

<!-- 권장: singleTask -->
android:launchMode="singleTask"
 
<!-- singleTask 사용 시:
     - 기존 Activity가 있으면 재사용
     - onNewIntent()가 호출됨
     - Task 최상단으로 이동
-->

Application Class Configuration

MyApplication.kt

package com.example.myapp
 
import android.app.Application
import io.nbase.adapter.adstage.AdStage
 
class MyApplication : Application() {
    
    override fun onCreate() {
        super.onCreate()
        
        // AdStage SDK 초기화
        AdStage.initialize(
            context = this,
            apiKey = "your-api-key-here",
            serverUrl = "https://api.adstage.app" // 선택사항, 기본값 사용 가능
        )
        
        // 딥링크 리스너 설정
        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", """
                    ✅ 딥링크 수신
                    - Short Path: ${data.shortPath}
                    - Deeplink ID: ${data.deeplinkId}
                    - Source: ${data.source}
                    - Parameters: ${data.parameters}
                """.trimIndent())
                
                // 전역적으로 처리하거나 이벤트 버스로 전달
                handleGlobalDeeplink(data)
            }
            
            override fun onDeeplinkFailed(error: String, shortPath: String?) {
                android.util.Log.e("AdStage", """
                    ❌ 딥링크 실패
                    - Error: $error
                    - Short Path: $shortPath
                """.trimIndent())
            }
        })
    }
    
    private fun handleGlobalDeeplink(data: io.nbase.adapter.adstage.models.DeeplinkData) {
        // 전역 이벤트 버스나 SharedFlow를 통해 현재 Activity에 전달
        // 또는 딥링크 매니저에 저장 후 Activity에서 가져가기
    }
}

Registering the 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 Configuration

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)
        
        // 딥링크 처리 (앱이 처음 실행되거나 백그라운드에서 실행될 때)
        handleIntent(intent)
        
        // 로컬 딥링크 리스너 설정 (선택사항)
        setupLocalDeeplinkListener()
    }
    
    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        setIntent(intent) // 중요: 새 Intent로 교체
        
        Log.d(TAG, "onNewIntent called")
        
        // 딥링크 처리 (앱이 이미 실행 중일 때 새 딥링크 수신)
        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())
        
        // AdStage SDK에 Intent 전달
        val handled = AdStage.handleIntent(this, intent)
        
        if (handled) {
            Log.i(TAG, "✅ AdStage가 딥링크를 처리했습니다")
        } else {
            Log.d(TAG, "ℹ️ AdStage 딥링크가 아닙니다")
            
            // 일반 Intent 처리
            handleRegularIntent(intent)
        }
    }
    
    private fun handleRegularIntent(intent: Intent) {
        when (intent.action) {
            Intent.ACTION_VIEW -> {
                // 일반 웹 링크나 커스텀 스킴 처리
                val uri = intent.data
                Log.d(TAG, "Regular deep link: $uri")
            }
            // 다른 액션 처리...
        }
    }
    
    /**
     * 로컬 딥링크 리스너 (Activity에서만 처리)
     * Application에서 전역 리스너를 설정했다면 선택사항
     */
    private fun setupLocalDeeplinkListener() {
        AdStage.setDeeplinkListener(object : DeeplinkListener {
            override fun onDeeplinkReceived(data: DeeplinkData) {
                Log.d(TAG, "📱 Activity에서 딥링크 수신: ${data.shortPath}")
                
                // 비즈니스 로직 처리
                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, "❌ 딥링크 실패: $error")
                // 에러 UI 표시
            }
        })
    }
    
    private fun handleCampaignDeeplink(data: DeeplinkData) {
        val campaign = data.parameters["campaign"]
        val channel = data.parameters["channel"]
        
        Log.d(TAG, """
            🎯 캠페인 딥링크 처리
            - Campaign: $campaign
            - Channel: $channel
        """.trimIndent())
        
        // 캠페인 화면으로 이동
        // startActivity(Intent(this, CampaignActivity::class.java).apply {
        //     putExtra("campaign", campaign)
        // })
    }
    
    private fun handlePromoDeeplink(data: DeeplinkData) {
        val promoCode = data.parameters["promo"]
        
        Log.d(TAG, "🎁 프로모션 코드: $promoCode")
        
        // 프로모션 적용
        // applyPromoCode(promoCode)
    }
    
    private fun handleDefaultDeeplink(data: DeeplinkData) {
        Log.d(TAG, "📋 기본 딥링크 처리: ${data.shortPath}")
        
        // 메인 화면 유지하거나 특정 화면으로 이동
    }
    
    override fun onDestroy() {
        super.onDestroy()
        
        // 리스너 정리 (메모리 누수 방지)
        // 전역 리스너를 사용한다면 여기서 clear하지 않음
        // 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.deeplinkId: "507f1f77bcf86cd799439011"
                // data.source: DIRECT_LINK 또는 INSTALL_REFERRER
                // data.eventType: OPEN 또는 INSTALL
                // data.parameters: Map<String, String>
                
                // 트래킹 파라미터 추출 (서버 응답 키 기준)
                val channel = data.parameters["channel"]
                val subChannel = data.parameters["subChannel"]
                val campaign = data.parameters["campaign"]
                
                // 커스텀 파라미터 추출
                val customParam = data.parameters["customKey"]
                
                // 화면 이동
                navigateToScreen(data)
            }
            
            override fun onDeeplinkFailed(error: String, shortPath: String?) {
                // 에러 처리
                showErrorDialog(error)
            }
        })
    }
}
// 자동으로 처리됨!
// AdStage.initialize() 시점에 Install Referrer를 자동으로 조회하고
// 저장된 딥링크가 있으면 onDeeplinkReceived가 자동 호출됨
 
// 수동 처리가 필요한 경우:
AdStage.handleInstallReferrer(context)
import io.nbase.adapter.adstage.models.DeeplinkSource
 
override fun onDeeplinkReceived(data: DeeplinkData) {
    when (data.source) {
        DeeplinkSource.DIRECT_LINK -> {
            Log.d(TAG, "🔗 직접 딥링크 클릭 (실시간)")
            // 즉시 화면 전환 가능
        }
        DeeplinkSource.INSTALL_REFERRER -> {
            Log.d(TAG, "📦 디퍼드 딥링크 (Play Store Install Referrer)")
            // 온보딩 후 화면 전환 등
        }
    }
}

1. Request Object Approach

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 = "여름 프로모션 링크",
                    description = "2024년 여름 시즌 프로모션",
                    channel = "google-ads",
                    campaign = "summer2024",
                    parameters = mapOf(
                        "promo" to "SUMMER20",
                        "discount" to 20
                    )
                )
                
                val response = AdStage.createDeeplink(request)
                
                Log.d(TAG, """
                    ✅ 딥링크 생성 완료
                    - Short URL: ${response.shortUrl}
                    - Short Path: ${response.shortPath}
                    - ID: ${response.id}
                """.trimIndent())
                
                // 생성된 URL 공유
                shareUrl(response.shortUrl)
                
            } catch (e: Exception) {
                Log.e(TAG, "❌ 딥링크 생성 실패: ${e.message}")
            }
        }
    }
}

2. Builder Pattern (DSL Style)

fun createDeeplinkWithBuilder() {
    CoroutineScope(Dispatchers.Main).launch {
        try {
            val response = AdStage.createDeeplink("겨울 프로모션") {
                description("2024-2025 겨울 시즌 프로모션")
                shortPath("WINTER24")  // 사용자 지정 경로
                
                // 트래킹 파라미터
                channel("facebook-ads")
                subChannel("instagram")
                campaign("winter2024")
                adGroup("fashion-lovers")
                creative("banner-001")
                content("hero-image")
                keyword("winter-sale")
                
                // 리다이렉트 설정
                redirectConfig {
                    type(RedirectType.APP)  // STORE, APP, WEB
                    
                    // Android 설정
                    android {
                        appScheme("myapp://promo/winter")
                        packageName("com.example.myapp")
                        webUrl("https://example.com/promo/winter")
                    }
                    
                    // iOS 설정
                    ios {
                        appScheme("myapp://promo/winter")
                        bundleId("com.example.myapp")
                        appStoreId("123456789")
                        webUrl("https://example.com/promo/winter")
                    }
                    
                    // 데스크톱 설정
                    desktop {
                        webUrl("https://example.com/promo/winter")
                    }
                }
                
                // 커스텀 파라미터
                parameter("discount", 30)
                parameter("promoCode", "WINTER30")
                parameter("validUntil", "2025-03-31")
                
                // 상태 설정
                status(DeeplinkStatus.ACTIVE)
            }
            
            Log.d(TAG, "✅ Short URL: ${response.shortUrl}")
            
        } catch (e: Exception) {
            Log.e(TAG, "❌ 에러: ${e.message}")
        }
    }
}

3. Callback Approach (Asynchronous)

fun createDeeplinkWithCallback() {
    val request = CreateDeeplinkRequest(
        name = "앱 초대 링크",
        channel = "referral",
        campaign = "invite-friend",
        parameters = mapOf(
            "referrer" to "USER123",
            "bonus" to 5000
        )
    )
    
    // 콜백 방식은 suspend 함수를 코루틴으로 감싸서 사용
    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, "딥링크 생성 성공: ${response.shortUrl}")
    
    // UI 업데이트
    runOnUiThread {
        textView.text = response.shortUrl
        shareButton.isEnabled = true
    }
}
 
private fun onError(error: Exception) {
    Log.e(TAG, "딥링크 생성 실패: ${error.message}")
    
    runOnUiThread {
        Toast.makeText(this, "딥링크 생성 실패", Toast.LENGTH_SHORT).show()
    }
}

4. RedirectType Explanation

enum class RedirectType {
    STORE,  // 앱 미설치 시 → 스토어로 이동
            // 앱 설치 시 → 앱 실행 (디퍼드 딥링크)
    
    APP,    // 앱 미설치 시 → 웹 폴백 URL로 이동
            // 앱 설치 시 → 앱 실행 (실시간 딥링크)
    
    WEB     // 항상 웹 URL로 이동
            // 앱 설치 여부 무관
}
fun shareDeeplink(shortUrl: String) {
    val shareIntent = Intent(Intent.ACTION_SEND).apply {
        type = "text/plain"
        putExtra(Intent.EXTRA_TEXT, """
            🎁 특별 프로모션 초대!
            
            이 링크를 통해 가입하면 5,000원 할인 쿠폰을 드립니다.
            $shortUrl
        """.trimIndent())
    }
    
    startActivity(Intent.createChooser(shareIntent, "친구 초대하기"))
}

Advanced Features

1. Setting Global User Attributes

// Application onCreate()에서 설정
val userAttributes = UserAttributes(
    gender = "male",
    country = "KR",
    city = "Seoul",
    age = "28",
    language = "ko-KR"
)
AdStage.setUserAttributes(userAttributes)
 
// 이후 trackEvent 호출 시 자동으로 포함됨

2. Retrieving and Clearing User Attributes

// 현재 설정된 사용자 속성 조회
val current: UserAttributes? = AdStage.getUserAttributes()
 
// 로그아웃 시 사용자 속성 제거
AdStage.clearUserAttributes()
 
// 모든 전역 정보 초기화 (사용자 속성 제거)
AdStage.clearAll()

Device information (model, OS version, app version, advertising identifier, etc.) and sessions are automatically collected and managed by the SDK. There is no need to call any separate device information setup or session management API.


Troubleshooting

Checklist:

  • intent-filter is correctly configured in AndroidManifest.xml
  • android:exported="true" is set
  • android:launchMode="singleTask" is recommended
  • AdStage.initialize() is called
  • setDeeplinkListener() is called
  • 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:

  • No Install Referrer permission
  • Installation not through Google Play Store (direct APK installation)
  • Install Referrer API initialization failure

Solution:

// 수동으로 Install Referrer 처리
AdStage.handleInstallReferrer(applicationContext)
 
// 로그 확인
Log.d(TAG, "Install Referrer 처리 완료")

How to Check:

# 1. Digital Asset Links 파일 확인
https://go.myapp.com/.well-known/assetlinks.json
 
# 2. 검증 도구 사용
https://developers.google.com/digital-asset-links/tools/generator
 
# 3. 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.** { *; }

# 모델 클래스
-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

<!-- 별도 프로세스에서 실행되는 Activity가 있는 경우 -->
<activity
    android:name=".SomeActivity"
    android:process=":separate">
    <!-- 이 Activity에서도 딥링크를 처리하려면 별도 초기화 필요 -->
</activity>
// 각 프로세스에서 AdStage.initialize() 호출 필요
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        
        // 모든 프로세스에서 초기화
        AdStage.initialize(this, apiKey)
    }
}

Testing Methods

1. ADB Testing

# URL Scheme 테스트
adb shell am start -a android.intent.action.VIEW -d "myapp://promo/summer"
 
# App Link 테스트
adb shell am start -a android.intent.action.VIEW -d "https://go.myapp.com/ABCDEF"
 
# 파라미터 포함
adb shell am start -a android.intent.action.VIEW -d "myapp://promo?campaign=summer&discount=20"

2. Checking Intent Logs

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. Testing on a Real Device

// 테스트용 딥링크 생성
CoroutineScope(Dispatchers.Main).launch {
    val response = AdStage.createDeeplink("테스트 링크") {
        channel("test")
        campaign("test-campaign")
        parameter("test", "true")
    }
    
    Log.d(TAG, "테스트 URL: ${response.shortUrl}")
    
    // URL을 복사하여 브라우저나 메신저에서 테스트
}

References


Table of Contents