adstage
行動 SDK深層連結

Android

AdStage DeepLink 整合指南(Android)

目錄

  1. 概述
  2. 專案設定
  3. AndroidManifest.xml 設定
  4. Application 類別設定
  5. MainActivity 設定
  6. 深層連結接收處理
  7. 建立深層連結
  8. 進階功能
  9. 疑難排解

概述

AdStage DeepLink SDK 提供以下功能:

  • 即時深層連結:透過 URL Scheme、App Link 立即處理
  • 延遲深層連結:應用程式安裝後首次啟動時還原
  • 動態建立深層連結:透過伺服器 API 建立可追蹤的連結
  • 歸因追蹤:以 UTM 參數為基礎的行銷分析

專案設定

新增 Gradle 相依套件

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 設定

1. 權限設定

<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. launchMode 設定的重要事項

<!-- 建議:singleTask -->
android:launchMode="singleTask"
 
<!-- 使用 singleTask 時:
     - 若已有 Activity 則重複使用
     - 會呼叫 onNewIntent()
     - 移至 Task 最上層
-->

Application 類別設定

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 取用
    }
}

於 AndroidManifest.xml 中註冊 Application

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

MainActivity 設定

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())
        
        // 將 Intent 傳遞給 AdStage SDK
        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 -> {
                // 處理一般網頁連結或自訂 scheme
                val uri = intent.data
                Log.d(TAG, "Regular deep link: $uri")
            }
            // 處理其他 action...
        }
    }
    
    /**
     * 本機深層連結監聽器(僅於 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()
    }
}

深層連結接收處理

1. 即時深層連結(應用程式執行中)

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)
            }
        })
    }
}

2. 延遲深層連結(應用程式安裝後首次啟動)

// 會自動處理!
// 於 AdStage.initialize() 時會自動查詢 Install Referrer,
// 若有已儲存的深層連結,則會自動呼叫 onDeeplinkReceived
 
// 需要手動處理時:
AdStage.handleInstallReferrer(context)

3. 區分深層連結來源

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 物件方式

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 模式(DSL 風格)

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. 回呼方式(非同步)

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 說明

enum class RedirectType {
    STORE,  // 應用程式未安裝時 → 前往商店
            // 應用程式已安裝時 → 啟動應用程式(延遲深層連結)
    
    APP,    // 應用程式未安裝時 → 前往網頁備援 URL
            // 應用程式已安裝時 → 啟動應用程式(即時深層連結)
    
    WEB     // 一律前往網頁 URL
            // 與應用程式是否安裝無關
}

5. 深層連結分享範例

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, "邀請好友"))
}

進階功能

1. 設定全域使用者屬性

// 於 Application onCreate() 中設定
val userAttributes = UserAttributes(
    gender = "male",
    country = "KR",
    city = "Seoul",
    age = "28",
    language = "ko-KR"
)
AdStage.setUserAttributes(userAttributes)
 
// 之後呼叫 trackEvent 時會自動包含這些屬性

2. 查詢與重設使用者屬性

// 查詢目前已設定的使用者屬性
val current: UserAttributes? = AdStage.getUserAttributes()
 
// 登出時移除使用者屬性
AdStage.clearUserAttributes()
 
// 重設所有全域資訊(移除使用者屬性)
AdStage.clearAll()

裝置資訊(機型、OS 版本、應用程式版本、廣告識別碼等)與工作階段由 SDK 自動 收集與管理。無需另行設定裝置資訊或呼叫工作階段管理 API。


疑難排解

1. 收不到深層連結

檢查清單:

  • AndroidManifest.xml 中的 intent-filter 是否正確設定
  • 確認已設定 android:exported="true"
  • 建議設定 android:launchMode="singleTask"
  • 確認已呼叫 AdStage.initialize()
  • 確認已呼叫 setDeeplinkListener()
  • 確認已呼叫 handleIntent()

偵錯:

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)
}

2. 延遲深層連結無法運作

原因:

  • 沒有 Install Referrer 權限
  • 並非透過 Google Play Store 安裝(直接安裝 APK)
  • Install Referrer API 初始化失敗

解決方法:

// 手動處理 Install Referrer
AdStage.handleInstallReferrer(applicationContext)
 
// 確認記錄
Log.d(TAG, "Install Referrer 處理完成")

確認方法:

# 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 範例:

[{
  "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 混淆問題

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. 多行程環境

<!-- 有於獨立行程中執行的 Activity 時 -->
<activity
    android:name=".SomeActivity"
    android:process=":separate">
    <!-- 若該 Activity 也要處理深層連結,則需另行初始化 -->
</activity>
// 需於每個行程中呼叫 AdStage.initialize()
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        
        // 於所有行程中初始化
        AdStage.initialize(this, apiKey)
    }
}

測試方法

1. ADB 測試

# 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. 確認 Intent 記錄

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. 實機測試

// 建立測試用深層連結
CoroutineScope(Dispatchers.Main).launch {
    val response = AdStage.createDeeplink("測試連結") {
        channel("test")
        campaign("test-campaign")
        parameter("test", "true")
    }
    
    Log.d(TAG, "測試 URL: ${response.shortUrl}")
    
    // 複製該 URL,於瀏覽器或通訊軟體中測試
}

參考資料


目錄