commit a671b0d2724cd0f4bf4faede3a5ae2963706b753
Author: CORE-FOLDCCCore <1813547935@qq.com>
Date: Thu Jun 4 17:16:17 2026 +0800
Implement TapADN commercialization module
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..20b61b7
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,9 @@
+# [1.0.0]
+
+### 新增
+
+* 接入 Dirichlet/TapADN 聚合 Unity SDK `4.2.5.0`。
+* 新增 `TapadnAdController`、激励视频、插屏、开屏播放器,实现 `CC-Framework.Commercialization` 抽象层。
+* 新增 Android 构建后处理:Manifest 权限、TapADN FileProvider、微信 OpenSDK WXEntryActivity/queries、AndroidX/Jetifier 属性。
+* 新增 `TapadnCommercialization` 便捷入口,由实现模块创建 controller 并初始化 `ADManager`。
+* 官方 Demo Sample 从主包剔除,调试内容改为可选 `Samples~`。
diff --git a/CHANGELOG.md.meta b/CHANGELOG.md.meta
new file mode 100644
index 0000000..66c1416
--- /dev/null
+++ b/CHANGELOG.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: ae16d7859fcf496b80493823cb6fa18d
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file
diff --git a/DirichletMediation.meta b/DirichletMediation.meta
new file mode 100644
index 0000000..a024212
--- /dev/null
+++ b/DirichletMediation.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 9d3b1d66c5f548b0a92be7e4a9c7d4c1
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/DirichletMediation/Editor.meta b/DirichletMediation/Editor.meta
new file mode 100644
index 0000000..eb68427
--- /dev/null
+++ b/DirichletMediation/Editor.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d76a8c4482f84d83ab0b17d2cf0b8346
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/DirichletMediation/Editor/DirichletGradlePostProcessor.cs b/DirichletMediation/Editor/DirichletGradlePostProcessor.cs
new file mode 100644
index 0000000..4a47bba
--- /dev/null
+++ b/DirichletMediation/Editor/DirichletGradlePostProcessor.cs
@@ -0,0 +1,272 @@
+#if UNITY_ANDROID
+using System.IO;
+using System.Text;
+using System.Text.RegularExpressions;
+using UnityEditor;
+using UnityEditor.Android;
+using UnityEngine;
+
+namespace Dirichlet.Mediation.Editor
+{
+ ///
+ /// Post-processes the Gradle files to inject Dirichlet SDK dependencies.
+ ///
+ /// This approach allows coexistence with other SDKs (e.g., TapSDK) by injecting
+ /// dependencies into Unity-generated Gradle files rather than shipping static templates.
+ ///
+ public class DirichletGradlePostProcessor : IPostGenerateGradleAndroidProject
+ {
+ private const string TAG = "[DirichletMediation]";
+
+ // Marker comments to identify our injected content
+ private const string DIRICHLET_DEPS_START = "// Dirichlet Mediation Dependencies Start";
+ private const string DIRICHLET_DEPS_END = "// Dirichlet Mediation Dependencies End";
+ private const string DIRICHLET_REPOS_START = "// Dirichlet Mediation Repositories Start";
+ private const string DIRICHLET_REPOS_END = "// Dirichlet Mediation Repositories End";
+
+ public int callbackOrder => 100; // Run after EDM4U (which uses lower values)
+
+ public void OnPostGenerateGradleAndroidProject(string path)
+ {
+ var enableCsj = EditorPrefs.GetBool("Dirichlet.Android.EnableCSJ", true);
+ var enableGdt = EditorPrefs.GetBool("Dirichlet.Android.EnableGDT", true);
+ var enableIqy = EditorPrefs.GetBool("Dirichlet.Android.EnableIQY", true);
+
+ Debug.Log($"{TAG} Processing Gradle project at: {path}");
+ Debug.Log($"{TAG} CSJ enabled: {enableCsj}, GDT enabled: {enableGdt}, IQY enabled: {enableIqy}");
+
+ ProcessBuildGradle(path, enableCsj, enableGdt, enableIqy);
+ ProcessSettingsGradle(path, enableCsj, enableGdt, enableIqy);
+ }
+
+ private void ProcessBuildGradle(string projectPath, bool enableCsj, bool enableGdt, bool enableIqy)
+ {
+ // Unity 2019.3+: projectPath is unityLibrary folder, build.gradle is directly inside
+ // Unity 2019.2 and below: projectPath might be the root, need to search
+ var gradlePath = Path.Combine(projectPath, "build.gradle");
+
+ if (!File.Exists(gradlePath))
+ {
+ // Fallback: try to find build.gradle in subdirectories
+ var searchPaths = new[]
+ {
+ Path.Combine(projectPath, "unityLibrary", "build.gradle"),
+ Path.Combine(projectPath, "src", "main", "build.gradle")
+ };
+
+ foreach (var path in searchPaths)
+ {
+ if (File.Exists(path))
+ {
+ gradlePath = path;
+ break;
+ }
+ }
+ }
+
+ if (!File.Exists(gradlePath))
+ {
+ Debug.LogWarning($"{TAG} Could not find build.gradle at {projectPath}");
+ return;
+ }
+
+ Debug.Log($"{TAG} Found build.gradle at: {gradlePath}");
+
+ var content = File.ReadAllText(gradlePath);
+ Debug.Log($"{TAG} Original build.gradle length: {content.Length}");
+
+ // Remove any previously injected content (for clean re-injection)
+ content = RemoveInjectedContent(content, DIRICHLET_DEPS_START, DIRICHLET_DEPS_END);
+ content = RemoveInjectedContent(content, DIRICHLET_REPOS_START, DIRICHLET_REPOS_END);
+
+ // Inject repositories
+ content = InjectRepositories(content, enableCsj, enableGdt, enableIqy);
+
+ // Inject dependencies
+ content = InjectDependencies(content, enableCsj, enableGdt, enableIqy);
+
+ File.WriteAllText(gradlePath, content);
+ Debug.Log($"{TAG} Updated build.gradle with Dirichlet Mediation dependencies");
+ }
+
+ private string InjectRepositories(string content, bool enableCsj, bool enableGdt, bool enableIqy)
+ {
+ // Check if our repos are already injected
+ if (content.Contains(DIRICHLET_REPOS_START))
+ {
+ return content;
+ }
+
+ var reposBlock = new StringBuilder();
+ reposBlock.AppendLine(DIRICHLET_REPOS_START);
+ reposBlock.AppendLine(" google()");
+ reposBlock.AppendLine(" mavenCentral()");
+ reposBlock.AppendLine(" flatDir {");
+ reposBlock.AppendLine(" dirs 'libs', 'DirichletMediation/libs'");
+ reposBlock.AppendLine(" }");
+
+ if (enableCsj)
+ {
+ reposBlock.AppendLine(" maven { url 'https://artifact.bytedance.com/repository/pangle' }");
+ }
+ if (enableGdt)
+ {
+ reposBlock.AppendLine(" maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }");
+ }
+
+ reposBlock.AppendLine($" {DIRICHLET_REPOS_END}");
+
+ // Try to find repositories block and inject after opening brace
+ var reposPattern = new Regex(@"(repositories\s*\{)");
+ if (reposPattern.IsMatch(content))
+ {
+ content = reposPattern.Replace(content, m =>
+ m.Groups[1].Value + "\n " + reposBlock.ToString(), 1);
+ Debug.Log($"{TAG} Injected repositories block");
+ }
+ else
+ {
+ Debug.LogWarning($"{TAG} Could not find repositories block, adding one");
+ var applyPattern = new Regex(@"(apply plugin:\s*'com\.android\.library'[^\n]*\n)");
+ if (applyPattern.IsMatch(content))
+ {
+ content = applyPattern.Replace(content, m =>
+ m.Groups[1].Value + "\nrepositories {\n " + reposBlock.ToString() + "}\n", 1);
+ }
+ }
+
+ return content;
+ }
+
+ private string InjectDependencies(string content, bool enableCsj, bool enableGdt, bool enableIqy)
+ {
+ // Check if our deps are already injected
+ if (content.Contains(DIRICHLET_DEPS_START))
+ {
+ return content;
+ }
+
+ var depsBlock = new StringBuilder();
+ depsBlock.AppendLine(DIRICHLET_DEPS_START);
+
+ // Core Mediation AAR
+ depsBlock.AppendLine(" implementation(name: 'DirichletAD_Mediation_4.2.5.0', ext: 'aar')");
+
+ // CSJ (穿山甲) Adapter and SDK
+ if (enableCsj)
+ {
+ depsBlock.AppendLine(" implementation(name: 'DirichletAD_CSJ_Adapter_4.2.5.0', ext: 'aar')");
+ depsBlock.AppendLine(" implementation(name: 'open_ad_sdk_7.4.2.2', ext: 'aar')");
+ }
+
+ // GDT (广点通) Adapter and SDK
+ if (enableGdt)
+ {
+ depsBlock.AppendLine(" implementation(name: 'DirichletAD_GDT_Adapter_4.2.5.0', ext: 'aar')");
+ depsBlock.AppendLine(" implementation(name: 'GDTSDK.unionNormal.4.671.1541', ext: 'aar')");
+ }
+
+ // IQY (爱奇艺) Adapter and SDK
+ if (enableIqy)
+ {
+ depsBlock.AppendLine(" implementation(name: 'DirichletAD_IQY_Adapter_4.2.5.0', ext: 'aar')");
+ depsBlock.AppendLine(" implementation(name: 'iadsdk-release-2.3.102.110', ext: 'aar')");
+ depsBlock.AppendLine(" implementation 'com.android.support.constraint:constraint-layout:1.1.3'");
+ }
+
+ // Maven dependencies (required for SDK functionality)
+ depsBlock.AppendLine(" implementation 'com.android.support:recyclerview-v7:28.0.0'");
+ depsBlock.AppendLine(" implementation 'com.github.bumptech.glide:glide:4.9.0'");
+ depsBlock.AppendLine(" implementation 'com.android.support:support-v4:28.0.0'");
+ depsBlock.AppendLine(" implementation 'com.android.support:support-annotations:28.0.0'");
+ depsBlock.AppendLine(" implementation 'com.android.support:appcompat-v7:28.0.0'");
+ depsBlock.AppendLine(" implementation 'com.squareup.okhttp3:okhttp:3.12.1'");
+
+ depsBlock.AppendLine($" {DIRICHLET_DEPS_END}");
+
+ // Find dependencies block and inject after opening brace
+ var depsPattern = new Regex(@"(dependencies\s*\{)");
+ if (depsPattern.IsMatch(content))
+ {
+ content = depsPattern.Replace(content, m =>
+ m.Groups[1].Value + "\n " + depsBlock.ToString(), 1);
+ Debug.Log($"{TAG} Injected dependencies block");
+ }
+ else
+ {
+ Debug.LogWarning($"{TAG} Could not find dependencies block");
+ }
+
+ return content;
+ }
+
+ private void ProcessSettingsGradle(string projectPath, bool enableCsj, bool enableGdt, bool enableIqy)
+ {
+ var parentDir = Directory.GetParent(projectPath)?.FullName;
+ if (string.IsNullOrEmpty(parentDir))
+ {
+ Debug.LogWarning($"{TAG} Could not get parent directory");
+ return;
+ }
+
+ var settingsPath = Path.Combine(parentDir, "settings.gradle");
+ if (!File.Exists(settingsPath))
+ {
+ Debug.LogWarning($"{TAG} Could not find settings.gradle at {settingsPath}");
+ return;
+ }
+
+ var content = File.ReadAllText(settingsPath);
+
+ // Check if already injected
+ if (content.Contains(DIRICHLET_REPOS_START))
+ {
+ Debug.Log($"{TAG} settings.gradle already has Dirichlet repos");
+ return;
+ }
+
+ // Remove any previously injected content
+ content = RemoveInjectedContent(content, DIRICHLET_REPOS_START, DIRICHLET_REPOS_END);
+
+ var reposBlock = new StringBuilder();
+ reposBlock.AppendLine(DIRICHLET_REPOS_START);
+ reposBlock.AppendLine(" google()");
+ reposBlock.AppendLine(" mavenCentral()");
+ reposBlock.AppendLine(" flatDir {");
+ reposBlock.AppendLine(" dirs \"${project(':unityLibrary').projectDir}/libs\", \"${project(':unityLibrary').projectDir}/DirichletMediation/libs\"");
+ reposBlock.AppendLine(" }");
+
+ if (enableCsj)
+ {
+ reposBlock.AppendLine(" maven { url 'https://artifact.bytedance.com/repository/pangle' }");
+ }
+ if (enableGdt)
+ {
+ reposBlock.AppendLine(" maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }");
+ }
+
+ reposBlock.AppendLine($" {DIRICHLET_REPOS_END}");
+
+ // Find dependencyResolutionManagement repositories block
+ var reposPattern = new Regex(@"(dependencyResolutionManagement\s*\{[\s\S]*?repositories\s*\{)");
+ if (reposPattern.IsMatch(content))
+ {
+ content = reposPattern.Replace(content, m =>
+ m.Groups[1].Value + "\n " + reposBlock.ToString(), 1);
+ File.WriteAllText(settingsPath, content);
+ Debug.Log($"{TAG} Updated settings.gradle with Dirichlet Mediation repositories");
+ }
+ else
+ {
+ Debug.LogWarning($"{TAG} Could not find dependencyResolutionManagement repositories block in settings.gradle");
+ }
+ }
+
+ private string RemoveInjectedContent(string content, string startMarker, string endMarker)
+ {
+ var pattern = new Regex($@"\s*{Regex.Escape(startMarker)}[\s\S]*?{Regex.Escape(endMarker)}\s*");
+ return pattern.Replace(content, "\n");
+ }
+ }
+}
+#endif
diff --git a/DirichletMediation/Editor/DirichletGradlePostProcessor.cs.meta b/DirichletMediation/Editor/DirichletGradlePostProcessor.cs.meta
new file mode 100644
index 0000000..e35ef36
--- /dev/null
+++ b/DirichletMediation/Editor/DirichletGradlePostProcessor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a9018fbefe0e148e0843d119260e4c8f
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/DirichletMediation/Editor/DirichletMediationDependencies.xml b/DirichletMediation/Editor/DirichletMediationDependencies.xml
new file mode 100644
index 0000000..a2db33c
--- /dev/null
+++ b/DirichletMediation/Editor/DirichletMediationDependencies.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+ https://maven.google.com
+
+
+
+
+
+ https://maven.google.com
+
+
+
+
+
+ https://maven.google.com
+
+
+
+
+
+ https://maven.google.com
+
+
+
+
+
+
+ https://repo.maven.apache.org/maven2
+
+
+
+
+
+ https://repo.maven.apache.org/maven2
+
+
+
+
+
+ https://maven.google.com
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DirichletMediation/Editor/DirichletMediationDependencies.xml.meta b/DirichletMediation/Editor/DirichletMediationDependencies.xml.meta
new file mode 100644
index 0000000..801cae7
--- /dev/null
+++ b/DirichletMediation/Editor/DirichletMediationDependencies.xml.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 414bdf38f1dc04fbd9e3d14650f42ada
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/DirichletMediation/Editor/DirichletMediationIOSPostProcessor.cs b/DirichletMediation/Editor/DirichletMediationIOSPostProcessor.cs
new file mode 100644
index 0000000..cf8c4f2
--- /dev/null
+++ b/DirichletMediation/Editor/DirichletMediationIOSPostProcessor.cs
@@ -0,0 +1,854 @@
+#if UNITY_IOS
+using System.IO;
+using System.Linq;
+using System.Text;
+using UnityEditor;
+using UnityEditor.Callbacks;
+using UnityEditor.iOS.Xcode;
+using UnityEditor.iOS.Xcode.Extensions;
+using UnityEngine;
+
+namespace Dirichlet.Mediation.Editor
+{
+ ///
+ /// iOS build post-processor for Dirichlet Mediation SDK.
+ /// Generates dynamic Podfile based on adapter settings and runs CocoaPods installation.
+ ///
+ /// 重构说明(2026-02-05):
+ /// - 不再在 Podfile 内做 target 猜测,改为 Post Process 直接从 pbxproj 读取真实 target 名称
+ /// - 解析失败时直接中断并输出可诊断信息,不再静默兜底
+ ///
+ /// 修复(2026-03-12):
+ /// - 所有 pods 统一放到 Framework target(UnityFramework),不再分层
+ /// - 原因:SDK 通过 NSClassFromString 动态查找 Adapter 类,Adapter 必须与 SDK 在同一 target,
+ /// 否则静态库符号会被 strip 导致运行时找不到适配器类
+ ///
+ public class DirichletMediationIOSPostProcessor
+ {
+ private const string SDKVersion = "4.2.0.2";
+ private const string MinIOSVersion = "11.0";
+
+ // 环境变量 override(仅在解析失败或接入方工程极端定制时使用)
+ private const string ENV_FRAMEWORK_TARGET = "DIRICHLET_UNITY_FRAMEWORK_TARGET";
+ private const string ENV_APP_TARGET = "DIRICHLET_UNITY_APP_TARGET";
+
+ ///
+ /// 解析出的 target 信息
+ ///
+ private class TargetInfo
+ {
+ public string FrameworkTargetName { get; set; }
+ public string AppTargetName { get; set; }
+ public string FrameworkTargetGuid { get; set; }
+ public string AppTargetGuid { get; set; }
+ }
+
+ [PostProcessBuild(100)]
+ public static void OnPostprocessBuild(BuildTarget buildTarget, string pathToBuiltProject)
+ {
+ if (buildTarget != BuildTarget.iOS)
+ {
+ return;
+ }
+
+ Debug.Log("[DirichletMediation] Starting iOS post-process...");
+
+ try
+ {
+ // 确保默认值:iOS 适配器默认勾选(避免历史版本的 EditorPrefs 默认值导致“默认不勾选”)
+ EnsureDefaultAdapterPrefs();
+
+ // 0. 解析 target 信息(核心改动:不做猜测,直接从 pbxproj 读取)
+ var targetInfo = ResolveTargetInfo(pathToBuiltProject);
+
+ // 1. Generate Podfile dynamically based on adapter settings and resolved targets
+ GeneratePodfile(pathToBuiltProject, targetInfo);
+
+ // 2. Modify Xcode project settings (before pod install)
+ ModifyXcodeProject(pathToBuiltProject, targetInfo);
+
+ // 3. Modify Info.plist for required permissions
+ ModifyInfoPlist(pathToBuiltProject);
+
+ // 4. Run pod install
+ RunPodInstall(pathToBuiltProject);
+
+ // 5. Embed GDT dynamic frameworks into app target (must run after pod install)
+ EmbedGDTDynamicFrameworks(pathToBuiltProject, targetInfo);
+
+ Debug.Log("[DirichletMediation] iOS post-process completed successfully.");
+ }
+ catch (System.Exception ex)
+ {
+ Debug.LogError($"[DirichletMediation] iOS post-process failed: {ex.Message}");
+ Debug.LogError($"[DirichletMediation] Stack trace: {ex.StackTrace}");
+ throw; // 重新抛出,让 Unity 构建失败(不静默跳过)
+ }
+ }
+
+ ///
+ /// 从 pbxproj 解析出 framework target 和 app target 的真实名称
+ /// 不做猜测,解析失败时直接中断
+ ///
+ private static TargetInfo ResolveTargetInfo(string projectPath)
+ {
+ Debug.Log("[DirichletMediation] Resolving target info from pbxproj...");
+
+ // 检查环境变量 override
+ var envFrameworkTarget = System.Environment.GetEnvironmentVariable(ENV_FRAMEWORK_TARGET);
+ var envAppTarget = System.Environment.GetEnvironmentVariable(ENV_APP_TARGET);
+
+ if (!string.IsNullOrEmpty(envFrameworkTarget) && !string.IsNullOrEmpty(envAppTarget))
+ {
+ Debug.Log($"[DirichletMediation] Using targets from environment variables:");
+ Debug.Log($" Framework target: {envFrameworkTarget}");
+ Debug.Log($" App target: {envAppTarget}");
+ return new TargetInfo
+ {
+ FrameworkTargetName = envFrameworkTarget,
+ AppTargetName = envAppTarget,
+ // GUID 在需要时从 pbxproj 反查
+ };
+ }
+
+ // 查找 .xcodeproj 文件
+ var xcodeProjectName = DetectXcodeProjectName(projectPath);
+ var projectFilePath = Path.Combine(projectPath, $"{xcodeProjectName}.xcodeproj/project.pbxproj");
+
+ if (!File.Exists(projectFilePath))
+ {
+ throw new System.Exception($"project.pbxproj not found at: {projectFilePath}");
+ }
+
+ var pbxProject = new PBXProject();
+ pbxProject.ReadFromFile(projectFilePath);
+
+#if UNITY_2019_3_OR_NEWER
+ var frameworkGuid = pbxProject.GetUnityFrameworkTargetGuid();
+ var appGuid = pbxProject.GetUnityMainTargetGuid();
+#else
+ // Unity 2019.3 之前只有单一 target
+ var frameworkGuid = pbxProject.TargetGuidByName("Unity-iPhone");
+ var appGuid = frameworkGuid;
+#endif
+
+ // 直接从 pbxproj 解析 GUID -> name 映射(最稳健,避免 TargetGuidByName 行为差异)
+ var nativeTargetMap = GetNativeTargetGuidToNameMap(projectFilePath);
+
+ if (string.IsNullOrEmpty(frameworkGuid) || string.IsNullOrEmpty(appGuid))
+ {
+ var allTargets = nativeTargetMap.Values.Distinct().ToArray();
+ var targetList = allTargets.Any() ? string.Join(", ", allTargets) : "(none)";
+ throw new System.Exception(
+ $"Unity PBXProject API returned empty target GUID.\n" +
+ $" .xcodeproj: {xcodeProjectName}\n" +
+ $" Available targets: {targetList}\n" +
+ $" Framework GUID: {frameworkGuid ?? "(null)"}\n" +
+ $" App GUID: {appGuid ?? "(null)"}\n\n" +
+ $"解决方案:\n" +
+ $" 1. 升级 Unity 到 2019.3+(或团结引擎对应版本),确保导出包含 UnityFramework/App 双 target\n" +
+ $" 2. 或使用环境变量强制指定 target:\n" +
+ $" export {ENV_FRAMEWORK_TARGET}=YourFrameworkTarget\n" +
+ $" export {ENV_APP_TARGET}=YourAppTarget"
+ );
+ }
+
+ // 从 GUID 反查 target name
+ nativeTargetMap.TryGetValue(frameworkGuid, out var frameworkTargetName);
+ nativeTargetMap.TryGetValue(appGuid, out var appTargetName);
+
+ // 兜底:某些 Unity 版本可能返回非 PBXNativeTarget GUID,这时再尝试旧逻辑反查
+ frameworkTargetName = frameworkTargetName ?? GetTargetNameByGuid(pbxProject, frameworkGuid, projectFilePath);
+ appTargetName = appTargetName ?? GetTargetNameByGuid(pbxProject, appGuid, projectFilePath);
+
+ // 验证解析结果
+ if (string.IsNullOrEmpty(frameworkTargetName) || string.IsNullOrEmpty(appTargetName))
+ {
+ // 输出所有 targets 帮助诊断
+ var allTargets = nativeTargetMap.Values.Distinct().ToArray();
+ var targetList = allTargets.Any() ? string.Join(", ", allTargets) : "(none)";
+
+ throw new System.Exception(
+ $"Failed to resolve target names from pbxproj.\n" +
+ $" .xcodeproj: {xcodeProjectName}\n" +
+ $" Available targets: {targetList}\n" +
+ $" Framework GUID: {frameworkGuid ?? "(null)"}\n" +
+ $" App GUID: {appGuid ?? "(null)"}\n\n" +
+ $"解决方案:\n" +
+ $" 1. 设置环境变量强制指定 target:\n" +
+ $" export {ENV_FRAMEWORK_TARGET}=YourFrameworkTarget\n" +
+ $" export {ENV_APP_TARGET}=YourAppTarget\n" +
+ $" 2. 检查导出工程是否包含 UnityFramework 和 Unity-iPhone(或对应的团结引擎 target)"
+ );
+ }
+
+ Debug.Log($"[DirichletMediation] Resolved targets:");
+ Debug.Log($" Framework target: {frameworkTargetName} (GUID: {frameworkGuid})");
+ Debug.Log($" App target: {appTargetName} (GUID: {appGuid})");
+
+ return new TargetInfo
+ {
+ FrameworkTargetName = frameworkTargetName,
+ AppTargetName = appTargetName,
+ FrameworkTargetGuid = frameworkGuid,
+ AppTargetGuid = appGuid
+ };
+ }
+
+ ///
+ /// 从 GUID 反查 target name
+ /// Unity PBXProject API 没有直接提供此方法,需要通过 TargetGuidByName 反向验证
+ ///
+ private static string GetTargetNameByGuid(PBXProject pbxProject, string targetGuid, string projectFilePath)
+ {
+ if (string.IsNullOrEmpty(targetGuid))
+ {
+ return null;
+ }
+
+ // 最可靠的方式:直接从 pbxproj 解析 GUID -> name
+ var nativeTargetMap = GetNativeTargetGuidToNameMap(projectFilePath);
+ if (nativeTargetMap.TryGetValue(targetGuid, out var parsedName) && !string.IsNullOrEmpty(parsedName))
+ {
+ return parsedName;
+ }
+
+ // 常见的 Unity/Tuanjie target 名称
+ var commonTargetNames = new[]
+ {
+ "UnityFramework", "Unity-iPhone",
+ "TuanjieFramework", "Tuanjie-iPhone",
+ "GameAssembly", // 部分定制工程
+ };
+
+ foreach (var name in commonTargetNames)
+ {
+ try
+ {
+ var guid = pbxProject.TargetGuidByName(name);
+ if (guid == targetGuid)
+ {
+ return name;
+ }
+ }
+ catch
+ {
+ // Target 不存在,继续尝试下一个
+ }
+ }
+
+ // 如果常见名称都不匹配,尝试从文件中解析所有 target
+ var allTargets = GetAllTargetNames(projectFilePath);
+ foreach (var name in allTargets)
+ {
+ try
+ {
+ var guid = pbxProject.TargetGuidByName(name);
+ if (guid == targetGuid)
+ {
+ return name;
+ }
+ }
+ catch
+ {
+ // 继续尝试
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// 从 project.pbxproj 解析 PBXNativeTarget 的 GUID -> name 映射
+ ///
+ private static System.Collections.Generic.Dictionary GetNativeTargetGuidToNameMap(string projectFilePath)
+ {
+ var result = new System.Collections.Generic.Dictionary(System.StringComparer.OrdinalIgnoreCase);
+
+ try
+ {
+ // 形如:
+ // 9D25AB9C213FB47800354C27 /* UnityFramework */ = {
+ var entryStartRegex = new System.Text.RegularExpressions.Regex(
+ @"^\s*([0-9A-Fa-f]{24})\s*/\*\s*(.*?)\s*\*/\s*=\s*\{\s*$",
+ System.Text.RegularExpressions.RegexOptions.CultureInvariant
+ );
+
+ string currentGuid = null;
+ string currentName = null;
+ var braceDepth = 0;
+ var isNativeTarget = false;
+
+ foreach (var line in File.ReadLines(projectFilePath))
+ {
+ if (braceDepth == 0)
+ {
+ var m = entryStartRegex.Match(line);
+ if (!m.Success)
+ {
+ continue;
+ }
+
+ currentGuid = m.Groups[1].Value.Trim();
+ currentName = m.Groups[2].Value.Trim();
+ braceDepth = CountBraceDelta(line); // start line includes '{'
+ isNativeTarget = false;
+ continue;
+ }
+
+ if (!isNativeTarget && line.IndexOf("isa = PBXNativeTarget;", System.StringComparison.Ordinal) >= 0)
+ {
+ isNativeTarget = true;
+ }
+
+ braceDepth += CountBraceDelta(line);
+
+ if (braceDepth <= 0)
+ {
+ if (isNativeTarget && !string.IsNullOrEmpty(currentGuid) && !string.IsNullOrEmpty(currentName))
+ {
+ result[currentGuid] = currentName;
+ }
+
+ currentGuid = null;
+ currentName = null;
+ braceDepth = 0;
+ isNativeTarget = false;
+ }
+ }
+ }
+ catch
+ {
+ // ignore
+ }
+
+ return result;
+ }
+
+ private static int CountBraceDelta(string line)
+ {
+ if (string.IsNullOrEmpty(line))
+ {
+ return 0;
+ }
+
+ var delta = 0;
+ foreach (var c in line)
+ {
+ if (c == '{') delta++;
+ else if (c == '}') delta--;
+ }
+ return delta;
+ }
+
+ ///
+ /// 从 pbxproj 文件解析所有 target 名称(用于诊断输出)
+ ///
+ private static string[] GetAllTargetNames(string projectFilePath)
+ {
+ var map = GetNativeTargetGuidToNameMap(projectFilePath);
+ return map.Values.Distinct().ToArray();
+ }
+
+ ///
+ /// 生成 Podfile
+ /// 所有 pods 统一放到 Framework target(UnityFramework),因为:
+ /// - Bridge (.mm) 和 DirichletMediationSDK 都编译/运行在 Framework target 中
+ /// - SDK 通过 NSClassFromString 动态查找 Adapter 类,Adapter 必须与 SDK 在同一 target
+ /// 否则静态库符号可能被 strip 或不在同一二进制中,导致运行时找不到类
+ ///
+ private static void GeneratePodfile(string projectPath, TargetInfo targetInfo)
+ {
+ // Read adapter settings from EditorPrefs
+ // Note: DirichletAdSDK (DRA adapter) is always enabled as core SDK
+ var enableCsj = EditorPrefs.GetBool("Dirichlet.iOS.EnableCSJ", true);
+ var enableGdt = EditorPrefs.GetBool("Dirichlet.iOS.EnableGDT", true);
+
+ var podfileContent = new StringBuilder();
+ podfileContent.AppendLine("# Generated by Dirichlet Mediation Unity Plugin");
+ podfileContent.AppendLine("#");
+ podfileContent.AppendLine($"# Framework target: {targetInfo.FrameworkTargetName}");
+ podfileContent.AppendLine($"# App target: {targetInfo.AppTargetName}");
+ podfileContent.AppendLine();
+ podfileContent.AppendLine("source 'https://cdn.cocoapods.org/'");
+ podfileContent.AppendLine();
+ podfileContent.AppendLine($"platform :ios, '{MinIOSVersion}'");
+ podfileContent.AppendLine("use_frameworks! :linkage => :static");
+ podfileContent.AppendLine();
+
+ // 所有 pods 统一放到 Framework target
+ // SDK 通过 NSClassFromString 查找 Adapter 类,必须在同一 target 中
+ podfileContent.AppendLine($"target '{targetInfo.FrameworkTargetName}' do");
+ podfileContent.AppendLine($" pod 'DirichletMediationSDK', '{SDKVersion}'");
+
+ if (enableCsj)
+ {
+ podfileContent.AppendLine($" pod 'DirichletMediationAdapterCSJ', '{SDKVersion}'");
+ }
+
+ if (enableGdt)
+ {
+ podfileContent.AppendLine($" pod 'DirichletMediationAdapterGDT', '{SDKVersion}'");
+ }
+
+ // DirichletAdSDK (DRA adapter) is always included as core SDK
+ podfileContent.AppendLine($" pod 'DirichletMediationAdapterDRA', '{SDKVersion}'");
+
+ podfileContent.AppendLine("end");
+ podfileContent.AppendLine();
+
+ // Post-install: 基础构建设置
+ podfileContent.AppendLine("post_install do |installer|");
+ podfileContent.AppendLine(" installer.pods_project.targets.each do |target|");
+ podfileContent.AppendLine(" target.build_configurations.each do |config|");
+ podfileContent.AppendLine($" config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '{MinIOSVersion}'");
+ podfileContent.AppendLine(" config.build_settings['ENABLE_BITCODE'] = 'NO'");
+ podfileContent.AppendLine(" end");
+ podfileContent.AppendLine(" end");
+ podfileContent.AppendLine("end");
+
+ var podfilePath = Path.Combine(projectPath, "Podfile");
+ File.WriteAllText(podfilePath, podfileContent.ToString());
+
+ Debug.Log($"[DirichletMediation] Generated Podfile at {podfilePath}");
+ Debug.Log($"[DirichletMediation] All pods allocated to {targetInfo.FrameworkTargetName} (CSJ={enableCsj}, GDT={enableGdt}, DRA=always)");
+ }
+
+ ///
+ /// 检测 Xcode 项目名称,兼容 Unity/Tuanjie/自定义项目
+ /// 动态搜索目录下的 .xcodeproj 文件
+ ///
+ private static string DetectXcodeProjectName(string projectPath)
+ {
+ // 搜索所有 .xcodeproj 目录
+ var xcodeprojDirs = Directory.GetDirectories(projectPath, "*.xcodeproj");
+
+ if (xcodeprojDirs.Length == 0)
+ {
+ throw new System.Exception(
+ $"No .xcodeproj found in: {projectPath}\n" +
+ "请确认 Unity 导出路径正确,且导出已完成。"
+ );
+ }
+
+ // 获取第一个 xcodeproj 的名称(不含扩展名)
+ var xcodeprojPath = xcodeprojDirs[0];
+ var xcodeprojName = Path.GetFileNameWithoutExtension(xcodeprojPath);
+
+ Debug.Log($"[DirichletMediation] Detected Xcode project: {xcodeprojName}");
+ return xcodeprojName;
+ }
+
+ ///
+ /// 修改 Xcode 工程设置(使用已解析的 target 信息)
+ ///
+ private static void ModifyXcodeProject(string projectPath, TargetInfo targetInfo)
+ {
+ var xcodeProjectName = DetectXcodeProjectName(projectPath);
+ var projectFilePath = Path.Combine(projectPath, $"{xcodeProjectName}.xcodeproj/project.pbxproj");
+ var pbxProject = new PBXProject();
+ pbxProject.ReadFromFile(projectFilePath);
+
+ // 使用已解析的 target GUID,或通过名称反查
+ var targetGuid = targetInfo.FrameworkTargetGuid;
+ var mainTargetGuid = targetInfo.AppTargetGuid;
+
+ if (string.IsNullOrEmpty(targetGuid))
+ {
+ targetGuid = pbxProject.TargetGuidByName(targetInfo.FrameworkTargetName);
+ }
+ if (string.IsNullOrEmpty(mainTargetGuid))
+ {
+ mainTargetGuid = pbxProject.TargetGuidByName(targetInfo.AppTargetName);
+ }
+
+ Debug.Log($"[DirichletMediation] Modifying Xcode project:");
+ Debug.Log($" Framework target: {targetInfo.FrameworkTargetName} (GUID: {targetGuid})");
+ Debug.Log($" App target: {targetInfo.AppTargetName} (GUID: {mainTargetGuid})");
+
+ // NOTE: System frameworks (AdSupport, AVFoundation, WebKit, CoreVideo, etc.)
+ // are declared in SDK podspecs and will be automatically linked by CocoaPods.
+ // No need to manually add them here.
+ // - DirichletAdSDK.podspec: AdSupport, SystemConfiguration, Security
+ // - DirichletCoreSDK.podspec: SystemConfiguration, Security
+ // - DirichletMediationAdapterCSJ.podspec: CoreVideo
+ // - Third-party SDKs (Ads-CN, GDTMobSDK) declare their own framework dependencies.
+
+ // Set build settings for framework target
+ pbxProject.SetBuildProperty(targetGuid, "ENABLE_BITCODE", "NO");
+ pbxProject.SetBuildProperty(targetGuid, "CLANG_ENABLE_MODULES", "YES");
+
+ // Set build settings for main target
+ pbxProject.SetBuildProperty(mainTargetGuid, "ENABLE_BITCODE", "NO");
+ pbxProject.SetBuildProperty(mainTargetGuid, "CLANG_ENABLE_MODULES", "YES");
+ pbxProject.SetBuildProperty(mainTargetGuid, "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "YES");
+
+ // Set LD_RUNPATH_SEARCH_PATHS to allow dynamic frameworks to be found at runtime
+ pbxProject.SetBuildProperty(mainTargetGuid, "LD_RUNPATH_SEARCH_PATHS", "$(inherited) @executable_path/Frameworks");
+ pbxProject.SetBuildProperty(targetGuid, "LD_RUNPATH_SEARCH_PATHS", "$(inherited) @executable_path/Frameworks @loader_path/Frameworks");
+
+ pbxProject.WriteToFile(projectFilePath);
+ Debug.Log("[DirichletMediation] Modified Xcode project settings");
+ }
+
+ ///
+ /// GDTMobSDK 提供的是预编译动态库(GDTMobSDK.framework, Tquic.framework),
+ /// 必须 embed 到 app bundle 中才能在运行时加载。
+ /// 由于所有 pods 都在 Framework target 上,CocoaPods 不会自动 embed 到 app target,
+ /// 需要在 pod install 之后手动处理。
+ /// 依赖 UnityEditor.iOS.Xcode.Extensions 中的 AddFileToEmbedFrameworks 扩展方法。
+ ///
+ private static void EmbedGDTDynamicFrameworks(string projectPath, TargetInfo targetInfo)
+ {
+ var enableGdt = EditorPrefs.GetBool("Dirichlet.iOS.EnableGDT", true);
+ if (!enableGdt)
+ {
+ Debug.Log("[DirichletMediation] GDT adapter disabled, skipping dynamic framework embedding");
+ return;
+ }
+
+ var xcodeProjectName = DetectXcodeProjectName(projectPath);
+ var projectFilePath = Path.Combine(projectPath, $"{xcodeProjectName}.xcodeproj/project.pbxproj");
+ var pbxProject = new PBXProject();
+ pbxProject.ReadFromFile(projectFilePath);
+
+ var mainTargetGuid = targetInfo.AppTargetGuid;
+ if (string.IsNullOrEmpty(mainTargetGuid))
+ {
+ mainTargetGuid = pbxProject.TargetGuidByName(targetInfo.AppTargetName);
+ }
+
+ // GDTMobSDK 的动态框架列表
+ var dynamicFrameworkNames = new[] { "GDTMobSDK.framework", "Tquic.framework" };
+ var podsDir = Path.Combine(projectPath, "Pods");
+ var embedded = 0;
+
+ foreach (var frameworkName in dynamicFrameworkNames)
+ {
+ var frameworkPath = FindDynamicFramework(podsDir, frameworkName);
+ if (string.IsNullOrEmpty(frameworkPath))
+ {
+ Debug.LogWarning($"[DirichletMediation] Dynamic framework not found: {frameworkName}");
+ continue;
+ }
+
+ var relativePath = frameworkPath.StartsWith(projectPath)
+ ? frameworkPath.Substring(projectPath.Length + 1)
+ : frameworkPath;
+
+ var fileGuid = pbxProject.AddFile(relativePath, "Frameworks/" + frameworkName);
+ PBXProjectExtensions.AddFileToEmbedFrameworks(pbxProject, mainTargetGuid, fileGuid);
+ embedded++;
+ Debug.Log($"[DirichletMediation] Embedded dynamic framework: {frameworkName}");
+ }
+
+ if (embedded > 0)
+ {
+ pbxProject.WriteToFile(projectFilePath);
+ Debug.Log($"[DirichletMediation] Embedded {embedded} GDT dynamic frameworks into {targetInfo.AppTargetName}");
+ }
+ }
+
+ ///
+ /// 在 Pods 目录中递归搜索指定的 .framework(优先 ios-arm64 真机 slice)
+ ///
+ private static string FindDynamicFramework(string podsDir, string frameworkName)
+ {
+ if (!Directory.Exists(podsDir))
+ {
+ return null;
+ }
+
+ var candidates = Directory.GetDirectories(podsDir, frameworkName, SearchOption.AllDirectories);
+
+ foreach (var candidate in candidates)
+ {
+ if (candidate.Contains("ios-arm64") && !candidate.Contains("simulator"))
+ {
+ return candidate;
+ }
+ }
+
+ foreach (var candidate in candidates)
+ {
+ if (!candidate.Contains("simulator"))
+ {
+ return candidate;
+ }
+ }
+
+ return candidates.Length > 0 ? candidates[0] : null;
+ }
+
+ private static void ModifyInfoPlist(string projectPath)
+ {
+ var plistPath = Path.Combine(projectPath, "Info.plist");
+ var plist = new PlistDocument();
+ plist.ReadFromFile(plistPath);
+
+ var rootDict = plist.root;
+
+ // Note: The following Info.plist keys should be configured by the developer manually
+ // to avoid potential App Store review issues:
+ // - NSAppTransportSecurity: Configure based on your app's network requirements
+ // - NSUserTrackingUsageDescription: Required for iOS 14+ IDFA access, use your custom description
+ // - NSLocationWhenInUseUsageDescription: Only add if your app uses location services
+ //
+ // Reference: https://ssp.dirichlet.cn/docs/dirichlet-mediation-sdk/dirichlet-mediation-sdk-guide-ios/
+
+ // Add SKAdNetwork identifiers for attribution tracking
+ AddSKAdNetworkIds(rootDict);
+
+ plist.WriteToFile(plistPath);
+ Debug.Log("[DirichletMediation] Modified Info.plist (SKAdNetwork IDs only)");
+ }
+
+ private static void AddSKAdNetworkIds(PlistElementDict rootDict)
+ {
+ if (rootDict.values.ContainsKey("SKAdNetworkItems"))
+ {
+ Debug.Log("[DirichletMediation] SKAdNetworkItems already exists, skipping");
+ return;
+ }
+
+ // Add SKAdNetwork IDs required by CSJ (Pangle/穿山甲)
+ // Reference: https://www.csjplatform.com/supportcenter/5377
+ var skAdNetworkArray = rootDict.CreateArray("SKAdNetworkItems");
+
+ var commonSkAdNetworkIds = new[]
+ {
+ "238da6jt44.skadnetwork", // 穿山甲 SKAdNetwork ID
+ "x2jnk7ly8j.skadnetwork", // 穿山甲 SKAdNetwork ID
+ "22mmun2rn5.skadnetwork" // 穿山甲 SKAdNetwork ID
+ };
+
+ foreach (var skAdNetworkId in commonSkAdNetworkIds)
+ {
+ var dict = skAdNetworkArray.AddDict();
+ dict.SetString("SKAdNetworkIdentifier", skAdNetworkId);
+ }
+
+ Debug.Log($"[DirichletMediation] Added {commonSkAdNetworkIds.Length} SKAdNetwork IDs to Info.plist");
+ }
+
+ private static void RunPodInstall(string projectPath)
+ {
+ var podfilePath = Path.Combine(projectPath, "Podfile");
+ if (!File.Exists(podfilePath))
+ {
+ Debug.LogWarning("[DirichletMediation] Podfile not found, skipping pod install");
+ return;
+ }
+
+ Debug.Log("[DirichletMediation] Running 'pod install'...");
+ Debug.Log("[DirichletMediation] Note: This may take a few minutes on first build or when adapters change.");
+
+ try
+ {
+ // Try to find pod executable
+ var podPath = FindPodExecutable();
+ if (string.IsNullOrEmpty(podPath))
+ {
+ Debug.LogWarning("[DirichletMediation] CocoaPods not found. Please install CocoaPods:");
+ Debug.LogWarning(" sudo gem install cocoapods");
+ Debug.LogWarning("Then run 'pod install' manually in:");
+ Debug.LogWarning($" {projectPath}");
+ return;
+ }
+
+ // 优先不做 repo update(更快、更稳定);如遇到“找不到 podspec”再退化为 --repo-update
+ var firstArgs = "install";
+ var exitCode = RunPodCommand(podPath, firstArgs, projectPath, out var output, out var error);
+
+ if (exitCode != 0 && ShouldRetryWithRepoUpdate(output, error))
+ {
+ Debug.LogWarning("[DirichletMediation] pod install failed (missing specs suspected). Retrying with --repo-update...");
+ var retryArgs = "install --repo-update";
+ exitCode = RunPodCommand(podPath, retryArgs, projectPath, out output, out error);
+ }
+
+ if (exitCode == 0)
+ {
+ Debug.Log("[DirichletMediation] pod install completed successfully");
+ if (!string.IsNullOrEmpty(output) && output.Length < 2000)
+ {
+ Debug.Log($"Output:\n{output}");
+ }
+ return;
+ }
+
+ Debug.LogError($"[DirichletMediation] pod install failed with exit code {exitCode}");
+ if (!string.IsNullOrEmpty(error))
+ {
+ Debug.LogError($"Error:\n{error}");
+ }
+ if (!string.IsNullOrEmpty(output))
+ {
+ Debug.LogError($"Output:\n{output}");
+ }
+
+ if (IsCocoaPodsCdnHttp2Error(output, error))
+ {
+ Debug.LogWarning(
+ "[DirichletMediation] Detected CocoaPods CDN HTTP/2 error (e.g. 'Error in the HTTP2 framing layer'). " +
+ "This is usually a network/proxy/SSL issue. Try rerun without repo update, switch network, upgrade CocoaPods, " +
+ "or run 'pod install' manually. (Repo doc: docs/FAQ/CocoaPods-SSL证书问题排查SOP.md)"
+ );
+ }
+
+ // 失败即中断:避免导出工程处于“半配置”状态,后续 Xcode build 更难排查
+ throw new System.Exception($"pod install failed with exit code {exitCode}");
+ }
+ catch (System.Exception ex)
+ {
+ Debug.LogError($"[DirichletMediation] Failed to run pod install: {ex.Message}");
+ Debug.LogWarning("[DirichletMediation] Please run 'pod install' manually in the Xcode project directory:");
+ Debug.LogWarning($" cd {projectPath}");
+ Debug.LogWarning(" pod install");
+ throw;
+ }
+ }
+
+ private static int RunPodCommand(string podPath, string arguments, string projectPath, out string output, out string error)
+ {
+ var processInfo = new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = podPath,
+ Arguments = arguments,
+ WorkingDirectory = projectPath,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+
+ // Set UTF-8 encoding to avoid CocoaPods encoding issues
+ processInfo.EnvironmentVariables["LANG"] = "en_US.UTF-8";
+ processInfo.EnvironmentVariables["LC_ALL"] = "en_US.UTF-8";
+
+ using (var process = System.Diagnostics.Process.Start(processInfo))
+ {
+ output = process.StandardOutput.ReadToEnd();
+ error = process.StandardError.ReadToEnd();
+ process.WaitForExit();
+ return process.ExitCode;
+ }
+ }
+
+ private static bool ShouldRetryWithRepoUpdate(string output, string error)
+ {
+ var combined = (output ?? string.Empty) + "\n" + (error ?? string.Empty);
+
+ // 常见“本地 specs 未更新导致无法解析依赖”的报错关键字
+ return combined.Contains("Unable to find a specification for")
+ || combined.Contains("None of your spec sources contain a spec satisfying")
+ || combined.Contains("spec satisfying")
+ || combined.Contains("No podspec found for")
+ || combined.Contains("could not find compatible versions for pod");
+ }
+
+ private static bool IsCocoaPodsCdnHttp2Error(string output, string error)
+ {
+ var combined = (output ?? string.Empty) + "\n" + (error ?? string.Empty);
+ return combined.Contains("CDN: trunk Repo update failed")
+ || combined.Contains("Error in the HTTP2 framing layer")
+ || combined.Contains("URL couldn't be downloaded: https://cdn.cocoapods.org/");
+ }
+
+ private static string FindPodExecutable()
+ {
+ // 1. POD_BINARY 环境变量优先,方便 CI 或接入方手动配置
+ var envValue = System.Environment.GetEnvironmentVariable("POD_BINARY");
+ if (!string.IsNullOrEmpty(envValue))
+ {
+ var expanded = envValue.Trim();
+ if (File.Exists(expanded))
+ {
+ Debug.Log($"[DirichletMediation] Found pod via POD_BINARY: {expanded}");
+ return expanded;
+ }
+
+ Debug.LogWarning($"[DirichletMediation] POD_BINARY points to non-existing path: {expanded}");
+ }
+
+ var possiblePaths = new[]
+ {
+ "/usr/local/bin/pod",
+ "/opt/homebrew/bin/pod",
+ "/usr/bin/pod",
+ Path.Combine(System.Environment.GetEnvironmentVariable("HOME") ?? "", ".rbenv/shims/pod"),
+ Path.Combine(System.Environment.GetEnvironmentVariable("HOME") ?? "", ".rvm/wrappers/default/pod")
+ };
+
+ foreach (var path in possiblePaths)
+ {
+ if (File.Exists(path))
+ {
+ Debug.Log($"[DirichletMediation] Found pod at: {path}");
+ return path;
+ }
+ }
+
+ // Try using 'which pod'
+ try
+ {
+ var processInfo = new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = "/usr/bin/env",
+ Arguments = "which pod",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+
+ using (var process = System.Diagnostics.Process.Start(processInfo))
+ {
+ var output = process.StandardOutput.ReadToEnd().Trim();
+ var error = process.StandardError.ReadToEnd().Trim();
+ process.WaitForExit();
+
+ if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output))
+ {
+ Debug.Log($"[DirichletMediation] Found pod via 'which': {output}");
+ return output;
+ }
+
+ if (!string.IsNullOrEmpty(error))
+ {
+ Debug.LogWarning($"[DirichletMediation] 'which pod' failed: {error}");
+ }
+ }
+ }
+ catch (System.Exception ex)
+ {
+ Debug.LogWarning($"[DirichletMediation] Failed to resolve pod via 'which': {ex.Message}");
+ }
+
+ return null;
+ }
+
+ private const string PrefKeyDefaultsInitialized = "Dirichlet.Mediation.AdapterSettings.DefaultsInitialized.v1";
+
+ private static void EnsureDefaultAdapterPrefs()
+ {
+ if (EditorPrefs.GetBool(PrefKeyDefaultsInitialized, false))
+ {
+ return;
+ }
+
+ // 默认“勾上”:CSJ/GDT(iOS)
+ // 注意:如接入方需要关闭,可在 Adapter Settings 窗口中手动取消勾选。
+ EditorPrefs.SetBool("Dirichlet.iOS.EnableCSJ", true);
+ EditorPrefs.SetBool("Dirichlet.iOS.EnableGDT", true);
+
+ // 同时保证 Android 默认一致(无副作用)
+ EditorPrefs.SetBool("Dirichlet.Android.EnableCSJ", true);
+ EditorPrefs.SetBool("Dirichlet.Android.EnableGDT", true);
+
+ EditorPrefs.SetBool(PrefKeyDefaultsInitialized, true);
+ }
+ }
+}
+
+#endif
diff --git a/DirichletMediation/Editor/DirichletMediationIOSPostProcessor.cs.meta b/DirichletMediation/Editor/DirichletMediationIOSPostProcessor.cs.meta
new file mode 100644
index 0000000..0da8425
--- /dev/null
+++ b/DirichletMediation/Editor/DirichletMediationIOSPostProcessor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5bc3db08a1b434ef2bc8aba5413e588c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/DirichletMediation/Runtime.meta b/DirichletMediation/Runtime.meta
new file mode 100644
index 0000000..fe8311c
--- /dev/null
+++ b/DirichletMediation/Runtime.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 33121b74910d6437b86b802fedfe5af4
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/DirichletMediation/Runtime/Dirichlet.Mediation.Runtime.asmdef b/DirichletMediation/Runtime/Dirichlet.Mediation.Runtime.asmdef
new file mode 100644
index 0000000..91d5894
--- /dev/null
+++ b/DirichletMediation/Runtime/Dirichlet.Mediation.Runtime.asmdef
@@ -0,0 +1,15 @@
+{
+ "name": "Dirichlet.Mediation.Runtime",
+ "references": [],
+ "includePlatforms": [],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
+
+
diff --git a/DirichletMediation/Runtime/Dirichlet.Mediation.Runtime.asmdef.meta b/DirichletMediation/Runtime/Dirichlet.Mediation.Runtime.asmdef.meta
new file mode 100644
index 0000000..dcf1729
--- /dev/null
+++ b/DirichletMediation/Runtime/Dirichlet.Mediation.Runtime.asmdef.meta
@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 556788bfca0f498d945d7fa53061b066
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+
+
diff --git a/DirichletMediation/Runtime/DirichletAdTypes.cs b/DirichletMediation/Runtime/DirichletAdTypes.cs
new file mode 100644
index 0000000..7dbfcd4
--- /dev/null
+++ b/DirichletMediation/Runtime/DirichletAdTypes.cs
@@ -0,0 +1,1376 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using UnityEngine;
+
+namespace Dirichlet.Mediation
+{
+ ///
+ /// Platform handle for ad instances. This is a simple wrapper around the handle string
+ /// returned from native bridge, matching the native SDK pattern where ad objects are
+ /// returned directly from loadXXXAd() methods.
+ ///
+ public sealed class DirichletPlatformAdHandle
+ {
+ public string DebugId { get; }
+
+ internal DirichletPlatformAdHandle(string debugId)
+ {
+ DebugId = debugId ?? throw new ArgumentNullException(nameof(debugId));
+ }
+
+ internal static DirichletPlatformAdHandle FromNative(string handleId)
+ {
+ if (string.IsNullOrEmpty(handleId))
+ {
+ handleId = Guid.NewGuid().ToString("N");
+ }
+ return new DirichletPlatformAdHandle(handleId);
+ }
+
+ internal static DirichletPlatformAdHandle CreateStub()
+ {
+ return new DirichletPlatformAdHandle(Guid.NewGuid().ToString("N"));
+ }
+
+ public override string ToString() => $"DirichletPlatformAdHandle(DebugId={DebugId})";
+ }
+
+ public enum DirichletAdType
+ {
+ RewardVideo = 0,
+ Interstitial = 1,
+ Banner = 2,
+ Splash = 3,
+ ExpressFeed = 4,
+ NativeFeed = 5
+ }
+
+ ///
+ /// Mirrors the Android-side DirichletAdManager to keep configuration state and create ad natives.
+ ///
+ public static class DirichletAdManager
+ {
+ private static readonly object StateLock = new object();
+ private static DirichletAdConfig currentConfig;
+
+ ///
+ /// Returns the last configuration applied to the native SDK.
+ ///
+ public static DirichletAdConfig CurrentConfig
+ {
+ get
+ {
+ lock (StateLock)
+ {
+ return currentConfig;
+ }
+ }
+ }
+
+ internal static void ApplyConfig(DirichletAdConfig config)
+ {
+ lock (StateLock)
+ {
+ currentConfig = config;
+ }
+ }
+
+ internal static void Clear()
+ {
+ lock (StateLock)
+ {
+ currentConfig = null;
+ }
+ }
+
+ ///
+ /// Creates a Unity-side DirichletAdNative wrapper that mirrors the Android aggregator API.
+ ///
+ public static DirichletAdNative CreateAdNative()
+ {
+ return new DirichletAdNative(DirichletSdk.GetBridge());
+ }
+
+ internal static DirichletAdNative CreateAdNative(IDirichletPlatformBridge bridge)
+ {
+ return new DirichletAdNative(bridge);
+ }
+ }
+
+ ///
+ /// Thin wrapper around native ad handles provided by the platform bridge.
+ ///
+
+ public sealed class DirichletAdRequest
+ {
+ public long SpaceId { get; }
+ public string Extra1 { get; }
+ public string UserId { get; }
+ public string RewardName { get; }
+ public int? RewardAmount { get; }
+ public string Query { get; }
+ public int? ExpressViewWidth { get; }
+ public int? ExpressViewHeight { get; }
+ public int? ExpressImageWidth { get; }
+ public int? ExpressImageHeight { get; }
+ public long? MinaId { get; }
+
+ ///
+ /// Banner auto-ad slide rotation interval, in seconds. Native default is 30.
+ ///
+ public int? SlideInterval { get; }
+
+ private DirichletAdRequest(Builder builder)
+ {
+ SpaceId = builder.spaceId;
+ Extra1 = builder.extra1;
+ UserId = builder.userId;
+ RewardName = builder.rewardName;
+ RewardAmount = builder.rewardAmount;
+ Query = builder.query;
+ ExpressViewWidth = builder.expressViewWidth;
+ ExpressViewHeight = builder.expressViewHeight;
+ ExpressImageWidth = builder.expressImageWidth;
+ ExpressImageHeight = builder.expressImageHeight;
+ MinaId = builder.minaId;
+ SlideInterval = builder.slideInterval;
+ }
+
+ public Builder ToBuilder()
+ {
+ var builder = new Builder()
+ .WithSpaceId(SpaceId)
+ .WithExtra1(Extra1)
+ .WithUserId(UserId)
+ .WithRewardName(RewardName)
+ .WithQuery(Query);
+
+ if (RewardAmount.HasValue)
+ {
+ builder.WithRewardAmount(RewardAmount.Value);
+ }
+
+ if (ExpressViewWidth.HasValue || ExpressViewHeight.HasValue)
+ {
+ builder.WithExpressViewSize(ExpressViewWidth ?? -1, ExpressViewHeight ?? -1);
+ }
+
+ if (ExpressImageWidth.HasValue || ExpressImageHeight.HasValue)
+ {
+ builder.WithExpressImageSize(ExpressImageWidth ?? -1, ExpressImageHeight ?? -1);
+ }
+
+ if (MinaId.HasValue)
+ {
+ builder.WithMinaId(MinaId.Value);
+ }
+
+ if (SlideInterval.HasValue)
+ {
+ builder.WithSlideInterval(SlideInterval.Value);
+ }
+
+ return builder;
+ }
+
+ internal Dictionary ToBridgePayload()
+ {
+ var payload = new Dictionary(StringComparer.Ordinal)
+ {
+ ["space_id"] = SpaceId
+ };
+
+ if (!string.IsNullOrEmpty(Extra1))
+ {
+ payload["extra1"] = Extra1;
+ }
+
+ if (!string.IsNullOrEmpty(UserId))
+ {
+ payload["user_id"] = UserId;
+ }
+
+ if (!string.IsNullOrEmpty(RewardName))
+ {
+ payload["reward_name"] = RewardName;
+ }
+
+ if (RewardAmount.HasValue)
+ {
+ payload["reward_amount"] = RewardAmount.Value;
+ }
+
+ if (!string.IsNullOrEmpty(Query))
+ {
+ payload["query"] = Query;
+ }
+
+ if (ExpressViewWidth.HasValue)
+ {
+ payload["express_width"] = ExpressViewWidth.Value;
+ }
+
+ if (ExpressViewHeight.HasValue)
+ {
+ payload["express_height"] = ExpressViewHeight.Value;
+ }
+
+ if (ExpressImageWidth.HasValue)
+ {
+ payload["express_image_width"] = ExpressImageWidth.Value;
+ }
+
+ if (ExpressImageHeight.HasValue)
+ {
+ payload["express_image_height"] = ExpressImageHeight.Value;
+ }
+
+ if (MinaId.HasValue)
+ {
+ payload["mina_id"] = MinaId.Value;
+ }
+
+ if (SlideInterval.HasValue)
+ {
+ // Native Java API is `withSlideInternal` (spelled "Internal" by upstream);
+ // keep the wire key matching the native method name.
+ payload["slide_internal"] = SlideInterval.Value;
+ }
+
+ return payload;
+ }
+
+ public sealed class Builder
+ {
+ internal long spaceId;
+ internal string extra1;
+ internal string userId;
+ internal string rewardName;
+ internal int? rewardAmount;
+ internal string query;
+ internal int? expressViewWidth;
+ internal int? expressViewHeight;
+ internal int? expressImageWidth;
+ internal int? expressImageHeight;
+ internal long? minaId;
+ internal int? slideInterval;
+
+ public Builder WithSpaceId(long value)
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), "SpaceId must be greater than zero.");
+ }
+
+ spaceId = value;
+ return this;
+ }
+
+ public Builder WithExtra1(string value)
+ {
+ extra1 = value;
+ return this;
+ }
+
+ public Builder WithUserId(string value)
+ {
+ userId = value;
+ return this;
+ }
+
+ public Builder WithRewardName(string value)
+ {
+ rewardName = value;
+ return this;
+ }
+
+ public Builder WithRewardAmount(int value)
+ {
+ rewardAmount = value;
+ return this;
+ }
+
+ public Builder WithQuery(string value)
+ {
+ query = value;
+ return this;
+ }
+
+ public Builder WithExpressViewSize(int width, int height)
+ {
+ expressViewWidth = width;
+ expressViewHeight = height;
+ return this;
+ }
+
+ public Builder WithExpressImageSize(int width, int height)
+ {
+ expressImageWidth = width;
+ expressImageHeight = height;
+ return this;
+ }
+
+ public Builder WithMinaId(long value)
+ {
+ minaId = value;
+ return this;
+ }
+
+ ///
+ /// Sets banner auto-ad slide rotation interval, in seconds (recommended 30–120, native default 30).
+ ///
+ public Builder WithSlideInterval(int seconds)
+ {
+ slideInterval = seconds;
+ return this;
+ }
+
+ public DirichletAdRequest Build()
+ {
+ if (spaceId <= 0)
+ {
+ throw new InvalidOperationException("SpaceId must be set to a value greater than zero before building the request.");
+ }
+
+ return new DirichletAdRequest(this);
+ }
+ }
+ }
+
+ public enum DirichletBannerAlignment
+ {
+ Top = 0,
+ Bottom = 1
+ }
+
+ public sealed class DirichletAdShowOptions
+ {
+ public DirichletBannerAlignment BannerAlignment { get; set; } = DirichletBannerAlignment.Bottom;
+ public int BannerOffset { get; set; }
+
+ internal Dictionary ToBridgePayload()
+ {
+ var payload = new Dictionary(StringComparer.Ordinal);
+
+ // Banner options are always included if set, Bridge will handle based on ad type
+ payload["banner_baseline"] = (int)BannerAlignment;
+ payload["banner_offset"] = BannerOffset;
+
+ return payload;
+ }
+ }
+
+ public sealed class DirichletAdNative
+ {
+ private readonly IDirichletPlatformBridge bridge;
+ private static readonly object RewardAutoSessionLock = new object();
+ private static readonly Dictionary RewardAutoSessions = new Dictionary(StringComparer.Ordinal);
+
+ public static DirichletAdNative Create() => DirichletAdManager.CreateAdNative();
+
+ internal DirichletAdNative(IDirichletPlatformBridge bridge)
+ {
+ this.bridge = bridge ?? throw new ArgumentNullException(nameof(bridge));
+ }
+
+ public void LoadRewardVideoAd(DirichletAdRequest request, Action onLoaded, Action onFailure)
+ {
+ if (!ValidateRequest(request, onFailure))
+ {
+ return;
+ }
+
+ bridge.LoadRewardVideoAd(request,
+ handle =>
+ {
+ var ad = DirichletRewardVideoAd.Create(bridge, handle, request.SpaceId);
+ ad.MarkLoaded();
+ onLoaded?.Invoke(ad);
+ },
+ onFailure);
+ }
+
+ public void LoadInterstitialAd(DirichletAdRequest request, Action onLoaded, Action onFailure)
+ {
+ if (!ValidateRequest(request, onFailure))
+ {
+ return;
+ }
+
+ bridge.LoadInterstitialAd(request,
+ handle =>
+ {
+ var ad = DirichletInterstitialAd.Create(bridge, handle, request.SpaceId);
+ ad.MarkLoaded();
+ onLoaded?.Invoke(ad);
+ },
+ onFailure);
+ }
+
+ public void LoadBannerAd(DirichletAdRequest request, Action onLoaded, Action onFailure)
+ {
+ if (!ValidateRequest(request, onFailure))
+ {
+ return;
+ }
+
+ bridge.LoadBannerAd(request,
+ handle =>
+ {
+ var ad = DirichletBannerAd.Create(bridge, handle, request.SpaceId);
+ ad.MarkLoaded();
+ onLoaded?.Invoke(ad);
+ },
+ onFailure);
+ }
+
+ public void LoadSplashAd(DirichletAdRequest request, Action onLoaded, Action onFailure)
+ {
+ if (!ValidateRequest(request, onFailure))
+ {
+ return;
+ }
+
+ bridge.LoadSplashAd(request,
+ handle =>
+ {
+ var ad = DirichletSplashAd.Create(bridge, handle, request.SpaceId);
+ ad.MarkLoaded();
+ onLoaded?.Invoke(ad);
+ },
+ onFailure);
+ }
+
+ public void LoadExpressFeedAd(DirichletAdRequest request, Action> onLoaded, Action onFailure)
+ {
+ NotifyNotSupported("express_feed", onFailure);
+ }
+
+ public void LoadNativeFeedAd(DirichletAdRequest request, Action> onLoaded, Action onFailure)
+ {
+ NotifyNotSupported("native_feed", onFailure);
+ }
+
+ ///
+ /// Shows a reward video ad with automatic load-and-show logic.
+ /// This method combines loading and showing into a single operation.
+ ///
+ /// - Android: 使用 native 侧的 AutoAd(可能包含缓存策略,保持原有行为)
+ /// - iOS: 使用 Unity C# 侧的“load 成功后立刻 show”的简化方案(不做缓存)
+ ///
+ /// Ad request parameters
+ /// Listener for all ad events (show/close/reward/click/error)
+ public void ShowRewardVideoAutoAd(DirichletAdRequest request, IDirichletRewardVideoAutoAdListener listener)
+ {
+ if (!ValidateRequest(request, error => listener?.OnError(error)))
+ {
+ return;
+ }
+
+#if UNITY_ANDROID && !UNITY_EDITOR
+ bridge.ShowRewardVideoAutoAd(request, listener);
+#else
+ // iOS/Editor/other platforms: use simplified load+show implementation (no cache).
+ ShowRewardVideoLoadAndShowInternal(request, listener);
+#endif
+ }
+
+ ///
+ /// Shows an interstitial ad with automatic load-and-show logic.
+ /// Android only - iOS receives OnError with not_supported.
+ ///
+ public void ShowInterstitialAutoAd(DirichletAdRequest request, IDirichletInterstitialAutoAdListener listener)
+ {
+ if (!ValidateRequest(request, error => listener?.OnError(error)))
+ {
+ return;
+ }
+
+ bridge.ShowInterstitialAutoAd(request, listener);
+ }
+
+ ///
+ /// Shows a banner ad with automatic load + rotation logic. Container is created internally and
+ /// anchored at the bottom of the screen by default. Use
+ /// to control rotation interval. Android only - iOS receives OnError with not_supported.
+ ///
+ public void ShowBannerAutoAd(DirichletAdRequest request, IDirichletBannerAutoAdListener listener)
+ {
+ ShowBannerAutoAd(request, null, listener);
+ }
+
+ ///
+ /// Banner auto-ad with explicit alignment/offset overrides. Advanced overload —
+ /// the standard form is .
+ ///
+ public void ShowBannerAutoAd(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletBannerAutoAdListener listener)
+ {
+ if (!ValidateRequest(request, error => listener?.OnError(error)))
+ {
+ return;
+ }
+
+ bridge.ShowBannerAutoAd(request, options ?? new DirichletAdShowOptions(), listener);
+ }
+
+ ///
+ /// Shows a splash ad with automatic load logic. Container is a fullscreen overlay created internally.
+ /// Android only - iOS receives OnError with not_supported.
+ ///
+ public void ShowSplashAutoAd(DirichletAdRequest request, IDirichletSplashAutoAdListener listener)
+ {
+ ShowSplashAutoAd(request, null, listener);
+ }
+
+ ///
+ /// Splash auto-ad advanced overload (currently no effective options for splash).
+ ///
+ public void ShowSplashAutoAd(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletSplashAutoAdListener listener)
+ {
+ if (!ValidateRequest(request, error => listener?.OnError(error)))
+ {
+ return;
+ }
+
+ bridge.ShowSplashAutoAd(request, options ?? new DirichletAdShowOptions(), listener);
+ }
+
+ ///
+ /// Preloads an ad for later auto-show. Currently only type=3 (reward video) is handled by the native SDK.
+ /// Android only - iOS is a no-op.
+ ///
+ public void PreLoad(DirichletAdRequest request, int type)
+ {
+ if (!ValidateRequest(request, null))
+ {
+ return;
+ }
+
+ bridge.PreLoad(request, type);
+ }
+
+ private void ShowRewardVideoLoadAndShowInternal(DirichletAdRequest request, IDirichletRewardVideoAutoAdListener listener)
+ {
+ LoadRewardVideoAd(
+ request,
+ ad =>
+ {
+ if (ad == null)
+ {
+ listener?.OnError(new DirichletError("invalid_ad", "Load callback returned null ad"));
+ return;
+ }
+
+ var sessionId = Guid.NewGuid().ToString("N");
+ var session = new AutoRewardVideoSession(sessionId, ad, listener);
+ RegisterRewardAutoSession(session);
+ ad.SetInteractionListener(session);
+
+ // Load succeeded; show immediately.
+ var shown = ad.Show();
+ if (!shown)
+ {
+ session.FailAndDispose(new DirichletError("show_failed", "ShowRewardVideoAd returned false"));
+ }
+ },
+ error =>
+ {
+ listener?.OnError(error ?? new DirichletError("load_failed", "LoadRewardVideoAd failed"));
+ });
+ }
+
+ private static bool ValidateRequest(DirichletAdRequest request, Action onFailure)
+ {
+ if (request == null)
+ {
+ onFailure?.Invoke(new DirichletError("invalid_request", "DirichletAdRequest cannot be null"));
+ return false;
+ }
+
+ if (request.SpaceId <= 0)
+ {
+ onFailure?.Invoke(new DirichletError("invalid_space_id", "DirichletAdRequest.SpaceId must be greater than zero"));
+ return false;
+ }
+
+ return true;
+ }
+
+ private void NotifyNotSupported(string feature, Action onFailure)
+ {
+ onFailure?.Invoke(new DirichletError("not_supported", $"Dirichlet Unity bridge does not yet support {feature} ads."));
+ }
+
+ private static void RegisterRewardAutoSession(AutoRewardVideoSession session)
+ {
+ if (session == null)
+ {
+ return;
+ }
+
+ lock (RewardAutoSessionLock)
+ {
+ RewardAutoSessions[session.SessionId] = session;
+ }
+ }
+
+ private static void UnregisterRewardAutoSession(string sessionId)
+ {
+ if (string.IsNullOrEmpty(sessionId))
+ {
+ return;
+ }
+
+ lock (RewardAutoSessionLock)
+ {
+ RewardAutoSessions.Remove(sessionId);
+ }
+ }
+
+ private sealed class AutoRewardVideoSession : IDirichletRewardAdInteractionListener
+ {
+ public string SessionId { get; }
+
+ private readonly DirichletRewardVideoAd ad;
+ private readonly IDirichletRewardVideoAutoAdListener listener;
+ private bool disposed;
+
+ public AutoRewardVideoSession(string sessionId, DirichletRewardVideoAd ad, IDirichletRewardVideoAutoAdListener listener)
+ {
+ SessionId = string.IsNullOrEmpty(sessionId) ? Guid.NewGuid().ToString("N") : sessionId;
+ this.ad = ad ?? throw new ArgumentNullException(nameof(ad));
+ this.listener = listener;
+ }
+
+ public void OnAdShow()
+ {
+ if (disposed)
+ {
+ return;
+ }
+
+ listener?.OnAdShow();
+ }
+
+ public void OnAdClick()
+ {
+ if (disposed)
+ {
+ return;
+ }
+
+ listener?.OnAdClick();
+ }
+
+ public void OnAdClose()
+ {
+ if (disposed)
+ {
+ return;
+ }
+
+ listener?.OnAdClose();
+ Dispose();
+ }
+
+ public void OnRewardVerify(DirichletRewardVerificationEventArgs args)
+ {
+ if (disposed)
+ {
+ return;
+ }
+
+ listener?.OnRewardVerify(args);
+ }
+
+ public void FailAndDispose(DirichletError error)
+ {
+ if (disposed)
+ {
+ return;
+ }
+
+ listener?.OnError(error ?? new DirichletError("unknown_error", "Unknown error"));
+ Dispose();
+ }
+
+ private void Dispose()
+ {
+ if (disposed)
+ {
+ return;
+ }
+
+ disposed = true;
+ UnregisterRewardAutoSession(SessionId);
+
+ try
+ {
+ ad?.Destroy();
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[Dirichlet] Auto reward ad Destroy failed: {ex.Message}");
+ }
+ }
+ }
+ }
+
+ ///
+ /// Base Unity-side ad handle. Responsible for keeping track of lifecycle state.
+ /// Matches the native SDK pattern where ad objects are returned from loadXXXAd() methods.
+ ///
+ public abstract class DirichletAd
+ {
+ protected readonly DirichletPlatformAdHandle PlatformHandle;
+ private readonly IDirichletPlatformBridge bridge;
+ private readonly long spaceId;
+
+ public string SlotId => spaceId > 0 ? spaceId.ToString(CultureInfo.InvariantCulture) : string.Empty;
+ public bool IsLoaded { get; protected set; }
+
+ ///
+ /// Returns whether the ad is still valid and can be shown.
+ /// This checks the native ad object's validity, which may expire after some time.
+ /// Always call this before Show() to ensure the ad hasn't expired.
+ ///
+ public bool IsValid => bridge?.IsAdValid(PlatformHandle) ?? false;
+
+ ///
+ /// Raised when the native layer confirms the ad was shown to the user.
+ ///
+ public event Action Shown;
+
+ ///
+ /// Raised when the user clicks the ad.
+ ///
+ public event Action Clicked;
+
+ ///
+ /// Raised when the ad is closed (either by user action or channel logic).
+ ///
+ public event Action Closed;
+
+ internal DirichletAd(IDirichletPlatformBridge bridge, DirichletPlatformAdHandle platformHandle, long spaceId)
+ {
+ this.bridge = bridge ?? throw new ArgumentNullException(nameof(bridge));
+ PlatformHandle = platformHandle ?? throw new ArgumentNullException(nameof(platformHandle));
+ this.spaceId = spaceId;
+ if (!string.IsNullOrEmpty(PlatformHandle.DebugId))
+ {
+ DirichletAdEventRouter.Register(PlatformHandle.DebugId, this);
+ }
+ }
+
+ internal IDirichletPlatformBridge Bridge => bridge;
+
+ public virtual bool Show()
+ {
+ if (!IsLoaded)
+ {
+ Debug.LogWarning($"[Dirichlet] Attempted to show ad before load success. Slot={SlotId}");
+ }
+
+ var shown = ShowInternal(null);
+ if (shown)
+ {
+ IsLoaded = false;
+ }
+ return shown;
+ }
+
+ public void Destroy()
+ {
+ if (!string.IsNullOrEmpty(PlatformHandle?.DebugId))
+ {
+ DirichletAdEventRouter.Unregister(PlatformHandle.DebugId);
+ }
+ DestroyInternal();
+ IsLoaded = false;
+ }
+
+ internal void MarkLoaded()
+ {
+ IsLoaded = true;
+ }
+
+ protected abstract bool ShowInternal(DirichletAdShowOptions options);
+
+ protected abstract void DestroyInternal();
+
+ internal virtual void HandleNativeEvent(DirichletNativeEvent nativeEvent)
+ {
+ switch (nativeEvent.EventName)
+ {
+ case DirichletNativeEventNames.Show:
+ OnShown();
+ break;
+ case DirichletNativeEventNames.Click:
+ OnClicked();
+ break;
+ case DirichletNativeEventNames.Close:
+ OnClosed();
+ break;
+ }
+ }
+
+ protected virtual void OnShown()
+ {
+ Shown?.Invoke();
+ }
+
+ protected virtual void OnClicked()
+ {
+ Clicked?.Invoke();
+ }
+
+ protected virtual void OnClosed()
+ {
+ Closed?.Invoke();
+ }
+ }
+
+ public abstract class DirichletRewardAdBase : DirichletAd
+ {
+ public event Action RewardVerified;
+ private IDirichletRewardAdInteractionListener interactionListener;
+
+ internal DirichletRewardAdBase(IDirichletPlatformBridge bridge, DirichletPlatformAdHandle handle, long spaceId)
+ : base(bridge, handle, spaceId)
+ {
+ }
+
+ public void SetInteractionListener(IDirichletRewardAdInteractionListener listener)
+ {
+ interactionListener = listener;
+ }
+
+ public void SetRewardAdInteractionListener(IDirichletRewardAdInteractionListener listener)
+ {
+ SetInteractionListener(listener);
+ }
+
+ protected override void OnShown()
+ {
+ base.OnShown();
+ interactionListener?.OnAdShow();
+ }
+
+ protected override void OnClicked()
+ {
+ base.OnClicked();
+ interactionListener?.OnAdClick();
+ }
+
+ protected override void OnClosed()
+ {
+ base.OnClosed();
+ interactionListener?.OnAdClose();
+ }
+
+ internal override void HandleNativeEvent(DirichletNativeEvent nativeEvent)
+ {
+ base.HandleNativeEvent(nativeEvent);
+
+ if (nativeEvent.EventName == DirichletNativeEventNames.Reward && nativeEvent.Data != null)
+ {
+ var data = nativeEvent.Data;
+ var args = new DirichletRewardVerificationEventArgs(
+ data.RewardVerify,
+ data.RewardAmount,
+ data.RewardName,
+ data.Code,
+ data.Message);
+ RewardVerified?.Invoke(args);
+ interactionListener?.OnRewardVerify(args);
+ }
+ }
+
+ protected override bool ShowInternal(DirichletAdShowOptions options)
+ {
+ return Bridge.ShowRewardVideoAd(PlatformHandle);
+ }
+
+ protected override void DestroyInternal()
+ {
+ Bridge.DestroyAd(PlatformHandle);
+ }
+ }
+
+ public sealed class DirichletRewardVideoAd : DirichletRewardAdBase
+ {
+ private DirichletRewardVideoAd(IDirichletPlatformBridge bridge, DirichletPlatformAdHandle handle, long spaceId)
+ : base(bridge, handle, spaceId)
+ {
+ }
+
+ internal static DirichletRewardVideoAd Create(IDirichletPlatformBridge bridge, DirichletPlatformAdHandle handle, long spaceId)
+ {
+ return new DirichletRewardVideoAd(bridge, handle, spaceId);
+ }
+ }
+
+ [Obsolete("Use DirichletRewardVideoAd instead.")]
+ public sealed class DirichletRewardAd : DirichletRewardAdBase
+ {
+ internal DirichletRewardAd(IDirichletPlatformBridge bridge, DirichletPlatformAdHandle handle, long spaceId) : base(bridge, handle, spaceId)
+ {
+ }
+ }
+
+ public sealed class DirichletInterstitialAd : DirichletAd
+ {
+ private IDirichletInterstitialAdInteractionListener interactionListener;
+
+ private DirichletInterstitialAd(IDirichletPlatformBridge bridge, DirichletPlatformAdHandle handle, long spaceId)
+ : base(bridge, handle, spaceId)
+ {
+ }
+
+ public void SetInteractionListener(IDirichletInterstitialAdInteractionListener listener)
+ {
+ interactionListener = listener;
+ }
+
+ protected override void OnShown()
+ {
+ base.OnShown();
+ interactionListener?.OnAdShow();
+ }
+
+ protected override void OnClicked()
+ {
+ base.OnClicked();
+ interactionListener?.OnAdClick();
+ }
+
+ protected override void OnClosed()
+ {
+ base.OnClosed();
+ interactionListener?.OnAdClose();
+ }
+
+ protected override bool ShowInternal(DirichletAdShowOptions options)
+ {
+ return Bridge.ShowInterstitialAd(PlatformHandle);
+ }
+
+ protected override void DestroyInternal()
+ {
+ Bridge.DestroyAd(PlatformHandle);
+ }
+
+ internal static DirichletInterstitialAd Create(IDirichletPlatformBridge bridge, DirichletPlatformAdHandle handle, long spaceId)
+ {
+ return new DirichletInterstitialAd(bridge, handle, spaceId);
+ }
+ }
+
+ public sealed class DirichletBannerAd : DirichletAd
+ {
+ private IDirichletBannerAdInteractionListener interactionListener;
+
+ private DirichletBannerAd(IDirichletPlatformBridge bridge, DirichletPlatformAdHandle handle, long spaceId)
+ : base(bridge, handle, spaceId)
+ {
+ }
+
+ public void SetInteractionListener(IDirichletBannerAdInteractionListener listener)
+ {
+ interactionListener = listener;
+ }
+
+ public bool Show(DirichletAdShowOptions options)
+ {
+ var showOptions = options ?? new DirichletAdShowOptions();
+ var shown = ShowInternal(showOptions);
+ if (shown)
+ {
+ IsLoaded = false;
+ }
+ return shown;
+ }
+
+ public override bool Show()
+ {
+ return Show(null);
+ }
+
+ protected override void OnShown()
+ {
+ base.OnShown();
+ interactionListener?.OnAdShow();
+ }
+
+ protected override void OnClicked()
+ {
+ base.OnClicked();
+ interactionListener?.OnAdClick();
+ }
+
+ protected override void OnClosed()
+ {
+ base.OnClosed();
+ interactionListener?.OnAdClose();
+ }
+
+ protected override bool ShowInternal(DirichletAdShowOptions options)
+ {
+ return Bridge.ShowBannerAd(PlatformHandle, options ?? new DirichletAdShowOptions());
+ }
+
+ protected override void DestroyInternal()
+ {
+ Bridge.DestroyAd(PlatformHandle);
+ }
+
+ internal static DirichletBannerAd Create(IDirichletPlatformBridge bridge, DirichletPlatformAdHandle handle, long spaceId)
+ {
+ return new DirichletBannerAd(bridge, handle, spaceId);
+ }
+ }
+
+ public sealed class DirichletSplashAd : DirichletAd
+ {
+ private IDirichletSplashAdInteractionListener interactionListener;
+
+ private DirichletSplashAd(IDirichletPlatformBridge bridge, DirichletPlatformAdHandle handle, long spaceId)
+ : base(bridge, handle, spaceId)
+ {
+ }
+
+ public void SetInteractionListener(IDirichletSplashAdInteractionListener listener)
+ {
+ interactionListener = listener;
+ }
+
+ protected override void OnShown()
+ {
+ base.OnShown();
+ interactionListener?.OnAdShow();
+ }
+
+ protected override void OnClicked()
+ {
+ base.OnClicked();
+ interactionListener?.OnAdClick();
+ }
+
+ protected override void OnClosed()
+ {
+ base.OnClosed();
+ interactionListener?.OnAdClose();
+ }
+
+ protected override bool ShowInternal(DirichletAdShowOptions options)
+ {
+ return Bridge.ShowSplashAd(PlatformHandle, options ?? new DirichletAdShowOptions());
+ }
+
+ protected override void DestroyInternal()
+ {
+ Bridge.DestroyAd(PlatformHandle);
+ }
+
+ internal static DirichletSplashAd Create(IDirichletPlatformBridge bridge, DirichletPlatformAdHandle handle, long spaceId)
+ {
+ return new DirichletSplashAd(bridge, handle, spaceId);
+ }
+ }
+
+ internal static class DirichletNativeEventNames
+ {
+ internal const string Show = "show";
+ internal const string Click = "click";
+ internal const string Close = "close";
+ internal const string Reward = "reward";
+ }
+
+ internal readonly struct DirichletNativeEvent
+ {
+ public string EventName { get; }
+ public string AdType { get; }
+ public DirichletNativeEventData Data { get; }
+
+ public DirichletNativeEvent(string eventName, string adType, DirichletNativeEventData data)
+ {
+ EventName = string.IsNullOrEmpty(eventName) ? string.Empty : eventName;
+ AdType = string.IsNullOrEmpty(adType) ? string.Empty : adType;
+ Data = data;
+ }
+ }
+
+ internal sealed class DirichletNativeEventData
+ {
+ public bool RewardVerify { get; }
+ public int RewardAmount { get; }
+ public string RewardName { get; }
+ public int Code { get; }
+ public string Message { get; }
+
+ public DirichletNativeEventData(bool rewardVerify, int rewardAmount, string rewardName, int code, string message)
+ {
+ RewardVerify = rewardVerify;
+ RewardAmount = rewardAmount;
+ RewardName = string.IsNullOrEmpty(rewardName) ? string.Empty : rewardName;
+ Code = code;
+ Message = message ?? string.Empty;
+ }
+ }
+
+ public sealed class DirichletRewardVerificationEventArgs : EventArgs
+ {
+ public bool IsVerified { get; }
+ public int RewardAmount { get; }
+ public string RewardName { get; }
+ public int Code { get; }
+ public string Message { get; }
+
+ internal DirichletRewardVerificationEventArgs(bool rewardVerified, int rewardAmount, string rewardName, int code, string message)
+ {
+ IsVerified = rewardVerified;
+ RewardAmount = rewardAmount;
+ RewardName = string.IsNullOrEmpty(rewardName) ? string.Empty : rewardName;
+ Code = code;
+ Message = message ?? string.Empty;
+ }
+ }
+
+ public interface IDirichletRewardAdInteractionListener
+ {
+ void OnAdShow();
+ void OnAdClick();
+ void OnAdClose();
+ void OnRewardVerify(DirichletRewardVerificationEventArgs args);
+ }
+
+ public interface IDirichletInterstitialAdInteractionListener
+ {
+ void OnAdShow();
+ void OnAdClick();
+ void OnAdClose();
+ }
+
+ public interface IDirichletBannerAdInteractionListener
+ {
+ void OnAdShow();
+ void OnAdClick();
+ void OnAdClose();
+ }
+
+ public interface IDirichletSplashAdInteractionListener
+ {
+ void OnAdShow();
+ void OnAdClick();
+ void OnAdClose();
+ }
+
+ ///
+ /// Listener interface for auto interstitial ad callbacks.
+ /// Android only - iOS will receive OnError with not_supported error.
+ ///
+ public interface IDirichletInterstitialAutoAdListener
+ {
+ void OnError(DirichletError error);
+ void OnAdShow();
+ void OnAdClose();
+ void OnAdClick();
+ }
+
+ ///
+ /// Listener interface for auto banner ad callbacks.
+ /// Android only - iOS will receive OnError with not_supported error.
+ ///
+ public interface IDirichletBannerAutoAdListener
+ {
+ void OnError(DirichletError error);
+ void OnAdShow();
+ void OnAdClose();
+ void OnAdClick();
+ }
+
+ ///
+ /// Listener interface for auto splash ad callbacks.
+ /// Android only - iOS will receive OnError with not_supported error.
+ ///
+ public interface IDirichletSplashAutoAdListener
+ {
+ void OnError(DirichletError error);
+ void OnAdShow();
+ void OnAdClose();
+ void OnAdClick();
+ }
+
+ ///
+ /// Listener interface for auto reward video ad callbacks.
+ /// Used with ShowRewardVideoAutoAd which combines load and show into one operation.
+ /// Android only - iOS will receive OnError with not_supported error.
+ ///
+ public interface IDirichletRewardVideoAutoAdListener
+ {
+ ///
+ /// Called when ad fails to load or show.
+ ///
+ void OnError(DirichletError error);
+
+ ///
+ /// Called when ad is shown to the user.
+ ///
+ void OnAdShow();
+
+ ///
+ /// Called when ad is closed.
+ ///
+ void OnAdClose();
+
+ ///
+ /// Called when reward verification is completed.
+ ///
+ void OnRewardVerify(DirichletRewardVerificationEventArgs args);
+
+ ///
+ /// Called when ad is clicked.
+ ///
+ void OnAdClick();
+ }
+
+ internal static class DirichletAdEventRouter
+ {
+ private const string CallbackObjectName = "DirichletMediationEventReceiver";
+ private static readonly Dictionary Ads = new Dictionary(StringComparer.Ordinal);
+ private static bool receiverInitialized;
+
+ internal static void Register(string handleId, DirichletAd ad)
+ {
+ if (string.IsNullOrEmpty(handleId) || ad == null)
+ {
+ return;
+ }
+
+ EnsureReceiver();
+ Ads[handleId] = new WeakReference(ad);
+ }
+
+ internal static void Unregister(string handleId)
+ {
+ if (string.IsNullOrEmpty(handleId))
+ {
+ return;
+ }
+
+ Ads.Remove(handleId);
+ }
+
+ private static void EnsureReceiver()
+ {
+ if (receiverInitialized)
+ {
+ return;
+ }
+
+ if (!DirichletSdk.IsUnityThread)
+ {
+ DirichletSdk.DispatchToUnityThread(EnsureReceiver);
+ return;
+ }
+
+ var existing = GameObject.Find(CallbackObjectName);
+ if (existing == null)
+ {
+ var host = new GameObject(CallbackObjectName)
+ {
+ hideFlags = HideFlags.HideAndDontSave
+ };
+ UnityEngine.Object.DontDestroyOnLoad(host);
+ host.AddComponent();
+ }
+
+ receiverInitialized = true;
+ }
+
+ internal static void HandleNativeEvent(string payload)
+ {
+ if (string.IsNullOrEmpty(payload))
+ {
+ return;
+ }
+
+ NativeEventPayload message;
+ try
+ {
+ message = JsonUtility.FromJson(payload);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[Dirichlet] Failed to parse native ad event: {ex.Message}\n{payload}");
+ return;
+ }
+
+ if (message == null || string.IsNullOrEmpty(message.handle) || string.IsNullOrEmpty(message.eventName))
+ {
+ return;
+ }
+
+ if (!Ads.TryGetValue(message.handle, out var weakReference))
+ {
+ return;
+ }
+
+ if (!(weakReference.Target is DirichletAd ad) || ad == null)
+ {
+ Ads.Remove(message.handle);
+ return;
+ }
+
+ var data = message.data != null
+ ? new DirichletNativeEventData(
+ message.data.rewardVerify,
+ message.data.rewardAmount,
+ message.data.rewardName,
+ message.data.code,
+ message.data.message)
+ : null;
+
+ var nativeEvent = new DirichletNativeEvent(message.eventName, message.adType, data);
+
+ if (DirichletSdk.IsUnityThread)
+ {
+ ad.HandleNativeEvent(nativeEvent);
+ }
+ else
+ {
+ DirichletSdk.DispatchToUnityThread(() => ad.HandleNativeEvent(nativeEvent));
+ }
+ }
+
+ [Serializable]
+ private class NativeEventPayload
+ {
+ public string handle;
+ public string eventName;
+ public string adType;
+ public NativeEventPayloadData data;
+ }
+
+ [Serializable]
+ private class NativeEventPayloadData
+ {
+ public bool rewardVerify;
+ public int rewardAmount;
+ public string rewardName;
+ public int code;
+ public string message;
+ }
+
+ private sealed class DirichletAdEventReceiver : MonoBehaviour
+ {
+ public void OnNativeEvent(string payload)
+ {
+ HandleNativeEvent(payload);
+ }
+ }
+ }
+}
+
+
diff --git a/DirichletMediation/Runtime/DirichletAdTypes.cs.meta b/DirichletMediation/Runtime/DirichletAdTypes.cs.meta
new file mode 100644
index 0000000..dc03408
--- /dev/null
+++ b/DirichletMediation/Runtime/DirichletAdTypes.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: eec9af0b7d54e4522b6d9c84d29e65de
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/DirichletMediation/Runtime/DirichletMediationSdk.cs b/DirichletMediation/Runtime/DirichletMediationSdk.cs
new file mode 100644
index 0000000..cf49d50
--- /dev/null
+++ b/DirichletMediation/Runtime/DirichletMediationSdk.cs
@@ -0,0 +1,2036 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Threading;
+using UnityEngine;
+
+namespace Dirichlet.Mediation
+{
+ ///
+ /// Entry point of the Dirichlet Mediation Unity wrapper.
+ /// Provides initialization, configuration, and shared helpers that are platform agnostic.
+ ///
+ public static class DirichletSdk
+ {
+ private static readonly IDirichletPlatformBridge Bridge = DirichletPlatformBridgeFactory.Create();
+
+ private static SynchronizationContext unityContext;
+ private static int unityThreadId;
+ private static readonly Queue pendingActions = new Queue();
+ private static readonly object pendingActionsLock = new object();
+ private static UnityThreadPump pump;
+
+ ///
+ /// Indicates whether the mediation SDK was initialized successfully.
+ ///
+ public static bool IsInitialized { get; private set; }
+
+ public static bool IsUnityThread => unityThreadId != 0 && Thread.CurrentThread.ManagedThreadId == unityThreadId;
+
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
+ private static void CaptureUnitySynchronizationContext()
+ {
+ unityThreadId = Thread.CurrentThread.ManagedThreadId;
+ unityContext = SynchronizationContext.Current;
+ EnsureUnityThreadPump();
+ }
+
+ // Internal thread dispatcher. Public SDK callbacks are already marshalled to Unity thread when needed.
+ internal static void DispatchToUnityThread(Action action)
+ {
+ if (action == null)
+ {
+ return;
+ }
+
+ if (IsUnityThread)
+ {
+ try
+ {
+ action();
+ }
+ catch (Exception ex)
+ {
+ Debug.LogException(ex);
+ }
+ return;
+ }
+
+ if (unityContext != null)
+ {
+ unityContext.Post(_ =>
+ {
+ try
+ {
+ action();
+ }
+ catch (Exception ex)
+ {
+ Debug.LogException(ex);
+ }
+ }, null);
+ return;
+ }
+
+ lock (pendingActionsLock)
+ {
+ pendingActions.Enqueue(action);
+ }
+
+ EnsureUnityThreadPump();
+ }
+
+ ///
+ /// Initializes the mediation SDK using the aggregator-style configuration.
+ ///
+ public static void Init(
+ DirichletAdConfig config,
+ Action onSuccess = null,
+ Action onFailure = null)
+ {
+ InitializeInternal(config, onSuccess, onFailure);
+ }
+
+ private static void InitializeInternal(
+ DirichletAdConfig config,
+ Action onSuccess,
+ Action onFailure)
+ {
+ if (IsInitialized)
+ {
+ Debug.Log("[Dirichlet] Initialize called, but SDK is already initialized.");
+ DispatchToUnityThread(() => onSuccess?.Invoke(DirichletInitResult.AlreadyInitialized()));
+ return;
+ }
+
+ if (config == null)
+ {
+ DirichletAdManager.Clear();
+ var error = new DirichletError("invalid_config", "DirichletAdConfig cannot be null");
+ Debug.LogError(error);
+ DispatchToUnityThread(() => onFailure?.Invoke(error));
+ return;
+ }
+
+ var options = config.ToPlatformOptions();
+ if (options == null)
+ {
+ DirichletAdManager.Clear();
+ var error = new DirichletError("invalid_config", "Failed to map DirichletAdConfig to native options");
+ Debug.LogError(error);
+ DispatchToUnityThread(() => onFailure?.Invoke(error));
+ return;
+ }
+
+ DirichletAdManager.ApplyConfig(config);
+
+ void SuccessHandler(DirichletInitResult result)
+ {
+ DispatchToUnityThread(() =>
+ {
+ IsInitialized = result?.Success ?? false;
+ onSuccess?.Invoke(result ?? DirichletInitResult.Ok("bridge_returned_null"));
+ });
+ }
+
+ void FailureHandler(DirichletError error)
+ {
+ DispatchToUnityThread(() =>
+ {
+ IsInitialized = false;
+ onFailure?.Invoke(error ?? new DirichletError("bridge_error", "Initialization failed"));
+ });
+ }
+
+ Bridge.Initialize(options, SuccessHandler, FailureHandler);
+ }
+
+ ///
+ /// Requests runtime permissions if the underlying SDK requires them.
+ ///
+ public static void RequestPermissionIfNecessary()
+ {
+ Bridge?.RequestPermissionIfNeeded();
+ }
+
+ [Obsolete("Use RequestPermissionIfNecessary() instead.")]
+ public static void RequestPermissionIfNeeded()
+ {
+ RequestPermissionIfNecessary();
+ }
+
+ ///
+ /// Returns the native SDK version if available.
+ ///
+ public static string GetVersion() => Bridge?.GetSdkVersion() ?? "unknown";
+
+ [Obsolete("Use GetVersion() instead.")]
+ public static string GetSdkVersion() => GetVersion();
+
+ internal static IDirichletPlatformBridge GetBridge() => Bridge;
+
+ private static void EnsureUnityThreadPump()
+ {
+ if (pump != null || !Application.isPlaying)
+ {
+ return;
+ }
+
+ if (!IsUnityThread)
+ {
+ unityContext?.Post(_ => EnsureUnityThreadPump(), null);
+ return;
+ }
+
+ var host = new GameObject("DirichletUnityThreadPump")
+ {
+ hideFlags = HideFlags.HideAndDontSave
+ };
+ UnityEngine.Object.DontDestroyOnLoad(host);
+ pump = host.AddComponent();
+ }
+
+ private sealed class UnityThreadPump : MonoBehaviour
+ {
+ private readonly List executionBuffer = new List(8);
+
+ private void Awake()
+ {
+ UnityEngine.Object.DontDestroyOnLoad(gameObject);
+ }
+
+ private void Update()
+ {
+ executionBuffer.Clear();
+
+ lock (pendingActionsLock)
+ {
+ while (pendingActions.Count > 0)
+ {
+ executionBuffer.Add(pendingActions.Dequeue());
+ }
+ }
+
+ for (int i = 0; i < executionBuffer.Count; i++)
+ {
+ var action = executionBuffer[i];
+ try
+ {
+ action?.Invoke();
+ }
+ catch (Exception ex)
+ {
+ Debug.LogException(ex);
+ }
+ }
+ }
+
+ private void OnDestroy()
+ {
+ if (ReferenceEquals(pump, this))
+ {
+ pump = null;
+ }
+ }
+ }
+ }
+
+ #region Initialization data
+
+ public enum DirichletAdNetworkType
+ {
+ Unknown = 0,
+ Csj = 1,
+ Gdt = 2,
+ Tap = 3
+ }
+
+ [Serializable]
+ ///
+ /// 配置 Dirichlet Mediation SDK 的初始化参数
+ ///
+ public sealed class DirichletAdConfig
+ {
+ public long MediaId { get; }
+ public string MediaName { get; }
+ public string MediaKey { get; }
+ public string GameChannel { get; }
+
+ ///
+ /// 子渠道标识
+ ///
+ ///
+ /// Android: 用于区分不同的子渠道来源
+ /// iOS: 此属性不使用,传递的值会被忽略
+ ///
+ public string SubChannel { get; }
+
+ public bool DebugEnabled { get; }
+ public string TapClientId { get; }
+ public bool ShakeEnabled { get; }
+ public string CustomConfigJson { get; }
+ public string DataJson { get; }
+
+ ///
+ /// 控制是否允许访问广告标识符
+ ///
+ ///
+ /// iOS: 控制 IDFA 访问。设置为 true 时,iOS 14+ 会检查 ATT 授权状态后读取 IDFA。
+ /// Android: 此属性不使用,Android 使用 OAID/AAID 机制。
+ /// 默认值: true
+ ///
+ public bool AllowIDFAAccess { get; }
+
+ ///
+ /// 外部配置的 aTags(JSON 格式)
+ ///
+ ///
+ /// 可选配置,用于传递额外的标签信息到广告 SDK。
+ /// 格式: JSON 字符串,例如 {"key":"value"}
+ /// 平台支持: iOS/Android 通用
+ ///
+ public string ATags { get; }
+
+ internal string LegacyAppId { get; }
+
+ private DirichletAdConfig(Builder builder)
+ {
+ MediaId = builder.mediaId;
+ LegacyAppId = builder.legacyAppId;
+ MediaName = builder.mediaName;
+ MediaKey = builder.mediaKey;
+ GameChannel = string.IsNullOrEmpty(builder.gameChannel) ? "default" : builder.gameChannel;
+ SubChannel = builder.subChannel;
+ DebugEnabled = builder.enableDebug;
+ TapClientId = builder.tapClientId;
+ ShakeEnabled = builder.shakeEnabled;
+ CustomConfigJson = builder.customConfigJson;
+ DataJson = builder.dataJson;
+ AllowIDFAAccess = builder.allowIDFAAccess;
+ ATags = builder.aTags;
+ }
+
+ public Builder ToBuilder()
+ {
+ return new Builder()
+ .WithMediaId(MediaId)
+ .WithMediaName(MediaName)
+ .WithMediaKey(MediaKey)
+ .WithGameChannel(GameChannel)
+ .WithSubChannel(SubChannel)
+ .EnableDebug(DebugEnabled)
+ .WithTapClientId(TapClientId)
+ .ShakeEnabled(ShakeEnabled)
+ .WithCustomConfigJson(CustomConfigJson)
+ .WithDataJson(DataJson)
+ .WithAppId(LegacyAppId)
+ .AllowIDFAAccess(AllowIDFAAccess)
+ .WithATags(ATags);
+ }
+
+ internal DirichletPlatformInitOptions ToPlatformOptions()
+ {
+ return new DirichletPlatformInitOptions
+ {
+ MediaId = MediaId,
+ AppId = LegacyAppId,
+ Channel = GameChannel,
+ SubChannel = SubChannel,
+ EnableLog = DebugEnabled,
+ MediaName = MediaName,
+ MediaKey = MediaKey,
+ TapClientId = TapClientId,
+ ShakeEnabled = ShakeEnabled,
+ CustomConfigJson = CustomConfigJson,
+ DataJson = DataJson,
+ AllowIDFAAccess = AllowIDFAAccess,
+ ATags = ATags
+ };
+ }
+
+ public override string ToString()
+ {
+ return $"DirichletAdConfig(MediaId={MediaId}, MediaName={MediaName}, GameChannel={GameChannel}, SubChannel={SubChannel}, DebugEnabled={DebugEnabled}, TapClientId={(string.IsNullOrEmpty(TapClientId) ? "" : TapClientId)}, ShakeEnabled={ShakeEnabled})";
+ }
+
+ public sealed class Builder
+ {
+ internal long mediaId;
+ internal string mediaName = "Unity Dirichlet Demo";
+ internal string mediaKey;
+ internal string gameChannel = "default";
+ internal string subChannel;
+ internal bool enableDebug = true;
+ internal string tapClientId;
+ internal bool shakeEnabled = true;
+ internal string customConfigJson;
+ internal string dataJson;
+ internal string legacyAppId;
+ internal bool allowIDFAAccess = true;
+ internal string aTags;
+
+ public Builder WithMediaId(long value)
+ {
+ mediaId = value;
+ return this;
+ }
+
+ public Builder WithAppId(string appId)
+ {
+ legacyAppId = appId;
+ if (!string.IsNullOrEmpty(appId) && long.TryParse(appId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) && parsed > 0)
+ {
+ mediaId = parsed;
+ }
+ return this;
+ }
+
+ public Builder WithMediaName(string value)
+ {
+ mediaName = value;
+ return this;
+ }
+
+ public Builder WithMediaKey(string value)
+ {
+ mediaKey = value;
+ return this;
+ }
+
+ public Builder WithGameChannel(string value)
+ {
+ gameChannel = value;
+ return this;
+ }
+
+ ///
+ /// 设置子渠道标识(Android only,iOS 会忽略此值)
+ ///
+ public Builder WithSubChannel(string value)
+ {
+ subChannel = value;
+ return this;
+ }
+
+ public Builder EnableDebug(bool enabled)
+ {
+ enableDebug = enabled;
+ return this;
+ }
+
+ public Builder WithTapClientId(string value)
+ {
+ tapClientId = value;
+ return this;
+ }
+
+ public Builder ShakeEnabled(bool enabled)
+ {
+ shakeEnabled = enabled;
+ return this;
+ }
+
+ public Builder WithCustomConfigJson(string json)
+ {
+ customConfigJson = json;
+ return this;
+ }
+
+ public Builder WithDataJson(string json)
+ {
+ dataJson = json;
+ return this;
+ }
+
+ ///
+ /// 设置是否允许访问广告标识符(iOS: IDFA,Android: 忽略)
+ ///
+ /// true 表示允许访问(默认),false 表示禁止访问
+ public Builder AllowIDFAAccess(bool enabled)
+ {
+ allowIDFAAccess = enabled;
+ return this;
+ }
+
+ ///
+ /// 设置外部 aTags(JSON 格式,两平台通用)
+ ///
+ /// JSON 字符串,例如 {"key":"value"}
+ public Builder WithATags(string value)
+ {
+ aTags = value;
+ return this;
+ }
+
+ public DirichletAdConfig Build()
+ {
+ return new DirichletAdConfig(this);
+ }
+ }
+ }
+
+ public sealed class DirichletInitResult
+ {
+ public bool Success { get; }
+ public string Message { get; }
+
+ private DirichletInitResult(bool success, string message)
+ {
+ Success = success;
+ Message = message;
+ }
+
+ public static DirichletInitResult Ok(string message = null) => new DirichletInitResult(true, message);
+ public static DirichletInitResult Failed(string message) => new DirichletInitResult(false, message);
+ public static DirichletInitResult AlreadyInitialized() => new DirichletInitResult(true, "already_initialized");
+ }
+
+ ///
+ /// 平台桥接层使用的初始化选项(内部类)
+ ///
+ internal sealed class DirichletPlatformInitOptions
+ {
+ public string AppId { get; set; }
+ public long MediaId { get; set; }
+ public string Channel { get; set; }
+
+ ///
+ /// 子渠道标识(Android only)
+ ///
+ public string SubChannel { get; set; }
+
+ public bool EnableLog { get; set; }
+ public string MediaName { get; set; }
+ public string MediaKey { get; set; }
+ public string TapClientId { get; set; }
+ public bool ShakeEnabled { get; set; }
+ public string CustomConfigJson { get; set; }
+ public string DataJson { get; set; }
+
+ ///
+ /// 控制 IDFA 访问(iOS only)
+ ///
+ public bool AllowIDFAAccess { get; set; }
+
+ ///
+ /// 外部 aTags JSON(通用)
+ ///
+ public string ATags { get; set; }
+
+ internal string GetAppIdString()
+ {
+ if (MediaId > 0)
+ {
+ return MediaId.ToString(CultureInfo.InvariantCulture);
+ }
+
+ return AppId ?? string.Empty;
+ }
+
+ public override string ToString()
+ {
+ return $"DirichletPlatformInitOptions(MediaId={MediaId}, AppId={AppId}, Channel={Channel}, SubChannel={SubChannel}, EnableLog={EnableLog}, MediaName={MediaName}, MediaKey={(string.IsNullOrEmpty(MediaKey) ? "" : "***")}, TapClientId={(string.IsNullOrEmpty(TapClientId) ? "" : TapClientId)}, ShakeEnabled={ShakeEnabled}, AllowIDFAAccess={AllowIDFAAccess}, ATags={(string.IsNullOrEmpty(ATags) ? "" : ATags)})";
+ }
+ }
+
+ public sealed class DirichletError
+ {
+ public string Code { get; }
+ public string Message { get; }
+ public string Adapter { get; }
+ public string Network { get; }
+
+ public DirichletError(string code, string message, string adapter = null, string network = null)
+ {
+ Code = string.IsNullOrEmpty(code) ? "unknown" : code;
+ Message = message ?? string.Empty;
+ Adapter = adapter;
+ Network = network;
+ }
+
+ public override string ToString()
+ {
+ return $"DirichletError(Code={Code}, Message={Message}, Adapter={Adapter}, Network={Network})";
+ }
+ }
+
+ #endregion
+
+ #region Platform bridge plumbing
+
+ internal interface IDirichletPlatformBridge
+ {
+ void Initialize(DirichletPlatformInitOptions options, Action onSuccess, Action onFailure);
+ void LoadRewardVideoAd(DirichletAdRequest request, Action onSuccess, Action onFailure);
+ bool ShowRewardVideoAd(DirichletPlatformAdHandle handle);
+
+ void LoadInterstitialAd(DirichletAdRequest request, Action onSuccess, Action onFailure);
+ bool ShowInterstitialAd(DirichletPlatformAdHandle handle);
+
+ void LoadBannerAd(DirichletAdRequest request, Action onSuccess, Action onFailure);
+ bool ShowBannerAd(DirichletPlatformAdHandle handle, DirichletAdShowOptions options);
+
+ void LoadSplashAd(DirichletAdRequest request, Action onSuccess, Action onFailure);
+ bool ShowSplashAd(DirichletPlatformAdHandle handle, DirichletAdShowOptions options);
+
+ void DestroyAd(DirichletPlatformAdHandle handle);
+ bool IsAdValid(DirichletPlatformAdHandle handle);
+ void RequestPermissionIfNeeded();
+ string GetSdkVersion();
+
+ ///
+ /// Shows a reward video ad with automatic load-and-show logic.
+ /// Android only - iOS will call onFailure with not_supported error.
+ ///
+ void ShowRewardVideoAutoAd(DirichletAdRequest request, IDirichletRewardVideoAutoAdListener listener);
+
+ ///
+ /// Shows an interstitial ad with automatic load-and-show logic. Android only.
+ ///
+ void ShowInterstitialAutoAd(DirichletAdRequest request, IDirichletInterstitialAutoAdListener listener);
+
+ ///
+ /// Shows a banner ad with automatic load + rotation logic. Android only.
+ ///
+ void ShowBannerAutoAd(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletBannerAutoAdListener listener);
+
+ ///
+ /// Shows a splash ad with automatic load logic. Android only.
+ ///
+ void ShowSplashAutoAd(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletSplashAutoAdListener listener);
+
+ ///
+ /// Preloads an ad for later auto-show. Android only.
+ ///
+ /// Ad type code; native SDK currently only handles type=3 (reward video).
+ void PreLoad(DirichletAdRequest request, int type);
+ }
+
+ internal static class DirichletPlatformBridgeFactory
+ {
+ private static IDirichletPlatformBridge instance;
+
+ internal static IDirichletPlatformBridge Create()
+ {
+ if (instance != null)
+ {
+ return instance;
+ }
+
+#if UNITY_ANDROID && !UNITY_EDITOR
+ instance = new AndroidDirichletBridge();
+#elif UNITY_IOS && !UNITY_EDITOR
+ instance = new IOSDirichletBridge();
+#else
+ instance = new NoopDirichletBridge();
+#endif
+ return instance;
+ }
+
+ internal static void OverrideForTesting(IDirichletPlatformBridge customBridge)
+ {
+ instance = customBridge;
+ }
+ }
+
+#if UNITY_ANDROID && !UNITY_EDITOR
+ internal sealed class AndroidDirichletBridge : IDirichletPlatformBridge
+ {
+ private const string BridgeClassName = "com.dirichlet.unity.DirichletUnityBridge";
+ private static AndroidJavaClass cachedBridgeClass;
+
+ private static AndroidJavaClass BridgeClass
+ {
+ get
+ {
+ if (cachedBridgeClass == null)
+ {
+ cachedBridgeClass = new AndroidJavaClass(BridgeClassName);
+ }
+ return cachedBridgeClass;
+ }
+ }
+
+ private readonly Dictionary loadCallbacks = new Dictionary();
+ private readonly object loadCallbacksLock = new object();
+
+ public void Initialize(DirichletPlatformInitOptions options, Action onSuccess, Action onFailure)
+ {
+ if (options == null)
+ {
+ DirichletSdk.DispatchToUnityThread(() => onFailure?.Invoke(new DirichletError("android_invalid_options", "Initialization options cannot be null")));
+ return;
+ }
+
+ try
+ {
+ var dataPayload = !string.IsNullOrEmpty(options.DataJson)
+ ? options.DataJson
+ : options.CustomConfigJson;
+
+ // Do not block Unity thread on Android init. The Java bridge enforces a timeout and reports via callback.
+ var callback = new AndroidInitCallback(
+ () => onSuccess?.Invoke(DirichletInitResult.Ok("android_bridge")),
+ onFailure);
+
+ BridgeClass.CallStatic(
+ "initializeAsync",
+ options.GetAppIdString(),
+ options.Channel ?? string.Empty,
+ options.SubChannel ?? string.Empty,
+ options.EnableLog,
+ options.MediaName ?? string.Empty,
+ options.MediaKey ?? string.Empty,
+ options.TapClientId ?? string.Empty,
+ dataPayload ?? string.Empty,
+ options.ShakeEnabled,
+ callback);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogException(ex);
+ DirichletSdk.DispatchToUnityThread(() => onFailure?.Invoke(new DirichletError("android_exception", ex.Message)));
+ }
+ }
+
+ private sealed class AndroidInitCallback : AndroidJavaProxy
+ {
+ // Reuse the existing Java callback interface to avoid adding more bridge-only types.
+ private const string ListenerInterface = "com.dirichlet.unity.DirichletUnityBridge$LoadListener";
+
+ private readonly Action success;
+ private readonly Action failure;
+
+ public AndroidInitCallback(Action success, Action failure)
+ : base(ListenerInterface)
+ {
+ this.success = success;
+ this.failure = failure;
+ }
+
+ // Called from Java (case-sensitive method name).
+ public void onSuccess()
+ {
+ DirichletSdk.DispatchToUnityThread(() => success?.Invoke());
+ }
+
+ // Called from Java (case-sensitive method name).
+ public void onError(string code, string message)
+ {
+ var errorCode = string.IsNullOrEmpty(code) ? "android_init_failed" : code;
+ DirichletSdk.DispatchToUnityThread(() => failure?.Invoke(new DirichletError(errorCode, message ?? string.Empty)));
+ }
+ }
+
+ public void RequestPermissionIfNeeded()
+ {
+ try
+ {
+ BridgeClass.CallStatic("requestPermissionIfNeeded");
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[Dirichlet][Android] requestPermissionIfNeeded failed: {ex.Message}");
+ }
+ }
+
+ public string GetSdkVersion()
+ {
+ try
+ {
+ return BridgeClass.CallStatic("getSdkVersion") ?? "android-unknown";
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[Dirichlet][Android] getSdkVersion failed: {ex.Message}");
+ return "android-error";
+ }
+ }
+
+ public void LoadRewardVideoAd(DirichletAdRequest request, Action onSuccess, Action onFailure)
+ {
+ LoadAdInternal(DirichletAdType.RewardVideo, request, onSuccess, onFailure);
+ }
+
+ public bool ShowRewardVideoAd(DirichletPlatformAdHandle handle)
+ {
+ return ShowAdInternal(handle, null);
+ }
+
+ public void LoadInterstitialAd(DirichletAdRequest request, Action onSuccess, Action onFailure)
+ {
+ LoadAdInternal(DirichletAdType.Interstitial, request, onSuccess, onFailure);
+ }
+
+ public bool ShowInterstitialAd(DirichletPlatformAdHandle handle)
+ {
+ return ShowAdInternal(handle, null);
+ }
+
+ public void LoadBannerAd(DirichletAdRequest request, Action onSuccess, Action onFailure)
+ {
+ LoadAdInternal(DirichletAdType.Banner, request, onSuccess, onFailure);
+ }
+
+ public bool ShowBannerAd(DirichletPlatformAdHandle handle, DirichletAdShowOptions options)
+ {
+ return ShowAdInternal(handle, options);
+ }
+
+ public void LoadSplashAd(DirichletAdRequest request, Action onSuccess, Action onFailure)
+ {
+ LoadAdInternal(DirichletAdType.Splash, request, onSuccess, onFailure);
+ }
+
+ public bool ShowSplashAd(DirichletPlatformAdHandle handle, DirichletAdShowOptions options)
+ {
+ return ShowAdInternal(handle, options);
+ }
+
+ public void DestroyAd(DirichletPlatformAdHandle handle)
+ {
+ DestroyAdInternal(handle);
+ }
+
+ public bool IsAdValid(DirichletPlatformAdHandle handle)
+ {
+ if (handle == null || string.IsNullOrEmpty(handle.DebugId))
+ {
+ return false;
+ }
+
+ try
+ {
+ return BridgeClass.CallStatic("isAdValid", handle.DebugId);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[DirichletMediation][Android] IsAdValid failed: {ex.Message}");
+ return false;
+ }
+ }
+
+ private void LoadAdInternal(DirichletAdType adType, DirichletAdRequest request, Action onSuccess, Action onFailure)
+ {
+ if (request == null)
+ {
+ DirichletSdk.DispatchToUnityThread(() => onFailure?.Invoke(new DirichletError("invalid_request", "Request cannot be null")));
+ return;
+ }
+
+ try
+ {
+ var payload = request.ToBridgePayload();
+ var callback = new AndroidLoadCallback(this, null, () =>
+ {
+ // Handle will be set in callback's onSuccess
+ }, onFailure);
+
+ string handleId;
+ using (var extras = BuildJsonObject(payload))
+ {
+ // Use the new direct load methods that match native SDK pattern
+ // extras already contains space_id from request.ToBridgePayload()
+ switch (adType)
+ {
+ case DirichletAdType.RewardVideo:
+ handleId = BridgeClass.CallStatic("loadRewardVideoAd", extras, callback);
+ break;
+ case DirichletAdType.Interstitial:
+ handleId = BridgeClass.CallStatic("loadInterstitialAd", extras, callback);
+ break;
+ case DirichletAdType.Banner:
+ handleId = BridgeClass.CallStatic("loadBannerAd", extras, callback);
+ break;
+ case DirichletAdType.Splash:
+ handleId = BridgeClass.CallStatic("loadSplashAd", extras, callback);
+ break;
+ default:
+ DirichletSdk.DispatchToUnityThread(() => onFailure?.Invoke(new DirichletError("unsupported_type", $"Unsupported ad type: {adType}")));
+ return;
+ }
+ }
+
+ if (string.IsNullOrEmpty(handleId))
+ {
+ DirichletSdk.DispatchToUnityThread(() => onFailure?.Invoke(new DirichletError("invalid_handle", "Bridge returned null handle")));
+ return;
+ }
+
+ // Create handle - simple wrapper around handle string
+ var handle = DirichletPlatformAdHandle.FromNative(handleId);
+ callback.SetHandle(handle);
+ callback.SetSuccessCallback(() => onSuccess?.Invoke(handle));
+
+ lock (loadCallbacksLock)
+ {
+ loadCallbacks[handleId] = callback;
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[Dirichlet][Android] LoadAd failed: {ex.Message}");
+ DirichletSdk.DispatchToUnityThread(() => onFailure?.Invoke(new DirichletError("android_exception", ex.Message)));
+ }
+ }
+
+ private bool ShowAdInternal(DirichletPlatformAdHandle handle, DirichletAdShowOptions options)
+ {
+ try
+ {
+ var payload = options?.ToBridgePayload();
+
+ using (var extras = BuildJsonObject(payload))
+ {
+ return BridgeClass.CallStatic("showAd", handle.DebugId, extras);
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[Dirichlet][Android] ShowAd failed: {ex.Message}");
+ return false;
+ }
+ }
+
+ private void DestroyAdInternal(DirichletPlatformAdHandle handle)
+ {
+ try
+ {
+ BridgeClass.CallStatic("destroyAd", handle.DebugId);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[Dirichlet][Android] DestroyAd failed: {ex.Message}");
+ }
+ finally
+ {
+ RemoveLoadCallback(handle?.DebugId);
+ }
+ }
+
+ private AndroidJavaObject BuildJsonObject(Dictionary dictionary)
+ {
+ if (dictionary == null || dictionary.Count == 0)
+ {
+ return null;
+ }
+
+ AndroidJavaObject json = null;
+
+ try
+ {
+ json = new AndroidJavaObject("org.json.JSONObject");
+
+ foreach (var kv in dictionary)
+ {
+ if (string.IsNullOrEmpty(kv.Key))
+ {
+ continue;
+ }
+
+ var value = kv.Value;
+ if (value == null)
+ {
+ continue;
+ }
+
+ try
+ {
+ switch (value)
+ {
+ case bool boolValue:
+ json.Call("put", kv.Key, boolValue);
+ break;
+ case int intValue:
+ json.Call("put", kv.Key, intValue);
+ break;
+ case long longValue:
+ json.Call("put", kv.Key, longValue);
+ break;
+ case float floatValue:
+ json.Call("put", kv.Key, (double)floatValue);
+ break;
+ case double doubleValue:
+ json.Call("put", kv.Key, doubleValue);
+ break;
+ case Enum enumValue:
+ json.Call("put", kv.Key, Convert.ToInt32(enumValue, CultureInfo.InvariantCulture));
+ break;
+ default:
+ json.Call("put", kv.Key, value.ToString());
+ break;
+ }
+ }
+ catch (Exception putEx)
+ {
+ Debug.LogWarning($"[Dirichlet][Android] Failed to add extra {kv.Key}: {putEx.Message}");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[Dirichlet][Android] Failed to build json object: {ex.Message}");
+ }
+
+ return json;
+ }
+
+ private void RemoveLoadCallback(string handleId)
+ {
+ if (string.IsNullOrEmpty(handleId))
+ {
+ return;
+ }
+
+ lock (loadCallbacksLock)
+ {
+ loadCallbacks.Remove(handleId);
+ }
+ }
+
+ private sealed class AndroidLoadCallback : AndroidJavaProxy
+ {
+ private readonly AndroidDirichletBridge owner;
+ private string handleId;
+ private Action success;
+ private readonly Action failure;
+
+ public AndroidLoadCallback(AndroidDirichletBridge owner, string handleId, Action success, Action failure)
+ : base("com.dirichlet.unity.DirichletUnityBridge$LoadListener")
+ {
+ this.owner = owner;
+ this.handleId = handleId;
+ this.success = success;
+ this.failure = failure;
+ }
+
+ public void SetHandle(DirichletPlatformAdHandle handle)
+ {
+ if (handle != null)
+ {
+ handleId = handle.DebugId;
+ }
+ }
+
+ public void SetSuccessCallback(Action callback)
+ {
+ success = callback;
+ }
+
+ public void onSuccess()
+ {
+ owner.RemoveLoadCallback(handleId);
+ DirichletSdk.DispatchToUnityThread(() => success?.Invoke());
+ }
+
+ public void onError(string code, string message)
+ {
+ owner.RemoveLoadCallback(handleId);
+ var errorCode = string.IsNullOrEmpty(code) ? "android_error" : code;
+ DirichletSdk.DispatchToUnityThread(() => failure?.Invoke(new DirichletError(errorCode, message ?? string.Empty)));
+ }
+ }
+
+ public void ShowRewardVideoAutoAd(DirichletAdRequest request, IDirichletRewardVideoAutoAdListener listener)
+ {
+ if (request == null)
+ {
+ DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("invalid_request", "Request cannot be null")));
+ return;
+ }
+
+ try
+ {
+ var payload = request.ToBridgePayload();
+ var callback = new AndroidRewardVideoAutoAdCallback(listener);
+
+ using (var extras = BuildJsonObject(payload))
+ {
+ BridgeClass.CallStatic("showRewardVideoAutoAd", extras, callback);
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[Dirichlet][Android] ShowRewardVideoAutoAd failed: {ex.Message}");
+ DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("android_exception", ex.Message)));
+ }
+ }
+
+ public void ShowInterstitialAutoAd(DirichletAdRequest request, IDirichletInterstitialAutoAdListener listener)
+ {
+ if (request == null)
+ {
+ DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("invalid_request", "Request cannot be null")));
+ return;
+ }
+
+ try
+ {
+ var payload = request.ToBridgePayload();
+ var callback = new AndroidInterstitialAutoAdCallback(listener);
+
+ using (var extras = BuildJsonObject(payload))
+ {
+ BridgeClass.CallStatic("showInterstitialAutoAd", extras, callback);
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[Dirichlet][Android] ShowInterstitialAutoAd failed: {ex.Message}");
+ DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("android_exception", ex.Message)));
+ }
+ }
+
+ public void ShowBannerAutoAd(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletBannerAutoAdListener listener)
+ {
+ if (request == null)
+ {
+ DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("invalid_request", "Request cannot be null")));
+ return;
+ }
+
+ try
+ {
+ var payload = request.ToBridgePayload();
+ var optionsPayload = (options ?? new DirichletAdShowOptions()).ToBridgePayload();
+ var callback = new AndroidBannerAutoAdCallback(listener);
+
+ using (var extras = BuildJsonObject(payload))
+ using (var optsJson = BuildJsonObject(optionsPayload))
+ {
+ BridgeClass.CallStatic("showBannerAutoAd", extras, optsJson, callback);
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[Dirichlet][Android] ShowBannerAutoAd failed: {ex.Message}");
+ DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("android_exception", ex.Message)));
+ }
+ }
+
+ public void ShowSplashAutoAd(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletSplashAutoAdListener listener)
+ {
+ if (request == null)
+ {
+ DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("invalid_request", "Request cannot be null")));
+ return;
+ }
+
+ try
+ {
+ var payload = request.ToBridgePayload();
+ var optionsPayload = (options ?? new DirichletAdShowOptions()).ToBridgePayload();
+ var callback = new AndroidSplashAutoAdCallback(listener);
+
+ using (var extras = BuildJsonObject(payload))
+ using (var optsJson = BuildJsonObject(optionsPayload))
+ {
+ BridgeClass.CallStatic("showSplashAutoAd", extras, optsJson, callback);
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[Dirichlet][Android] ShowSplashAutoAd failed: {ex.Message}");
+ DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("android_exception", ex.Message)));
+ }
+ }
+
+ public void PreLoad(DirichletAdRequest request, int type)
+ {
+ if (request == null)
+ {
+ Debug.LogWarning("[Dirichlet][Android] PreLoad called with null request");
+ return;
+ }
+
+ try
+ {
+ var payload = request.ToBridgePayload();
+ using (var extras = BuildJsonObject(payload))
+ {
+ BridgeClass.CallStatic("preLoad", extras, type);
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[Dirichlet][Android] PreLoad failed: {ex.Message}");
+ }
+ }
+
+ private sealed class AndroidInterstitialAutoAdCallback : AndroidJavaProxy
+ {
+ private readonly IDirichletInterstitialAutoAdListener listener;
+
+ public AndroidInterstitialAutoAdCallback(IDirichletInterstitialAutoAdListener listener)
+ : base("com.dirichlet.unity.DirichletUnityBridge$InterstitialAutoAdListener")
+ {
+ this.listener = listener;
+ }
+
+ public void onError(string code, string message)
+ {
+ var errorCode = string.IsNullOrEmpty(code) ? "android_error" : code;
+ DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError(errorCode, message ?? string.Empty)));
+ }
+
+ public void onAdShow() { DirichletSdk.DispatchToUnityThread(() => listener?.OnAdShow()); }
+ public void onAdClose() { DirichletSdk.DispatchToUnityThread(() => listener?.OnAdClose()); }
+ public void onAdClick() { DirichletSdk.DispatchToUnityThread(() => listener?.OnAdClick()); }
+ }
+
+ private sealed class AndroidBannerAutoAdCallback : AndroidJavaProxy
+ {
+ private readonly IDirichletBannerAutoAdListener listener;
+
+ public AndroidBannerAutoAdCallback(IDirichletBannerAutoAdListener listener)
+ : base("com.dirichlet.unity.DirichletUnityBridge$BannerAutoAdListener")
+ {
+ this.listener = listener;
+ }
+
+ public void onError(string code, string message)
+ {
+ var errorCode = string.IsNullOrEmpty(code) ? "android_error" : code;
+ DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError(errorCode, message ?? string.Empty)));
+ }
+
+ public void onAdShow() { DirichletSdk.DispatchToUnityThread(() => listener?.OnAdShow()); }
+ public void onAdClose() { DirichletSdk.DispatchToUnityThread(() => listener?.OnAdClose()); }
+ public void onAdClick() { DirichletSdk.DispatchToUnityThread(() => listener?.OnAdClick()); }
+ }
+
+ private sealed class AndroidSplashAutoAdCallback : AndroidJavaProxy
+ {
+ private readonly IDirichletSplashAutoAdListener listener;
+
+ public AndroidSplashAutoAdCallback(IDirichletSplashAutoAdListener listener)
+ : base("com.dirichlet.unity.DirichletUnityBridge$SplashAutoAdListener")
+ {
+ this.listener = listener;
+ }
+
+ public void onError(string code, string message)
+ {
+ var errorCode = string.IsNullOrEmpty(code) ? "android_error" : code;
+ DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError(errorCode, message ?? string.Empty)));
+ }
+
+ public void onAdShow() { DirichletSdk.DispatchToUnityThread(() => listener?.OnAdShow()); }
+ public void onAdClose() { DirichletSdk.DispatchToUnityThread(() => listener?.OnAdClose()); }
+ public void onAdClick() { DirichletSdk.DispatchToUnityThread(() => listener?.OnAdClick()); }
+ }
+
+ private sealed class AndroidRewardVideoAutoAdCallback : AndroidJavaProxy
+ {
+ private readonly IDirichletRewardVideoAutoAdListener listener;
+
+ public AndroidRewardVideoAutoAdCallback(IDirichletRewardVideoAutoAdListener listener)
+ : base("com.dirichlet.unity.DirichletUnityBridge$RewardVideoAutoAdListener")
+ {
+ this.listener = listener;
+ }
+
+ public void onError(string code, string message)
+ {
+ var errorCode = string.IsNullOrEmpty(code) ? "android_error" : code;
+ DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError(errorCode, message ?? string.Empty)));
+ }
+
+ public void onAdShow()
+ {
+ DirichletSdk.DispatchToUnityThread(() => listener?.OnAdShow());
+ }
+
+ public void onAdClose()
+ {
+ DirichletSdk.DispatchToUnityThread(() => listener?.OnAdClose());
+ }
+
+ public void onRewardVerify(bool rewardVerify, int rewardAmount, string rewardName, int code, string msg)
+ {
+ var args = new DirichletRewardVerificationEventArgs(rewardVerify, rewardAmount, rewardName ?? string.Empty, code, msg ?? string.Empty);
+ DirichletSdk.DispatchToUnityThread(() => listener?.OnRewardVerify(args));
+ }
+
+ public void onAdClick()
+ {
+ DirichletSdk.DispatchToUnityThread(() => listener?.OnAdClick());
+ }
+ }
+ }
+#elif UNITY_IOS && !UNITY_EDITOR
+ internal sealed class IOSDirichletBridge : IDirichletPlatformBridge
+ {
+ [System.Runtime.InteropServices.DllImport("__Internal")]
+ private static extern bool DirichletMediationUnityBridge_Initialize(
+ string mediaId, string mediaKey, bool enableLog, string mediaName,
+ string gameChannel, bool shakeEnabled, bool allowIDFAAccess, string aTags);
+
+ [System.Runtime.InteropServices.DllImport("__Internal")]
+ private static extern void DirichletMediationUnityBridge_RequestPermissionIfNeeded();
+
+ [System.Runtime.InteropServices.DllImport("__Internal")]
+ private static extern string DirichletMediationUnityBridge_GetSdkVersion();
+
+ [System.Runtime.InteropServices.DllImport("__Internal")]
+ private static extern string DirichletMediationUnityBridge_LoadRewardVideoAd(long spaceId, string extras);
+
+ [System.Runtime.InteropServices.DllImport("__Internal")]
+ private static extern string DirichletMediationUnityBridge_LoadInterstitialAd(long spaceId, string extras);
+
+ [System.Runtime.InteropServices.DllImport("__Internal")]
+ private static extern string DirichletMediationUnityBridge_LoadBannerAd(long spaceId, string extras);
+
+ [System.Runtime.InteropServices.DllImport("__Internal")]
+ private static extern string DirichletMediationUnityBridge_LoadSplashAd(long spaceId, string extras);
+
+ [System.Runtime.InteropServices.DllImport("__Internal")]
+ private static extern bool DirichletMediationUnityBridge_ShowAd(string handleId, string extras);
+
+ [System.Runtime.InteropServices.DllImport("__Internal")]
+ private static extern void DirichletMediationUnityBridge_DestroyAd(string handleId);
+
+ [System.Runtime.InteropServices.DllImport("__Internal")]
+ private static extern bool DirichletMediationUnityBridge_IsAdValid(string handleId);
+
+ private readonly Dictionary loadCallbacks = new Dictionary();
+ private readonly object loadCallbacksLock = new object();
+ private static bool loadCallbackReceiverInitialized;
+ private static bool initCallbackReceiverInitialized;
+ private static readonly object initCallbackLock = new object();
+ private static Action pendingInitSuccess;
+ private static Action pendingInitFailure;
+
+ public void Initialize(DirichletPlatformInitOptions options, Action onSuccess, Action onFailure)
+ {
+ if (options == null)
+ {
+ DirichletSdk.DispatchToUnityThread(() => onFailure?.Invoke(new DirichletError("ios_invalid_options", "Initialization options cannot be null")));
+ return;
+ }
+
+ // Register callbacks for async result
+ lock (initCallbackLock)
+ {
+ pendingInitSuccess = onSuccess;
+ pendingInitFailure = onFailure;
+ }
+
+ EnsureInitCallbackReceiver();
+
+ try
+ {
+ // iOS Mediation SDK uses async callback (aligned with Ad Unity implementation)
+ var started = DirichletMediationUnityBridge_Initialize(
+ options.GetAppIdString(),
+ options.MediaKey ?? string.Empty,
+ options.EnableLog,
+ options.MediaName ?? string.Empty,
+ options.Channel ?? string.Empty,
+ options.ShakeEnabled,
+ options.AllowIDFAAccess,
+ options.ATags ?? string.Empty);
+
+ if (!started)
+ {
+ lock (initCallbackLock)
+ {
+ pendingInitSuccess = null;
+ pendingInitFailure = null;
+ }
+
+ DirichletSdk.DispatchToUnityThread(() =>
+ onFailure?.Invoke(new DirichletError("ios_init_rejected", "Initialization could not be started")));
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.LogException(ex);
+ lock (initCallbackLock)
+ {
+ pendingInitSuccess = null;
+ pendingInitFailure = null;
+ }
+
+ DirichletSdk.DispatchToUnityThread(() =>
+ onFailure?.Invoke(new DirichletError("ios_exception", ex.Message)));
+ }
+ }
+
+ public void RequestPermissionIfNeeded()
+ {
+ try
+ {
+ DirichletMediationUnityBridge_RequestPermissionIfNeeded();
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[DirichletMediation][iOS] RequestPermissionIfNeeded failed: {ex.Message}");
+ }
+ }
+
+ public string GetSdkVersion()
+ {
+ try
+ {
+ return DirichletMediationUnityBridge_GetSdkVersion() ?? "ios-unknown";
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[DirichletMediation][iOS] GetSdkVersion failed: {ex.Message}");
+ return "ios-error";
+ }
+ }
+
+ public void LoadRewardVideoAd(DirichletAdRequest request, Action onSuccess, Action onFailure)
+ {
+ LoadAdInternal(DirichletAdType.RewardVideo, request, onSuccess, onFailure);
+ }
+
+ public bool ShowRewardVideoAd(DirichletPlatformAdHandle handle)
+ {
+ return ShowAdInternal(handle, null);
+ }
+
+ public void LoadInterstitialAd(DirichletAdRequest request, Action onSuccess, Action onFailure)
+ {
+ LoadAdInternal(DirichletAdType.Interstitial, request, onSuccess, onFailure);
+ }
+
+ public bool ShowInterstitialAd(DirichletPlatformAdHandle handle)
+ {
+ return ShowAdInternal(handle, null);
+ }
+
+ public void LoadBannerAd(DirichletAdRequest request, Action onSuccess, Action onFailure)
+ {
+ LoadAdInternal(DirichletAdType.Banner, request, onSuccess, onFailure);
+ }
+
+ public bool ShowBannerAd(DirichletPlatformAdHandle handle, DirichletAdShowOptions options)
+ {
+ return ShowAdInternal(handle, options);
+ }
+
+ public void LoadSplashAd(DirichletAdRequest request, Action onSuccess, Action onFailure)
+ {
+ LoadAdInternal(DirichletAdType.Splash, request, onSuccess, onFailure);
+ }
+
+ public bool ShowSplashAd(DirichletPlatformAdHandle handle, DirichletAdShowOptions options)
+ {
+ return ShowAdInternal(handle, options);
+ }
+
+ public void DestroyAd(DirichletPlatformAdHandle handle)
+ {
+ DestroyAdInternal(handle);
+ }
+
+ public bool IsAdValid(DirichletPlatformAdHandle handle)
+ {
+ if (handle == null || string.IsNullOrEmpty(handle.DebugId))
+ {
+ return false;
+ }
+
+ try
+ {
+ return DirichletMediationUnityBridge_IsAdValid(handle.DebugId);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[DirichletMediation][iOS] IsAdValid failed: {ex.Message}");
+ return false;
+ }
+ }
+
+ private void LoadAdInternal(DirichletAdType adType, DirichletAdRequest request, Action onSuccess, Action onFailure)
+ {
+ if (request == null)
+ {
+ DirichletSdk.DispatchToUnityThread(() => onFailure?.Invoke(new DirichletError("invalid_request", "Request cannot be null")));
+ return;
+ }
+
+ try
+ {
+ var payload = request.ToBridgePayload();
+ var callback = new IOSLoadCallback(this, null, () => { }, onFailure);
+
+ string handleId;
+ var extrasJson = BuildJsonString(payload);
+
+ switch (adType)
+ {
+ case DirichletAdType.RewardVideo:
+ handleId = DirichletMediationUnityBridge_LoadRewardVideoAd(request.SpaceId, extrasJson);
+ break;
+ case DirichletAdType.Interstitial:
+ handleId = DirichletMediationUnityBridge_LoadInterstitialAd(request.SpaceId, extrasJson);
+ break;
+ case DirichletAdType.Banner:
+ handleId = DirichletMediationUnityBridge_LoadBannerAd(request.SpaceId, extrasJson);
+ break;
+ case DirichletAdType.Splash:
+ handleId = DirichletMediationUnityBridge_LoadSplashAd(request.SpaceId, extrasJson);
+ break;
+ default:
+ DirichletSdk.DispatchToUnityThread(() => onFailure?.Invoke(new DirichletError("unsupported_type", $"Unsupported ad type: {adType}")));
+ return;
+ }
+
+ if (string.IsNullOrEmpty(handleId))
+ {
+ DirichletSdk.DispatchToUnityThread(() => onFailure?.Invoke(new DirichletError("invalid_handle", "Bridge returned null handle")));
+ return;
+ }
+
+ // Create handle and setup callback
+ var handle = DirichletPlatformAdHandle.FromNative(handleId);
+ callback.SetHandle(handle);
+ callback.SetSuccessCallback(() => onSuccess?.Invoke(handle));
+
+ lock (loadCallbacksLock)
+ {
+ loadCallbacks[handleId] = callback;
+ }
+
+ // Ensure load callback receiver is initialized
+ EnsureLoadCallbackReceiver();
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[DirichletMediation][iOS] LoadAd failed: {ex.Message}");
+ DirichletSdk.DispatchToUnityThread(() => onFailure?.Invoke(new DirichletError("ios_exception", ex.Message)));
+ }
+ }
+
+ private bool ShowAdInternal(DirichletPlatformAdHandle handle, DirichletAdShowOptions options)
+ {
+ try
+ {
+ var payload = options?.ToBridgePayload();
+ var extrasJson = BuildJsonString(payload);
+ return DirichletMediationUnityBridge_ShowAd(handle.DebugId, extrasJson);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[DirichletMediation][iOS] ShowAd failed: {ex.Message}");
+ return false;
+ }
+ }
+
+ private void DestroyAdInternal(DirichletPlatformAdHandle handle)
+ {
+ try
+ {
+ DirichletMediationUnityBridge_DestroyAd(handle.DebugId);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[DirichletMediation][iOS] DestroyAd failed: {ex.Message}");
+ }
+ finally
+ {
+ RemoveLoadCallback(handle?.DebugId);
+ }
+ }
+
+ private string BuildJsonString(Dictionary dictionary)
+ {
+ if (dictionary == null || dictionary.Count == 0)
+ {
+ return string.Empty;
+ }
+
+ try
+ {
+ var jsonBuilder = new System.Text.StringBuilder();
+ jsonBuilder.Append("{");
+ var first = true;
+
+ foreach (var kv in dictionary)
+ {
+ if (string.IsNullOrEmpty(kv.Key) || kv.Value == null)
+ {
+ continue;
+ }
+
+ if (!first)
+ {
+ jsonBuilder.Append(",");
+ }
+ first = false;
+
+ jsonBuilder.Append($"\"{kv.Key}\":");
+
+ if (kv.Value is string)
+ {
+ jsonBuilder.Append($"\"{kv.Value}\"");
+ }
+ else if (kv.Value is bool)
+ {
+ jsonBuilder.Append(((bool)kv.Value) ? "true" : "false");
+ }
+ else
+ {
+ jsonBuilder.Append(kv.Value.ToString());
+ }
+ }
+
+ jsonBuilder.Append("}");
+ return jsonBuilder.ToString();
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[DirichletMediation][iOS] Failed to build json string: {ex.Message}");
+ return string.Empty;
+ }
+ }
+
+ private void RemoveLoadCallback(string handleId)
+ {
+ if (string.IsNullOrEmpty(handleId))
+ {
+ return;
+ }
+
+ lock (loadCallbacksLock)
+ {
+ loadCallbacks.Remove(handleId);
+ }
+ }
+
+ private void EnsureLoadCallbackReceiver()
+ {
+ if (!DirichletSdk.IsUnityThread)
+ {
+ DirichletSdk.DispatchToUnityThread(EnsureLoadCallbackReceiver);
+ return;
+ }
+
+ const string receiverName = "DirichletMediationIOSLoadCallbackReceiver";
+
+ if (loadCallbackReceiverInitialized)
+ {
+ var existing = GameObject.Find(receiverName);
+ if (existing != null)
+ {
+ return;
+ }
+ Debug.LogWarning("[DirichletMediation][iOS] LoadCallbackReceiver was destroyed, recreating...");
+ loadCallbackReceiverInitialized = false;
+ }
+
+ var host = new GameObject(receiverName)
+ {
+ hideFlags = HideFlags.HideAndDontSave
+ };
+ UnityEngine.Object.DontDestroyOnLoad(host);
+ var receiver = host.AddComponent();
+ receiver.bridge = this;
+
+ loadCallbackReceiverInitialized = true;
+ }
+
+ private void EnsureInitCallbackReceiver()
+ {
+ if (!DirichletSdk.IsUnityThread)
+ {
+ DirichletSdk.DispatchToUnityThread(EnsureInitCallbackReceiver);
+ return;
+ }
+
+ const string receiverName = "DirichletMediationIOSInitCallbackReceiver";
+ if (initCallbackReceiverInitialized)
+ {
+ var existing = GameObject.Find(receiverName);
+ if (existing != null)
+ {
+ return;
+ }
+
+ Debug.LogWarning("[DirichletMediation][iOS] InitCallbackReceiver was destroyed, recreating...");
+ initCallbackReceiverInitialized = false;
+ }
+
+ var host = new GameObject(receiverName)
+ {
+ hideFlags = HideFlags.HideAndDontSave
+ };
+ UnityEngine.Object.DontDestroyOnLoad(host);
+ var receiver = host.AddComponent();
+ receiver.bridge = this;
+
+ initCallbackReceiverInitialized = true;
+ }
+
+ internal void HandleLoadCallback(string payload)
+ {
+ if (string.IsNullOrEmpty(payload))
+ {
+ return;
+ }
+
+ try
+ {
+ var message = JsonUtility.FromJson(payload);
+ if (message == null || string.IsNullOrEmpty(message.handle))
+ {
+ return;
+ }
+
+ IOSLoadCallback callback;
+ lock (loadCallbacksLock)
+ {
+ if (!loadCallbacks.TryGetValue(message.handle, out callback))
+ {
+ Debug.LogWarning($"[DirichletMediation][iOS] No callback found for handle: {message.handle}");
+ return;
+ }
+ }
+
+ if (message.eventName == "load_success")
+ {
+ callback.OnSuccess();
+ }
+ else if (message.eventName == "load_error")
+ {
+ var code = message.data?.code.ToString() ?? "unknown";
+ var msg = message.data?.message ?? "Unknown error";
+ callback.OnError(code, msg);
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[DirichletMediation][iOS] Failed to handle load callback: {ex.Message}\n{payload}");
+ }
+ }
+
+ internal void HandleInitCallback(string payload)
+ {
+ if (string.IsNullOrEmpty(payload))
+ {
+ return;
+ }
+
+ InitCallbackPayload message = null;
+ try
+ {
+ message = JsonUtility.FromJson(payload);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[DirichletMediation][iOS] Failed to parse init callback: {ex.Message}\n{payload}");
+ }
+
+ var success = message?.success ?? false;
+ var data = message?.data;
+ var code = data?.code ?? -1;
+ var domain = data?.domain;
+ var description = data?.message;
+
+ Action successCallback;
+ Action failureCallback;
+
+ lock (initCallbackLock)
+ {
+ successCallback = pendingInitSuccess;
+ failureCallback = pendingInitFailure;
+ pendingInitSuccess = null;
+ pendingInitFailure = null;
+ }
+
+ if (success)
+ {
+ var messageText = string.IsNullOrEmpty(description) ? "ios_mediation_bridge" : description;
+ var result = DirichletInitResult.Ok(messageText);
+ DirichletSdk.DispatchToUnityThread(() => successCallback?.Invoke(result));
+ return;
+ }
+
+ var errorCode = code > 0 ? $"ios_init_{code}" : "ios_init_failed";
+ var errorMessage = string.IsNullOrEmpty(description) ? "Initialization failed" : description;
+ if (!string.IsNullOrEmpty(domain))
+ {
+ errorMessage = $"{errorMessage} ({domain})";
+ }
+
+ var error = new DirichletError(errorCode, errorMessage);
+ DirichletSdk.DispatchToUnityThread(() =>
+ {
+ if (failureCallback != null)
+ {
+ failureCallback(error);
+ }
+ else
+ {
+ Debug.LogWarning($"[DirichletMediation][iOS] Init failure received but no callback registered: {error}");
+ }
+ });
+ }
+
+ [Serializable]
+ private class LoadCallbackPayload
+ {
+ public string handle;
+ public string eventName;
+ public string adType;
+ public LoadCallbackPayloadData data;
+ }
+
+ [Serializable]
+ private class LoadCallbackPayloadData
+ {
+ public int code;
+ public string message;
+ }
+
+ [Serializable]
+ private class InitCallbackPayload
+ {
+ public bool success;
+ public InitCallbackPayloadData data;
+ }
+
+ [Serializable]
+ private class InitCallbackPayloadData
+ {
+ public int code;
+ public string message;
+ public string domain;
+ }
+
+ private class IOSLoadCallbackReceiver : MonoBehaviour
+ {
+ public IOSDirichletBridge bridge;
+
+ public void OnLoadCallback(string payload)
+ {
+ bridge?.HandleLoadCallback(payload);
+ }
+ }
+
+ private class IOSInitCallbackReceiver : MonoBehaviour
+ {
+ public IOSDirichletBridge bridge;
+
+ public void OnInitCallback(string payload)
+ {
+ bridge?.HandleInitCallback(payload);
+ }
+ }
+
+ private sealed class IOSLoadCallback
+ {
+ private readonly IOSDirichletBridge owner;
+ private string handleId;
+ private Action success;
+ private readonly Action failure;
+
+ public IOSLoadCallback(IOSDirichletBridge owner, string handleId, Action success, Action failure)
+ {
+ this.owner = owner;
+ this.handleId = handleId;
+ this.success = success;
+ this.failure = failure;
+ }
+
+ public void SetHandle(DirichletPlatformAdHandle handle)
+ {
+ if (handle != null)
+ {
+ handleId = handle.DebugId;
+ }
+ }
+
+ public void SetSuccessCallback(Action callback)
+ {
+ success = callback;
+ }
+
+ public void OnSuccess()
+ {
+ owner.RemoveLoadCallback(handleId);
+ DirichletSdk.DispatchToUnityThread(() => success?.Invoke());
+ }
+
+ public void OnError(string code, string message)
+ {
+ owner.RemoveLoadCallback(handleId);
+ DirichletSdk.DispatchToUnityThread(() => failure?.Invoke(new DirichletError(code, message)));
+ }
+ }
+
+ public void ShowRewardVideoAutoAd(DirichletAdRequest request, IDirichletRewardVideoAutoAdListener listener)
+ {
+ // iOS hasn't added this API yet
+ DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("not_supported", "showRewardVideoAutoAd is not supported on iOS yet")));
+ }
+
+ public void ShowInterstitialAutoAd(DirichletAdRequest request, IDirichletInterstitialAutoAdListener listener)
+ {
+ DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("not_supported", "showInterstitialAutoAd is not supported on iOS yet")));
+ }
+
+ public void ShowBannerAutoAd(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletBannerAutoAdListener listener)
+ {
+ DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("not_supported", "showBannerAutoAd is not supported on iOS yet")));
+ }
+
+ public void ShowSplashAutoAd(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletSplashAutoAdListener listener)
+ {
+ DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("not_supported", "showSplashAutoAd is not supported on iOS yet")));
+ }
+
+ public void PreLoad(DirichletAdRequest request, int type)
+ {
+ Debug.LogWarning("[DirichletMediation][iOS] PreLoad is not supported on iOS yet");
+ }
+ }
+#else
+ internal sealed class NoopDirichletBridge : IDirichletPlatformBridge
+ {
+ public void Initialize(DirichletPlatformInitOptions options, Action onSuccess, Action onFailure)
+ {
+ Debug.LogWarning("[Dirichlet] No platform bridge available (editor/unsupported platform).");
+ DirichletSdk.DispatchToUnityThread(() => onSuccess?.Invoke(DirichletInitResult.Ok("noop")));
+ }
+
+ public void InitializeWithoutTap(DirichletPlatformInitOptions options, Action onSuccess, Action onFailure)
+ {
+ Debug.LogWarning("[Dirichlet] InitializeWithoutTap ignored on noop bridge.");
+ DirichletSdk.DispatchToUnityThread(() => onSuccess?.Invoke(DirichletInitResult.Ok("noop")));
+ }
+
+ public void UpdateConfig(DirichletPlatformInitOptions options)
+ {
+ Debug.Log("[Dirichlet] UpdateConfig ignored on noop bridge.");
+ }
+
+ public void RequestPermissionIfNeeded()
+ {
+ Debug.Log("[Dirichlet] RequestPermissionIfNeeded ignored on noop bridge.");
+ }
+
+ public string GetSdkVersion() => "noop";
+
+ private static DirichletPlatformAdHandle CreateStubHandle()
+ {
+ return DirichletPlatformAdHandle.CreateStub();
+ }
+
+ private static void LoadStubAd(DirichletPlatformAdHandle handle, Action onSuccess)
+ {
+ Debug.Log($"[Dirichlet] LoadAd noop for {handle.DebugId}");
+ DirichletSdk.DispatchToUnityThread(() => onSuccess?.Invoke(handle));
+ }
+
+ public void LoadRewardVideoAd(DirichletAdRequest request, Action onSuccess, Action onFailure)
+ {
+ var handle = CreateStubHandle();
+ LoadStubAd(handle, onSuccess);
+ }
+
+ public bool ShowRewardVideoAd(DirichletPlatformAdHandle handle)
+ {
+ Debug.Log($"[Dirichlet] ShowRewardAd noop for {handle.DebugId}");
+ return true;
+ }
+
+ public void LoadInterstitialAd(DirichletAdRequest request, Action onSuccess, Action onFailure)
+ {
+ var handle = CreateStubHandle();
+ LoadStubAd(handle, onSuccess);
+ }
+
+ public bool ShowInterstitialAd(DirichletPlatformAdHandle handle)
+ {
+ Debug.Log($"[Dirichlet] ShowInterstitialAd noop for {handle.DebugId}");
+ return true;
+ }
+
+ public void LoadBannerAd(DirichletAdRequest request, Action onSuccess, Action onFailure)
+ {
+ var handle = CreateStubHandle();
+ LoadStubAd(handle, onSuccess);
+ }
+
+ public bool ShowBannerAd(DirichletPlatformAdHandle handle, DirichletAdShowOptions options)
+ {
+ Debug.Log($"[Dirichlet] ShowBannerAd noop for {handle.DebugId}");
+ return true;
+ }
+
+ public void LoadSplashAd(DirichletAdRequest request, Action onSuccess, Action onFailure)
+ {
+ var handle = CreateStubHandle();
+ LoadStubAd(handle, onSuccess);
+ }
+
+ public bool ShowSplashAd(DirichletPlatformAdHandle handle, DirichletAdShowOptions options)
+ {
+ Debug.Log($"[Dirichlet] ShowSplashAd noop for {handle.DebugId}");
+ return true;
+ }
+
+ public void DestroyAd(DirichletPlatformAdHandle handle)
+ {
+ Debug.Log($"[Dirichlet] DestroyAd noop for {handle.DebugId}");
+ }
+
+ public bool IsAdValid(DirichletPlatformAdHandle handle)
+ {
+ Debug.Log($"[Dirichlet] IsAdValid noop for {handle?.DebugId}");
+ return true;
+ }
+
+ public void ShowRewardVideoAutoAd(DirichletAdRequest request, IDirichletRewardVideoAutoAdListener listener)
+ {
+ Debug.Log("[Dirichlet] ShowRewardVideoAutoAd noop");
+ DirichletSdk.DispatchToUnityThread(() =>
+ {
+ listener?.OnAdShow();
+ listener?.OnRewardVerify(new DirichletRewardVerificationEventArgs(true, 10, "noop_reward", 0, "noop"));
+ listener?.OnAdClose();
+ });
+ }
+
+ public void ShowInterstitialAutoAd(DirichletAdRequest request, IDirichletInterstitialAutoAdListener listener)
+ {
+ Debug.Log("[Dirichlet] ShowInterstitialAutoAd noop");
+ DirichletSdk.DispatchToUnityThread(() =>
+ {
+ listener?.OnAdShow();
+ listener?.OnAdClose();
+ });
+ }
+
+ public void ShowBannerAutoAd(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletBannerAutoAdListener listener)
+ {
+ Debug.Log("[Dirichlet] ShowBannerAutoAd noop");
+ DirichletSdk.DispatchToUnityThread(() => listener?.OnAdShow());
+ }
+
+ public void ShowSplashAutoAd(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletSplashAutoAdListener listener)
+ {
+ Debug.Log("[Dirichlet] ShowSplashAutoAd noop");
+ DirichletSdk.DispatchToUnityThread(() =>
+ {
+ listener?.OnAdShow();
+ listener?.OnAdClose();
+ });
+ }
+
+ public void PreLoad(DirichletAdRequest request, int type)
+ {
+ Debug.Log($"[Dirichlet] PreLoad noop (type={type})");
+ }
+ }
+#endif
+
+ #endregion
+}
+
+
diff --git a/DirichletMediation/Runtime/DirichletMediationSdk.cs.meta b/DirichletMediation/Runtime/DirichletMediationSdk.cs.meta
new file mode 100644
index 0000000..30427e3
--- /dev/null
+++ b/DirichletMediation/Runtime/DirichletMediationSdk.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 45ede85e8de28482f92f239b9e824766
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins.meta b/Plugins.meta
new file mode 100644
index 0000000..f3d65c9
--- /dev/null
+++ b/Plugins.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 30b3bfe6f7c94fd2ba05c41c8a8ae1f1
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins/Android.meta b/Plugins/Android.meta
new file mode 100644
index 0000000..e8f85df
--- /dev/null
+++ b/Plugins/Android.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 652205868ad149ecae26404dc3f16554
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins/Android/AndroidManifest.xml b/Plugins/Android/AndroidManifest.xml
new file mode 100644
index 0000000..ce30331
--- /dev/null
+++ b/Plugins/Android/AndroidManifest.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Plugins/Android/AndroidManifest.xml.meta b/Plugins/Android/AndroidManifest.xml.meta
new file mode 100644
index 0000000..cce2dda
--- /dev/null
+++ b/Plugins/Android/AndroidManifest.xml.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 3c972780d1f434d0cb5b25ce1dd8861b
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins/Android/DirichletMediation.meta b/Plugins/Android/DirichletMediation.meta
new file mode 100644
index 0000000..3717ddb
--- /dev/null
+++ b/Plugins/Android/DirichletMediation.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 228b690bf488f4593868f1ddf81e5a45
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins/Android/DirichletMediation/libs.meta b/Plugins/Android/DirichletMediation/libs.meta
new file mode 100644
index 0000000..1cae7cc
--- /dev/null
+++ b/Plugins/Android/DirichletMediation/libs.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 3733efcb61dd489fa8152ca50efff39a
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins/Android/DirichletMediation/libs/DirichletAD_CSJ_Adapter_4.2.5.0.aar b/Plugins/Android/DirichletMediation/libs/DirichletAD_CSJ_Adapter_4.2.5.0.aar
new file mode 100644
index 0000000..7c277fe
Binary files /dev/null and b/Plugins/Android/DirichletMediation/libs/DirichletAD_CSJ_Adapter_4.2.5.0.aar differ
diff --git a/Plugins/Android/DirichletMediation/libs/DirichletAD_CSJ_Adapter_4.2.5.0.aar.meta b/Plugins/Android/DirichletMediation/libs/DirichletAD_CSJ_Adapter_4.2.5.0.aar.meta
new file mode 100644
index 0000000..443cf59
--- /dev/null
+++ b/Plugins/Android/DirichletMediation/libs/DirichletAD_CSJ_Adapter_4.2.5.0.aar.meta
@@ -0,0 +1,32 @@
+fileFormatVersion: 2
+guid: c7970f095313643b682bb2d27b4ede2f
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 0
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ - first:
+ Android: Android
+ second:
+ enabled: 1
+ settings: {}
+ - first:
+ Any:
+ second:
+ enabled: 0
+ settings: {}
+ - first:
+ Editor: Editor
+ second:
+ enabled: 0
+ settings:
+ DefaultValueInitialized: true
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins/Android/DirichletMediation/libs/DirichletAD_GDT_Adapter_4.2.5.0.aar b/Plugins/Android/DirichletMediation/libs/DirichletAD_GDT_Adapter_4.2.5.0.aar
new file mode 100644
index 0000000..f00d661
Binary files /dev/null and b/Plugins/Android/DirichletMediation/libs/DirichletAD_GDT_Adapter_4.2.5.0.aar differ
diff --git a/Plugins/Android/DirichletMediation/libs/DirichletAD_GDT_Adapter_4.2.5.0.aar.meta b/Plugins/Android/DirichletMediation/libs/DirichletAD_GDT_Adapter_4.2.5.0.aar.meta
new file mode 100644
index 0000000..d7f63c4
--- /dev/null
+++ b/Plugins/Android/DirichletMediation/libs/DirichletAD_GDT_Adapter_4.2.5.0.aar.meta
@@ -0,0 +1,32 @@
+fileFormatVersion: 2
+guid: 174b13ee51c7a444fb02d6f2dfdb894f
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 0
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ - first:
+ Android: Android
+ second:
+ enabled: 1
+ settings: {}
+ - first:
+ Any:
+ second:
+ enabled: 0
+ settings: {}
+ - first:
+ Editor: Editor
+ second:
+ enabled: 0
+ settings:
+ DefaultValueInitialized: true
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins/Android/DirichletMediation/libs/DirichletAD_IQY_Adapter_4.2.5.0.aar b/Plugins/Android/DirichletMediation/libs/DirichletAD_IQY_Adapter_4.2.5.0.aar
new file mode 100644
index 0000000..4868a04
Binary files /dev/null and b/Plugins/Android/DirichletMediation/libs/DirichletAD_IQY_Adapter_4.2.5.0.aar differ
diff --git a/Plugins/Android/DirichletMediation/libs/DirichletAD_IQY_Adapter_4.2.5.0.aar.meta b/Plugins/Android/DirichletMediation/libs/DirichletAD_IQY_Adapter_4.2.5.0.aar.meta
new file mode 100644
index 0000000..685c79a
--- /dev/null
+++ b/Plugins/Android/DirichletMediation/libs/DirichletAD_IQY_Adapter_4.2.5.0.aar.meta
@@ -0,0 +1,32 @@
+fileFormatVersion: 2
+guid: 514287dbd7374309a1b94a565a605659
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 0
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ - first:
+ Android: Android
+ second:
+ enabled: 1
+ settings: {}
+ - first:
+ Any:
+ second:
+ enabled: 0
+ settings: {}
+ - first:
+ Editor: Editor
+ second:
+ enabled: 0
+ settings:
+ DefaultValueInitialized: true
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins/Android/DirichletMediation/libs/DirichletAD_Mediation_4.2.5.0.aar b/Plugins/Android/DirichletMediation/libs/DirichletAD_Mediation_4.2.5.0.aar
new file mode 100644
index 0000000..6746cea
Binary files /dev/null and b/Plugins/Android/DirichletMediation/libs/DirichletAD_Mediation_4.2.5.0.aar differ
diff --git a/Plugins/Android/DirichletMediation/libs/DirichletAD_Mediation_4.2.5.0.aar.meta b/Plugins/Android/DirichletMediation/libs/DirichletAD_Mediation_4.2.5.0.aar.meta
new file mode 100644
index 0000000..ef81e3d
--- /dev/null
+++ b/Plugins/Android/DirichletMediation/libs/DirichletAD_Mediation_4.2.5.0.aar.meta
@@ -0,0 +1,32 @@
+fileFormatVersion: 2
+guid: 4866e4d90c0ff47018ffd97469eace11
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 0
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ - first:
+ Android: Android
+ second:
+ enabled: 1
+ settings: {}
+ - first:
+ Any:
+ second:
+ enabled: 0
+ settings: {}
+ - first:
+ Editor: Editor
+ second:
+ enabled: 0
+ settings:
+ DefaultValueInitialized: true
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins/Android/DirichletMediation/src.meta b/Plugins/Android/DirichletMediation/src.meta
new file mode 100644
index 0000000..ab68a8e
--- /dev/null
+++ b/Plugins/Android/DirichletMediation/src.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 2d971358c2bfb48d0b3f792a83924f42
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins/Android/DirichletMediation/src/main.meta b/Plugins/Android/DirichletMediation/src/main.meta
new file mode 100644
index 0000000..3fded90
--- /dev/null
+++ b/Plugins/Android/DirichletMediation/src/main.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d720fa5d414114f8cb2794a88c403f42
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins/Android/DirichletMediation/src/main/java.meta b/Plugins/Android/DirichletMediation/src/main/java.meta
new file mode 100644
index 0000000..6b5d529
--- /dev/null
+++ b/Plugins/Android/DirichletMediation/src/main/java.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 93eb097a3680445f6a2f78d5eae1de63
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins/Android/DirichletMediation/src/main/java/com.meta b/Plugins/Android/DirichletMediation/src/main/java/com.meta
new file mode 100644
index 0000000..fcbf4a9
--- /dev/null
+++ b/Plugins/Android/DirichletMediation/src/main/java/com.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 32384e1dfd1c1422e92cc2a15056870c
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet.meta b/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet.meta
new file mode 100644
index 0000000..251c4a9
--- /dev/null
+++ b/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ef8ea0067be0345e292de883a7496086
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet/unity.meta b/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet/unity.meta
new file mode 100644
index 0000000..fedee16
--- /dev/null
+++ b/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet/unity.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 20ac9c2dde05f432da7fc53d70736c85
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet/unity/DirichletUnityBridge.java b/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet/unity/DirichletUnityBridge.java
new file mode 100644
index 0000000..b73d162
--- /dev/null
+++ b/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet/unity/DirichletUnityBridge.java
@@ -0,0 +1,1908 @@
+package com.dirichlet.unity;
+
+import android.app.Activity;
+import android.app.Application;
+import android.graphics.Color;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.FrameLayout;
+
+import com.tapsdk.tapad.group.AdNetworkType;
+import com.tapsdk.tapad.group.DirichletAdConfig;
+import com.tapsdk.tapad.group.DirichletAdManager;
+import com.tapsdk.tapad.group.DirichletAdNative;
+import com.tapsdk.tapad.group.DirichletAdRequest;
+import com.tapsdk.tapad.group.DirichletSdk;
+import com.tapsdk.tapad.group.ads.DirichletBannerAd;
+import com.tapsdk.tapad.group.ads.DirichletInterstitialAd;
+import com.tapsdk.tapad.group.ads.DirichletRewardVideoAd;
+import com.tapsdk.tapad.group.ads.DirichletSplashAd;
+import com.tapsdk.tapad.group.DirichletAdCustomController;
+import com.unity3d.player.UnityPlayer;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.UUID;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Collections;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Unity bridge for Dirichlet Mediation SDK.
+ *
+ * This bridge provides a Unity-compatible interface that closely mirrors the native SDK API.
+ * The API design follows the pattern:
+ * - DirichletAdManager.get().createAdNative(context) -> loadXXXAd() methods
+ * - Ad objects returned from loadXXXAd() -> show() and destroy() methods
+ *
+ * Usage pattern:
+ * 1. Initialize SDK: initialize(...)
+ * 2. Load ad: loadRewardVideoAd(...) / loadInterstitialAd(...) / etc.
+ * 3. Show ad: showAd(handle, options)
+ * 4. Destroy ad: destroyAd(handle)
+ *
+ * @see com.tapsdk.tapad.group.DirichletAdNative
+ * @see com.tapsdk.tapad.group.DirichletAdManager
+ */
+@SuppressWarnings("unused")
+public final class DirichletUnityBridge {
+
+ private static final String TAG = "DirichletUnityBridge";
+ private static final long INIT_TIMEOUT_MS = 5_000L;
+
+ private static final String UNITY_CALLBACK_OBJECT = "DirichletMediationEventReceiver";
+ private static final String UNITY_CALLBACK_METHOD = "OnNativeEvent";
+ private static final String EVENT_SHOW = "show";
+ private static final String EVENT_CLOSE = "close";
+ private static final String EVENT_CLICK = "click";
+ private static final String EVENT_REWARD = "reward";
+
+ private static final int TYPE_REWARD = 0;
+ private static final int TYPE_INTERSTITIAL = 1;
+ private static final int TYPE_BANNER = 2;
+ private static final int TYPE_SPLASH = 3;
+ private static final int TYPE_EXPRESS_FEED = 4;
+ private static final int TYPE_NATIVE_FEED = 5;
+
+ /**
+ * Cache of ad entries mapping handle IDs to ad instances.
+ * This allows Unity to reference Java objects via string handles.
+ */
+ private static final Map AD_CACHE = new ConcurrentHashMap<>();
+ private static final DirichletAdCustomController UNITY_CUSTOM_CONTROLLER = new UnityCustomController();
+
+ /**
+ * Singleton DirichletAdNative instance for auto-ad caching.
+ * Must be created once and reused to preserve the internal ad cache (rewardVideoAdMap).
+ * Used specifically by showRewardVideoAutoAd to maintain cache across calls.
+ */
+ private static volatile DirichletAdNative sAutoAdNative = null;
+ private static final Object sAutoAdNativeLock = new Object();
+
+ private static final Map REQUEST_BUILDER_METHODS = buildRequestMethodMap();
+
+ private DirichletUnityBridge() {
+ // Private constructor to prevent instantiation
+ }
+
+ /**
+ * Listener interface for ad load callbacks.
+ * Mirrors the native SDK listener pattern.
+ */
+ public interface LoadListener {
+ /**
+ * Called when ad load succeeds.
+ */
+ void onSuccess();
+
+ /**
+ * Called when ad load fails.
+ *
+ * @param code Error code
+ * @param message Error message
+ */
+ void onError(String code, String message);
+ }
+
+ public static boolean initialize(String appId,
+ String channel,
+ String subChannel,
+ boolean enableLog,
+ String mediaName,
+ String mediaKey,
+ String tapClientId,
+ String dataJson,
+ boolean shakeEnabled) {
+ // Never block the Android main thread (Unity often runs game loop on it).
+ // The synchronous boolean return is best-effort only.
+ if (Looper.getMainLooper() == Looper.myLooper()) {
+ Log.w(TAG, "initialize called on main thread; falling back to async init to avoid ANR");
+ initializeAsync(appId, channel, subChannel, enableLog, mediaName, mediaKey, tapClientId, dataJson, shakeEnabled, null);
+ return true;
+ }
+
+ return performInitialization(appId, channel, subChannel, enableLog, mediaName, mediaKey, tapClientId, dataJson, shakeEnabled);
+ }
+
+ /**
+ * Asynchronous initialization. This is the preferred entry point for Unity C# to avoid blocking the game loop.
+ * It reports completion via the provided listener. A timeout is enforced so Unity always receives a result.
+ */
+ public static void initializeAsync(String appId,
+ String channel,
+ String subChannel,
+ boolean enableLog,
+ String mediaName,
+ String mediaKey,
+ String tapClientId,
+ String dataJson,
+ boolean shakeEnabled,
+ LoadListener listener) {
+ final Activity activity = UnityPlayer.currentActivity;
+ if (activity == null) {
+ Log.e(TAG, "initializeAsync: Unity activity is null");
+ if (listener != null) {
+ listener.onError("activity_null", "Unity activity is null");
+ }
+ return;
+ }
+
+ final Application application = activity.getApplication();
+ final AtomicBoolean completed = new AtomicBoolean(false);
+ final Handler handler = new Handler(Looper.getMainLooper());
+
+ final Runnable timeoutTask = () -> {
+ if (listener == null) {
+ return;
+ }
+ if (completed.compareAndSet(false, true)) {
+ Log.w(TAG, "initializeAsync timed out waiting for callback");
+ listener.onError("timeout", "initialize timed out");
+ }
+ };
+ handler.postDelayed(timeoutTask, INIT_TIMEOUT_MS);
+
+ activity.runOnUiThread(() -> {
+ try {
+ long mediaId = safeParseLong(appId, 0L);
+ String dataPayload = mergeDataPayload(subChannel, dataJson);
+ DirichletAdConfig config = buildConfig(mediaId, channel, enableLog, mediaName, mediaKey, tapClientId, dataPayload, shakeEnabled);
+
+ DirichletSdk.InitListener sdkListener = new DirichletSdk.InitListener() {
+ @Override
+ public void onInitSuccess() {
+ if (listener == null) {
+ return;
+ }
+ if (completed.compareAndSet(false, true)) {
+ handler.removeCallbacks(timeoutTask);
+ listener.onSuccess();
+ }
+ }
+
+ @Override
+ public void onInitFail(int code, String msg) {
+ if (listener == null) {
+ return;
+ }
+ if (completed.compareAndSet(false, true)) {
+ handler.removeCallbacks(timeoutTask);
+ listener.onError(String.valueOf(code), msg);
+ }
+ }
+ };
+
+ DirichletSdk.init(application, config, sdkListener);
+ } catch (Throwable t) {
+ Log.e(TAG, "initializeAsync error", t);
+ if (listener != null && completed.compareAndSet(false, true)) {
+ handler.removeCallbacks(timeoutTask);
+ listener.onError("exception", t.getMessage() != null ? t.getMessage() : t.getClass().getSimpleName());
+ }
+ }
+ });
+ }
+
+ private static boolean performInitialization(String appId,
+ String channel,
+ String subChannel,
+ boolean enableLog,
+ String mediaName,
+ String mediaKey,
+ String tapClientId,
+ String dataJson,
+ boolean shakeEnabled) {
+ final Activity activity = UnityPlayer.currentActivity;
+ if (activity == null) {
+ Log.e(TAG, "initialize: Unity activity is null");
+ return false;
+ }
+
+ final Application application = activity.getApplication();
+ final CountDownLatch latch = new CountDownLatch(1);
+ final AtomicBoolean success = new AtomicBoolean(false);
+ final AtomicReference failureMessage = new AtomicReference<>();
+
+ activity.runOnUiThread(() -> {
+ try {
+ long mediaId = safeParseLong(appId, 0L);
+ String dataPayload = mergeDataPayload(subChannel, dataJson);
+ DirichletAdConfig config = buildConfig(mediaId, channel, enableLog, mediaName, mediaKey, tapClientId, dataPayload, shakeEnabled);
+
+ DirichletSdk.InitListener listener = new DirichletSdk.InitListener() {
+ @Override
+ public void onInitSuccess() {
+ success.set(true);
+ latch.countDown();
+ }
+
+ @Override
+ public void onInitFail(int code, String msg) {
+ failureMessage.set("code=" + code + ", msg=" + msg);
+ latch.countDown();
+ }
+ };
+
+ DirichletSdk.init(application, config, listener);
+ } catch (Throwable t) {
+ Log.e(TAG, "initialize error", t);
+ failureMessage.set(t.getMessage());
+ latch.countDown();
+ }
+ });
+
+ try {
+ if (!latch.await(INIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+ Log.w(TAG, "initialize timed out waiting for callback");
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+
+ if (!success.get() && failureMessage.get() != null) {
+ Log.e(TAG, "initialize failed: " + failureMessage.get());
+ }
+
+ return success.get();
+ }
+
+ private static DirichletAdConfig buildConfig(long mediaId,
+ String channel,
+ boolean enableLog,
+ String mediaName,
+ String mediaKey,
+ String tapClientId,
+ String dataPayload,
+ boolean shakeEnabled) {
+ DirichletAdConfig.Builder builder = new DirichletAdConfig.Builder()
+ .withMediaId(mediaId)
+ .enableDebug(enableLog)
+ .shakeEnabled(shakeEnabled)
+ .withCustomController(UNITY_CUSTOM_CONTROLLER);
+
+ if (!TextUtils.isEmpty(mediaName)) {
+ builder.withMediaName(mediaName);
+ }
+
+ if (!TextUtils.isEmpty(mediaKey)) {
+ builder.withMediaKey(mediaKey);
+ }
+
+ if (!TextUtils.isEmpty(channel)) {
+ builder.withGameChannel(channel);
+ }
+
+ if (!TextUtils.isEmpty(tapClientId)) {
+ builder.withTapClientId(tapClientId);
+ }
+
+ if (!TextUtils.isEmpty(dataPayload)) {
+ builder.withData(dataPayload);
+ }
+
+ return builder.build();
+ }
+
+ private static Integer toInteger(Object value) {
+ if (value instanceof Integer) {
+ return (Integer) value;
+ }
+ if (value instanceof Long) {
+ return ((Long) value).intValue();
+ }
+ if (value instanceof Double) {
+ return ((Double) value).intValue();
+ }
+ if (value instanceof String) {
+ try {
+ return Integer.parseInt(((String) value).trim());
+ } catch (NumberFormatException ignore) {
+ return null;
+ }
+ }
+ return null;
+ }
+
+ private static void applyGenericBuilderValue(DirichletAdRequest.Builder builder, String normalizedKey, Object value) {
+ if (builder == null || normalizedKey == null || normalizedKey.isEmpty()) {
+ return;
+ }
+
+ String methodName = REQUEST_BUILDER_METHODS.get(normalizedKey);
+ if (methodName == null) {
+ methodName = "with" + toPascalCase(normalizedKey);
+ }
+
+ if (methodName == null || methodName.isEmpty()) {
+ return;
+ }
+
+ if (!invokeBuilderWithSingleArg(builder, methodName, value)) {
+ if (!(value instanceof String)) {
+ invokeBuilderWithSingleArg(builder, methodName, String.valueOf(value));
+ }
+ }
+ }
+
+ private static boolean invokeBuilderWithSingleArg(Object target, String methodName, Object value) {
+ if (target == null || value == null || methodName == null || methodName.isEmpty()) {
+ return false;
+ }
+
+ if (value instanceof Integer) {
+ int intValue = (Integer) value;
+ if (tryInvoke(target, methodName, int.class, intValue)) return true;
+ if (tryInvoke(target, methodName, Integer.class, intValue)) return true;
+ if (tryInvoke(target, methodName, long.class, (long) intValue)) return true;
+ if (tryInvoke(target, methodName, Long.class, (long) intValue)) return true;
+ } else if (value instanceof Long) {
+ long longValue = (Long) value;
+ if (tryInvoke(target, methodName, long.class, longValue)) return true;
+ if (tryInvoke(target, methodName, Long.class, longValue)) return true;
+ if (tryInvoke(target, methodName, int.class, (int) longValue)) return true;
+ if (tryInvoke(target, methodName, Integer.class, (int) longValue)) return true;
+ } else if (value instanceof Double) {
+ double doubleValue = (Double) value;
+ if (tryInvoke(target, methodName, double.class, doubleValue)) return true;
+ if (tryInvoke(target, methodName, Double.class, doubleValue)) return true;
+ if (tryInvoke(target, methodName, float.class, (float) doubleValue)) return true;
+ if (tryInvoke(target, methodName, Float.class, (float) doubleValue)) return true;
+ } else if (value instanceof Boolean) {
+ boolean boolValue = (Boolean) value;
+ if (tryInvoke(target, methodName, boolean.class, boolValue)) return true;
+ if (tryInvoke(target, methodName, Boolean.class, boolValue)) return true;
+ } else if (value instanceof String) {
+ String stringValue = ((String) value).trim();
+ if (!stringValue.isEmpty()) {
+ try {
+ int parsed = Integer.parseInt(stringValue);
+ if (tryInvoke(target, methodName, int.class, parsed)) return true;
+ if (tryInvoke(target, methodName, Integer.class, parsed)) return true;
+ if (tryInvoke(target, methodName, long.class, (long) parsed)) return true;
+ if (tryInvoke(target, methodName, Long.class, (long) parsed)) return true;
+ } catch (NumberFormatException ignore) {
+ try {
+ long parsedLong = Long.parseLong(stringValue);
+ if (tryInvoke(target, methodName, long.class, parsedLong)) return true;
+ if (tryInvoke(target, methodName, Long.class, parsedLong)) return true;
+ } catch (NumberFormatException ignoredLong) {
+ try {
+ double parsedDouble = Double.parseDouble(stringValue);
+ if (tryInvoke(target, methodName, double.class, parsedDouble)) return true;
+ if (tryInvoke(target, methodName, Double.class, parsedDouble)) return true;
+ } catch (NumberFormatException ignoredDouble) {
+ // fall through to string invocation
+ }
+ }
+ }
+ if (tryInvoke(target, methodName, String.class, stringValue)) {
+ return true;
+ }
+ }
+ if (tryInvoke(target, methodName, String.class, stringValue)) {
+ return true;
+ }
+ return false;
+ }
+
+ return tryInvoke(target, methodName, String.class, value.toString());
+ }
+
+ private static void invokeBuilderTwoInts(Object target, String methodName, int first, int second) {
+ if (target == null || methodName == null || methodName.isEmpty()) {
+ return;
+ }
+
+ if (tryInvokeTwoArgs(target, methodName, int.class, int.class, first, second)) {
+ return;
+ }
+
+ tryInvokeTwoArgs(target, methodName, Integer.class, Integer.class, first, second);
+ }
+
+ private static boolean tryInvoke(Object target, String methodName, Class> paramType, Object argument) {
+ try {
+ Method method = target.getClass().getMethod(methodName, paramType);
+ method.setAccessible(true);
+ method.invoke(target, argument);
+ return true;
+ } catch (NoSuchMethodException ignored) {
+ return false;
+ } catch (Throwable t) {
+ Log.w(TAG, "Failed to invoke " + methodName + " with " + paramType + ": " + t.getMessage());
+ return false;
+ }
+ }
+
+ private static boolean tryInvokeTwoArgs(Object target, String methodName, Class> firstType, Class> secondType, Object arg1, Object arg2) {
+ try {
+ Method method = target.getClass().getMethod(methodName, firstType, secondType);
+ method.setAccessible(true);
+ method.invoke(target, arg1, arg2);
+ return true;
+ } catch (NoSuchMethodException ignored) {
+ return false;
+ } catch (Throwable t) {
+ Log.w(TAG, "Failed to invoke " + methodName + " with two args: " + t.getMessage());
+ return false;
+ }
+ }
+
+ private static String toPascalCase(String key) {
+ if (key == null || key.isEmpty()) {
+ return "";
+ }
+
+ StringBuilder builder = new StringBuilder(key.length());
+ boolean capitalizeNext = true;
+ for (int i = 0; i < key.length(); i++) {
+ char ch = key.charAt(i);
+ if (Character.isLetterOrDigit(ch)) {
+ if (capitalizeNext) {
+ builder.append(Character.toUpperCase(ch));
+ capitalizeNext = false;
+ } else {
+ builder.append(ch);
+ }
+ } else {
+ capitalizeNext = true;
+ }
+ }
+
+ return builder.toString();
+ }
+
+ private static JSONObject cloneJson(JSONObject value) {
+ if (value == null) {
+ return null;
+ }
+ try {
+ return new JSONObject(value.toString());
+ } catch (Exception ignored) {
+ return null;
+ }
+ }
+
+ private static void applyAdOptionSetters(AdEntry entry, JSONObject options) {
+ if (entry == null || options == null) {
+ return;
+ }
+
+ Object target = resolveAdObject(entry);
+ if (target == null) {
+ return;
+ }
+
+ Iterator keys = options.keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ if (key == null) {
+ continue;
+ }
+
+ String normalized = key.trim().toLowerCase(Locale.US);
+ if ("banner_baseline".equals(normalized) || "banner_offset".equals(normalized)) {
+ continue; // handled explicitly in show logic
+ }
+
+ Object value = options.opt(key);
+ if (value == null || JSONObject.NULL.equals(value)) {
+ continue;
+ }
+
+ String methodName = "set" + toPascalCase(normalized);
+ invokeBuilderWithSingleArg(target, methodName, value);
+ }
+ }
+
+ private static Object resolveAdObject(AdEntry entry) {
+ return entry != null ? entry.getAdObject() : null;
+ }
+
+ public static void requestPermissionIfNeeded() {
+ final Activity activity = UnityPlayer.currentActivity;
+ if (activity == null) {
+ Log.w(TAG, "requestPermissionIfNeeded: activity is null");
+ return;
+ }
+
+ activity.runOnUiThread(() -> DirichletAdManager.get().requestPermissionIfNecessary(activity));
+ }
+
+ public static String getSdkVersion() {
+ try {
+ return String.valueOf(DirichletSdk.getVersion());
+ } catch (Throwable t) {
+ Log.w(TAG, "getSdkVersion failed", t);
+ return "android-error";
+ }
+ }
+
+ /**
+ * Loads a reward video ad, matching the native SDK pattern.
+ * This follows DirichletAdNative.loadRewardVideoAd() from the native SDK.
+ *
+ * @param extras Request parameters as JSON (must include space_id)
+ * @param listener Callback for load success/failure
+ * @return Handle ID for the ad instance (can be used for show/destroy operations)
+ */
+ public static String loadRewardVideoAd(JSONObject extras, LoadListener listener) {
+ return loadAdInternal(TYPE_REWARD, extras, listener);
+ }
+
+ /**
+ * Loads an interstitial ad, matching the native SDK pattern.
+ * This follows DirichletAdNative.loadInterstitialAd() from the native SDK.
+ *
+ * @param extras Request parameters as JSON (must include space_id)
+ * @param listener Callback for load success/failure
+ * @return Handle ID for the ad instance (can be used for show/destroy operations)
+ */
+ public static String loadInterstitialAd(JSONObject extras, LoadListener listener) {
+ return loadAdInternal(TYPE_INTERSTITIAL, extras, listener);
+ }
+
+ /**
+ * Loads a banner ad, matching the native SDK pattern.
+ * This follows DirichletAdNative.loadBannerAd() from the native SDK.
+ *
+ * @param extras Request parameters as JSON (must include space_id)
+ * @param listener Callback for load success/failure
+ * @return Handle ID for the ad instance (can be used for show/destroy operations)
+ */
+ public static String loadBannerAd(JSONObject extras, LoadListener listener) {
+ return loadAdInternal(TYPE_BANNER, extras, listener);
+ }
+
+ /**
+ * Loads a splash ad, matching the native SDK pattern.
+ * This follows DirichletAdNative.loadSplashAd() from the native SDK.
+ *
+ * @param extras Request parameters as JSON (must include space_id)
+ * @param listener Callback for load success/failure
+ * @return Handle ID for the ad instance (can be used for show/destroy operations)
+ */
+ public static String loadSplashAd(JSONObject extras, LoadListener listener) {
+ return loadAdInternal(TYPE_SPLASH, extras, listener);
+ }
+
+ /**
+ * Listener interface for auto interstitial ad callbacks.
+ * Mirrors DirichletAdNative.InterstitialAutoAdListener from the native SDK.
+ */
+ public interface InterstitialAutoAdListener {
+ void onError(String code, String message);
+ void onAdShow();
+ void onAdClose();
+ void onAdClick();
+ }
+
+ /**
+ * Listener interface for auto banner ad callbacks.
+ * Mirrors DirichletAdNative.BannerAutoAdListener from the native SDK.
+ */
+ public interface BannerAutoAdListener {
+ void onError(String code, String message);
+ void onAdShow();
+ void onAdClose();
+ void onAdClick();
+ }
+
+ /**
+ * Listener interface for auto splash ad callbacks.
+ * Mirrors DirichletAdNative.SplashAutoAdListener from the native SDK.
+ */
+ public interface SplashAutoAdListener {
+ void onError(String code, String message);
+ void onAdShow();
+ void onAdClose();
+ void onAdClick();
+ }
+
+ /**
+ * Listener interface for auto reward video ad callbacks.
+ * This combines load and show callbacks into a single interface.
+ * Mirrors DirichletAdNative.RewardVideoAutoAdListener from the native SDK.
+ */
+ public interface RewardVideoAutoAdListener {
+ /**
+ * Called when ad fails to load or show.
+ *
+ * @param code Error code
+ * @param message Error message
+ */
+ void onError(String code, String message);
+
+ /**
+ * Called when ad is shown to the user.
+ */
+ void onAdShow();
+
+ /**
+ * Called when ad is closed.
+ */
+ void onAdClose();
+
+ /**
+ * Called when reward verification is completed.
+ *
+ * @param rewardVerify Whether the reward was verified
+ * @param rewardAmount Reward amount
+ * @param rewardName Reward name
+ * @param code Verification code
+ * @param msg Verification message
+ */
+ void onRewardVerify(boolean rewardVerify, int rewardAmount, String rewardName, int code, String msg);
+
+ /**
+ * Called when ad is clicked.
+ */
+ void onAdClick();
+ }
+
+ /**
+ * Shows a reward video ad with automatic load-and-show logic.
+ * This follows DirichletAdNative.showRewardVideoAutoAd() from the native SDK.
+ *
+ * The method will:
+ * 1. Show cached ad immediately if available and valid
+ * 2. Load a new ad in the background for next time
+ * 3. If no cached ad, wait for load and then show
+ *
+ * @param extras Request parameters as JSON (must include space_id)
+ * @param listener Callback for all ad events (load/show/close/reward/click/error)
+ */
+ public static void showRewardVideoAutoAd(JSONObject extras, RewardVideoAutoAdListener listener) {
+ Activity activity = UnityPlayer.currentActivity;
+ if (activity == null) {
+ if (listener != null) {
+ listener.onError("activity_null", "Unity activity is null");
+ }
+ return;
+ }
+
+ if (extras == null) {
+ if (listener != null) {
+ listener.onError("invalid_request", "Request extras cannot be null, must include space_id");
+ }
+ return;
+ }
+
+ long spaceId = extras.optLong("space_id", 0L);
+ if (spaceId <= 0) {
+ if (listener != null) {
+ listener.onError("invalid_space_id", "space_id must be provided and greater than zero in extras");
+ }
+ return;
+ }
+
+ activity.runOnUiThread(() -> {
+ try {
+ // Use singleton to preserve cache (rewardVideoAdMap) across calls
+ DirichletAdNative adNative = getOrCreateAutoAdNative(activity);
+ DirichletAdRequest.Builder builder = new DirichletAdRequest.Builder().withSpaceId(spaceId);
+
+ // Apply optional request parameters
+ String userId = extras.optString("user_id", null);
+ if (userId != null && !userId.isEmpty()) {
+ builder.withUserId(userId);
+ }
+
+ String rewardName = extras.optString("reward_name", null);
+ if (rewardName != null && !rewardName.isEmpty()) {
+ builder.withRewardName(rewardName);
+ }
+
+ int rewardAmount = extras.optInt("reward_amount", 0);
+ if (rewardAmount > 0) {
+ builder.withRewardAmount(rewardAmount);
+ }
+
+ String query = extras.optString("query", null);
+ if (query != null && !query.isEmpty()) {
+ builder.withQuery(query);
+ }
+
+ String extra1 = extras.optString("extra1", null);
+ if (extra1 != null && !extra1.isEmpty()) {
+ builder.withExtra1(extra1);
+ }
+
+ DirichletAdRequest request = builder.build();
+
+ // Create the native listener that bridges to Unity callbacks
+ DirichletAdNative.RewardVideoAutoAdListener nativeListener = new DirichletAdNative.RewardVideoAutoAdListener() {
+ @Override
+ public void onError(int code, String message) {
+ if (listener != null) {
+ listener.onError(String.valueOf(code), message);
+ }
+ }
+
+ @Override
+ public void onAdShow() {
+ if (listener != null) {
+ listener.onAdShow();
+ }
+ }
+
+ @Override
+ public void onAdClose() {
+ if (listener != null) {
+ listener.onAdClose();
+ }
+ }
+
+ @Override
+ public void onRewardVerify(boolean rewardVerify, int rewardAmount, String rewardName, int code, String msg) {
+ if (listener != null) {
+ listener.onRewardVerify(rewardVerify, rewardAmount, rewardName, code, msg);
+ }
+ }
+
+ @Override
+ public void onAdClick() {
+ if (listener != null) {
+ listener.onAdClick();
+ }
+ }
+ };
+
+ adNative.showRewardVideoAutoAd(request, activity, nativeListener);
+ } catch (Throwable t) {
+ Log.e(TAG, "showRewardVideoAutoAd exception", t);
+ if (listener != null) {
+ listener.onError("exception", t.getMessage() != null ? t.getMessage() : t.getClass().getSimpleName());
+ }
+ }
+ });
+ }
+
+ /**
+ * Shows an interstitial ad with automatic load-and-show logic.
+ * Mirrors DirichletAdNative.showInterstitialAutoAd() from the native SDK.
+ */
+ public static void showInterstitialAutoAd(JSONObject extras, InterstitialAutoAdListener listener) {
+ Activity activity = UnityPlayer.currentActivity;
+ if (activity == null) {
+ if (listener != null) {
+ listener.onError("activity_null", "Unity activity is null");
+ }
+ return;
+ }
+
+ if (!validateAutoAdSpaceId(extras, listener == null ? null : (code, message) -> listener.onError(code, message))) {
+ return;
+ }
+
+ activity.runOnUiThread(() -> {
+ try {
+ DirichletAdNative adNative = getOrCreateAutoAdNative(activity);
+ DirichletAdRequest request = buildAutoAdRequest(extras);
+
+ DirichletAdNative.InterstitialAutoAdListener nativeListener = new DirichletAdNative.InterstitialAutoAdListener() {
+ @Override
+ public void onError(int code, String message) {
+ if (listener != null) listener.onError(String.valueOf(code), message);
+ }
+ @Override public void onAdShow() { if (listener != null) listener.onAdShow(); }
+ @Override public void onAdClose() { if (listener != null) listener.onAdClose(); }
+ @Override public void onAdClick() { if (listener != null) listener.onAdClick(); }
+ };
+
+ adNative.showInterstitialAutoAd(request, activity, nativeListener);
+ } catch (Throwable t) {
+ Log.e(TAG, "showInterstitialAutoAd exception", t);
+ if (listener != null) {
+ listener.onError("exception", t.getMessage() != null ? t.getMessage() : t.getClass().getSimpleName());
+ }
+ }
+ });
+ }
+
+ /**
+ * Shows a banner ad with automatic load + rotation logic.
+ * Mirrors DirichletAdNative.showBannerAutoAd() from the native SDK.
+ * The container is created internally based on options (banner_baseline, banner_offset).
+ */
+ public static void showBannerAutoAd(JSONObject extras, JSONObject options, BannerAutoAdListener listener) {
+ Activity activity = UnityPlayer.currentActivity;
+ if (activity == null) {
+ if (listener != null) {
+ listener.onError("activity_null", "Unity activity is null");
+ }
+ return;
+ }
+
+ if (!validateAutoAdSpaceId(extras, listener == null ? null : (code, message) -> listener.onError(code, message))) {
+ return;
+ }
+
+ activity.runOnUiThread(() -> {
+ try {
+ DirichletAdNative adNative = getOrCreateAutoAdNative(activity);
+ DirichletAdRequest request = buildAutoAdRequest(extras);
+
+ int baseline = options != null ? options.optInt("banner_baseline", 1) : 1;
+ int offset = options != null ? options.optInt("banner_offset", 0) : 0;
+ FrameLayout container = attachBannerAutoContainer(activity, baseline, offset);
+ if (container == null) {
+ if (listener != null) listener.onError("attach_failed", "Failed to attach banner auto container");
+ return;
+ }
+
+ DirichletAdNative.BannerAutoAdListener nativeListener = new DirichletAdNative.BannerAutoAdListener() {
+ @Override
+ public void onError(int code, String message) {
+ if (listener != null) listener.onError(String.valueOf(code), message);
+ }
+ @Override public void onAdShow() { if (listener != null) listener.onAdShow(); }
+ @Override
+ public void onAdClose() {
+ detachBannerAutoContainer();
+ if (listener != null) listener.onAdClose();
+ }
+ @Override public void onAdClick() { if (listener != null) listener.onAdClick(); }
+ };
+
+ adNative.showBannerAutoAd(request, container, nativeListener);
+ } catch (Throwable t) {
+ Log.e(TAG, "showBannerAutoAd exception", t);
+ detachBannerAutoContainer();
+ if (listener != null) {
+ listener.onError("exception", t.getMessage() != null ? t.getMessage() : t.getClass().getSimpleName());
+ }
+ }
+ });
+ }
+
+ /**
+ * Shows a splash ad with automatic load logic.
+ * Mirrors DirichletAdNative.showSplashAutoAd() from the native SDK.
+ * The container is a fullscreen overlay created internally.
+ */
+ public static void showSplashAutoAd(JSONObject extras, JSONObject options, SplashAutoAdListener listener) {
+ Activity activity = UnityPlayer.currentActivity;
+ if (activity == null) {
+ if (listener != null) {
+ listener.onError("activity_null", "Unity activity is null");
+ }
+ return;
+ }
+
+ if (!validateAutoAdSpaceId(extras, listener == null ? null : (code, message) -> listener.onError(code, message))) {
+ return;
+ }
+
+ activity.runOnUiThread(() -> {
+ try {
+ DirichletAdNative adNative = getOrCreateAutoAdNative(activity);
+ DirichletAdRequest request = buildAutoAdRequest(extras);
+
+ FrameLayout container = attachSplashAutoContainer(activity);
+ if (container == null) {
+ if (listener != null) listener.onError("attach_failed", "Failed to attach splash auto container");
+ return;
+ }
+
+ DirichletAdNative.SplashAutoAdListener nativeListener = new DirichletAdNative.SplashAutoAdListener() {
+ @Override
+ public void onError(int code, String message) {
+ detachSplashAutoContainer();
+ if (listener != null) listener.onError(String.valueOf(code), message);
+ }
+ @Override public void onAdShow() { if (listener != null) listener.onAdShow(); }
+ @Override
+ public void onAdClose() {
+ detachSplashAutoContainer();
+ if (listener != null) listener.onAdClose();
+ }
+ @Override public void onAdClick() { if (listener != null) listener.onAdClick(); }
+ };
+
+ adNative.showSplashAutoAd(request, container, nativeListener);
+ } catch (Throwable t) {
+ Log.e(TAG, "showSplashAutoAd exception", t);
+ detachSplashAutoContainer();
+ if (listener != null) {
+ listener.onError("exception", t.getMessage() != null ? t.getMessage() : t.getClass().getSimpleName());
+ }
+ }
+ });
+ }
+
+ /**
+ * Preloads an ad for later auto-show. Mirrors DirichletAdNative.preLoad().
+ *
+ * @param extras Request parameters as JSON (must include space_id)
+ * @param type Ad type code; native SDK currently only handles type=3 (reward video)
+ */
+ public static void preLoad(JSONObject extras, int type) {
+ Activity activity = UnityPlayer.currentActivity;
+ if (activity == null) {
+ Log.w(TAG, "preLoad: activity null");
+ return;
+ }
+
+ if (extras == null) {
+ Log.w(TAG, "preLoad: extras null");
+ return;
+ }
+
+ long spaceId = extras.optLong("space_id", 0L);
+ if (spaceId <= 0) {
+ Log.w(TAG, "preLoad: invalid space_id");
+ return;
+ }
+
+ activity.runOnUiThread(() -> {
+ try {
+ DirichletAdNative adNative = getOrCreateAutoAdNative(activity);
+ DirichletAdRequest request = buildAutoAdRequest(extras);
+ adNative.preLoad(request, type);
+ } catch (Throwable t) {
+ Log.e(TAG, "preLoad exception", t);
+ }
+ });
+ }
+
+ /**
+ * Builds a DirichletAdRequest from JSON extras for auto-ad calls.
+ * Supports the same keys as buildRequest (banner uses slide_internal, splash uses express size).
+ */
+ private static DirichletAdRequest buildAutoAdRequest(JSONObject extras) {
+ long spaceId = extras.optLong("space_id", 0L);
+ DirichletAdRequest.Builder builder = new DirichletAdRequest.Builder().withSpaceId(spaceId);
+
+ Integer expressWidth = null;
+ Integer expressHeight = null;
+ Integer expressImageWidth = null;
+ Integer expressImageHeight = null;
+
+ Iterator keys = extras.keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ Object value = extras.opt(key);
+ if (value == null || JSONObject.NULL.equals(value)) {
+ continue;
+ }
+
+ String normalizedKey = key == null ? "" : key.trim().toLowerCase(Locale.US);
+ switch (normalizedKey) {
+ case "space_id":
+ continue;
+ case "express_width":
+ case "express_view_width":
+ expressWidth = toInteger(value);
+ break;
+ case "express_height":
+ case "express_view_height":
+ expressHeight = toInteger(value);
+ break;
+ case "express_image_width":
+ expressImageWidth = toInteger(value);
+ break;
+ case "express_image_height":
+ expressImageHeight = toInteger(value);
+ break;
+ default:
+ applyGenericBuilderValue(builder, normalizedKey, value);
+ break;
+ }
+ }
+
+ if (expressWidth != null || expressHeight != null) {
+ int width = expressWidth != null ? expressWidth : -1;
+ int height = expressHeight != null ? expressHeight : -1;
+ invokeBuilderTwoInts(builder, "withExpressViewAcceptedSize", width, height);
+ }
+
+ if (expressImageWidth != null || expressImageHeight != null) {
+ int width = expressImageWidth != null ? expressImageWidth : -1;
+ int height = expressImageHeight != null ? expressImageHeight : -1;
+ invokeBuilderTwoInts(builder, "withExpressImageAcceptedSize", width, height);
+ }
+
+ return builder.build();
+ }
+
+ private interface AutoErrorReporter {
+ void report(String code, String message);
+ }
+
+ private static boolean validateAutoAdSpaceId(JSONObject extras, AutoErrorReporter reporter) {
+ if (extras == null) {
+ if (reporter != null) reporter.report("invalid_request", "Request extras cannot be null, must include space_id");
+ return false;
+ }
+ long spaceId = extras.optLong("space_id", 0L);
+ if (spaceId <= 0) {
+ if (reporter != null) reporter.report("invalid_space_id", "space_id must be provided and greater than zero in extras");
+ return false;
+ }
+ return true;
+ }
+
+ private static volatile FrameLayout sBannerAutoContainer = null;
+ private static volatile FrameLayout sSplashAutoContainer = null;
+
+ private static FrameLayout attachBannerAutoContainer(Activity activity, int baseline, int offset) {
+ if (activity == null) return null;
+
+ ViewGroup root = activity.findViewById(android.R.id.content);
+ if (root == null) return null;
+
+ detachBannerAutoContainer();
+
+ FrameLayout container = new FrameLayout(activity);
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT);
+ params.gravity = baseline == 0 ? (Gravity.TOP | Gravity.CENTER_HORIZONTAL) : (Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
+ if (baseline == 0) {
+ params.topMargin = Math.max(0, offset);
+ } else {
+ params.bottomMargin = Math.max(0, offset);
+ }
+ container.setLayoutParams(params);
+ container.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+
+ root.addView(container);
+ sBannerAutoContainer = container;
+ return container;
+ }
+
+ private static void detachBannerAutoContainer() {
+ FrameLayout container = sBannerAutoContainer;
+ if (container == null) return;
+ sBannerAutoContainer = null;
+ ViewParent parent = container.getParent();
+ if (parent instanceof ViewGroup) {
+ ((ViewGroup) parent).removeView(container);
+ }
+ }
+
+ private static FrameLayout attachSplashAutoContainer(Activity activity) {
+ if (activity == null) return null;
+
+ ViewGroup root = activity.findViewById(android.R.id.content);
+ if (root == null) return null;
+
+ detachSplashAutoContainer();
+
+ FrameLayout overlay = new FrameLayout(activity);
+ overlay.setLayoutParams(new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.MATCH_PARENT));
+ overlay.setClickable(true);
+ overlay.setFocusable(true);
+ overlay.setBackgroundColor(Color.BLACK);
+ overlay.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+
+ root.addView(overlay);
+ sSplashAutoContainer = overlay;
+ return overlay;
+ }
+
+ private static void detachSplashAutoContainer() {
+ FrameLayout container = sSplashAutoContainer;
+ if (container == null) return;
+ sSplashAutoContainer = null;
+ ViewParent parent = container.getParent();
+ if (parent instanceof ViewGroup) {
+ ((ViewGroup) parent).removeView(container);
+ }
+ }
+
+ /**
+ * Gets or creates a singleton DirichletAdNative instance for auto-ad caching.
+ * The cache (rewardVideoAdMap) is stored as an instance variable in DirichletAdNativeImpl,
+ * so we must reuse the same instance to preserve cached ads across showRewardVideoAutoAd calls.
+ *
+ * @param activity The current activity context
+ * @return The singleton DirichletAdNative instance
+ */
+ private static DirichletAdNative getOrCreateAutoAdNative(Activity activity) {
+ if (sAutoAdNative == null) {
+ synchronized (sAutoAdNativeLock) {
+ if (sAutoAdNative == null) {
+ sAutoAdNative = DirichletAdManager.get().createAdNative(activity);
+ Log.d(TAG, "Created singleton DirichletAdNative for auto-ad caching");
+ }
+ }
+ }
+ return sAutoAdNative;
+ }
+
+ /**
+ * Internal method to load an ad of the specified type.
+ * Creates an AdEntry, stores it in cache, and initiates the load process.
+ */
+ private static String loadAdInternal(int adType, JSONObject extras, LoadListener listener) {
+ String handle = UUID.randomUUID().toString();
+ AdEntry entry = new AdEntry(handle, adType);
+ // Only add to cache after validation passes
+ // If loadAdWithEntry fails early, entry won't be in cache
+ loadAdWithEntry(entry, extras, listener);
+ return handle;
+ }
+
+ /**
+ * Internal method to perform the actual ad loading using DirichletAdNative.
+ * This matches the native SDK pattern: DirichletAdManager.get().createAdNative(context)
+ */
+ private static void loadAdWithEntry(AdEntry entry, JSONObject extras, LoadListener listener) {
+ Activity activity = UnityPlayer.currentActivity;
+ if (activity == null) {
+ if (listener != null) {
+ listener.onError("activity_null", "Unity activity is null");
+ }
+ return;
+ }
+
+ // Validate extras before adding to cache
+ try {
+ if (extras == null) {
+ if (listener != null) {
+ listener.onError("invalid_request", "Request extras cannot be null, must include space_id");
+ }
+ return;
+ }
+
+ long spaceId = extras.optLong("space_id", 0L);
+ if (spaceId <= 0) {
+ if (listener != null) {
+ listener.onError("invalid_space_id", "space_id must be provided and greater than zero in extras");
+ }
+ return;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "loadAd validation error", e);
+ if (listener != null) {
+ listener.onError("invalid_request", "Failed to validate request: " + e.getMessage());
+ }
+ return;
+ }
+
+ // Only add to cache after validation passes
+ AD_CACHE.put(entry.handle, entry);
+
+ activity.runOnUiThread(() -> {
+ try {
+ // Create AdNative instance, matching DirichletAdManager.get().createAdNative(context)
+ DirichletAdNative adNative = DirichletAdManager.get().createAdNative(activity);
+ DirichletAdRequest request = buildRequest(entry, extras);
+
+ switch (entry.type) {
+ case TYPE_REWARD:
+ adNative.loadRewardVideoAd(request, new DirichletAdNative.RewardVideoAdListener() {
+ @Override
+ public void onRewardVideoAdLoad(DirichletRewardVideoAd rewardVideoAd) {
+ entry.rewardAd = rewardVideoAd;
+ attachRewardInteraction(entry);
+ if (listener != null) {
+ listener.onSuccess();
+ }
+ }
+
+ @Override
+ public void onError(int code, String message) {
+ if (listener != null) {
+ listener.onError(String.valueOf(code), message);
+ }
+ }
+ });
+ break;
+ case TYPE_INTERSTITIAL:
+ adNative.loadInterstitialAd(request, new DirichletAdNative.InterstitialAdListener() {
+ @Override
+ public void onInterstitialAdLoad(DirichletInterstitialAd interstitialAd) {
+ entry.interstitialAd = interstitialAd;
+ attachInterstitialInteraction(entry);
+ if (listener != null) {
+ listener.onSuccess();
+ }
+ }
+
+ @Override
+ public void onError(int code, String message) {
+ if (listener != null) {
+ listener.onError(String.valueOf(code), message);
+ }
+ }
+ });
+ break;
+ case TYPE_BANNER:
+ adNative.loadBannerAd(request, new DirichletAdNative.BannerAdListener() {
+ @Override
+ public void onBannerAdLoad(DirichletBannerAd bannerAd) {
+ entry.bannerAd = bannerAd;
+ attachBannerInteraction(entry);
+ if (listener != null) {
+ listener.onSuccess();
+ }
+ }
+
+ @Override
+ public void onError(int code, String message) {
+ if (listener != null) {
+ listener.onError(String.valueOf(code), message);
+ }
+ }
+ });
+ break;
+ case TYPE_SPLASH:
+ adNative.loadSplashAd(request, new DirichletAdNative.SplashAdListener() {
+ @Override
+ public void onSplashAdLoad(DirichletSplashAd splashAd) {
+ entry.splashAd = splashAd;
+ attachSplashInteraction(entry);
+ if (listener != null) {
+ listener.onSuccess();
+ }
+ }
+
+ @Override
+ public void onError(int code, String message) {
+ if (listener != null) {
+ listener.onError(String.valueOf(code), message);
+ }
+ }
+ });
+ break;
+ default:
+ if (listener != null) {
+ listener.onError("unsupported_type", "Unsupported ad type: " + entry.type);
+ }
+ break;
+ }
+ } catch (IllegalArgumentException e) {
+ // Remove entry from cache on validation failure
+ AD_CACHE.remove(entry.handle);
+ Log.e(TAG, "loadAd validation failed", e);
+ if (listener != null) {
+ listener.onError("invalid_request", e.getMessage());
+ }
+ } catch (Throwable t) {
+ // Remove entry from cache on other exceptions
+ AD_CACHE.remove(entry.handle);
+ Log.e(TAG, "loadAd exception", t);
+ if (listener != null) {
+ String errorCode = t instanceof IllegalArgumentException ? "invalid_request" : "exception";
+ listener.onError(errorCode, t.getMessage() != null ? t.getMessage() : t.getClass().getSimpleName());
+ }
+ }
+ });
+ }
+
+ /**
+ * Shows an ad using the handle returned from loadRewardVideoAd/loadInterstitialAd/etc.
+ * This matches the native SDK pattern where ad objects have show() methods.
+ *
+ * @param handle The handle ID returned from the load method
+ * @param options Show options (e.g., banner alignment, offset)
+ * @return true if the ad was shown successfully, false otherwise
+ */
+ public static boolean showAd(String handle, JSONObject options) {
+ AdEntry entry = AD_CACHE.get(handle);
+ if (entry == null) {
+ Log.w(TAG, "showAd: handle not found " + handle);
+ return false;
+ }
+
+ Activity activity = UnityPlayer.currentActivity;
+ if (activity == null) {
+ Log.w(TAG, "showAd: activity null");
+ return false;
+ }
+
+ CountDownLatch latch = new CountDownLatch(1);
+ AtomicBoolean result = new AtomicBoolean(false);
+
+ activity.runOnUiThread(() -> {
+ try {
+ JSONObject localOptions = cloneJson(options);
+ applyAdOptionSetters(entry, localOptions);
+
+ switch (entry.type) {
+ case TYPE_REWARD:
+ if (entry.rewardAd != null) {
+ entry.rewardAd.showRewardVideoAd(activity);
+ result.set(true);
+ }
+ break;
+ case TYPE_INTERSTITIAL:
+ if (entry.interstitialAd != null) {
+ entry.interstitialAd.show(activity);
+ result.set(true);
+ }
+ break;
+ case TYPE_BANNER:
+ if (entry.bannerAd != null) {
+ int baseline = localOptions != null ? localOptions.optInt("banner_baseline", 1) : 1;
+ int offset = localOptions != null ? localOptions.optInt("banner_offset", 0) : 0;
+ entry.bannerAd.show(activity, baseline, offset);
+ // Hardware acceleration will be enabled in onAdShow callback
+ // when the view is actually added to the hierarchy
+ result.set(true);
+ }
+ break;
+ case TYPE_SPLASH:
+ if (entry.splashAd != null) {
+ FrameLayout container = attachSplashContainer(activity, entry);
+ if (container != null) {
+ attachSplashListener(activity, entry);
+ entry.splashAd.show(container);
+ result.set(true);
+ } else {
+ Log.w(TAG, "showAd: failed to attach splash container");
+ }
+ }
+ break;
+ default:
+ Log.w(TAG, "showAd: unsupported type " + entry.type);
+ break;
+ }
+ } catch (Throwable t) {
+ Log.e(TAG, "showAd exception", t);
+ detachSplashContainer(entry);
+ } finally {
+ latch.countDown();
+ }
+ });
+
+ try {
+ latch.await(1, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+
+ return result.get();
+ }
+
+ private static FrameLayout attachSplashContainer(Activity activity, AdEntry entry) {
+ if (activity == null || entry == null) {
+ return null;
+ }
+
+ ViewGroup root = activity.findViewById(android.R.id.content);
+ if (root == null) {
+ Log.w(TAG, "attachSplashContainer: root content view is null");
+ return null;
+ }
+
+ detachSplashContainer(entry);
+
+ FrameLayout overlay = new FrameLayout(activity);
+ FrameLayout.LayoutParams overlayParams = new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.MATCH_PARENT);
+ overlay.setLayoutParams(overlayParams);
+ overlay.setClickable(true);
+ overlay.setFocusable(true);
+ overlay.setBackgroundColor(Color.BLACK);
+ // Enable hardware acceleration layer for better video playback and rounded corners rendering
+ overlay.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+
+ int width = entry.splashWidth > 0 ? entry.splashWidth : FrameLayout.LayoutParams.MATCH_PARENT;
+ int height = entry.splashHeight > 0 ? entry.splashHeight : FrameLayout.LayoutParams.MATCH_PARENT;
+ FrameLayout.LayoutParams slotParams = new FrameLayout.LayoutParams(width, height);
+ slotParams.gravity = Gravity.CENTER;
+
+ FrameLayout slot = new FrameLayout(activity);
+ slot.setLayoutParams(slotParams);
+ // Enable hardware acceleration layer for ad content container
+ slot.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+
+ overlay.addView(slot);
+ root.addView(overlay);
+ entry.splashContainer = overlay;
+ return slot;
+ }
+
+ private static void attachSplashListener(Activity activity, AdEntry entry) {
+ if (entry == null || entry.splashAd == null) {
+ return;
+ }
+
+ entry.splashAd.setSplashInteractionListener(new DirichletSplashAd.AdInteractionListener() {
+ @Override
+ public void onAdClick() {
+ // no-op for Unity bridge; host app can listen via native SDK logs if needed
+ }
+
+ @Override
+ public void onAdShow() {
+ // no-op
+ }
+
+ @Override
+ public void onAdClose() {
+ if (activity != null) {
+ activity.runOnUiThread(() -> detachSplashContainer(entry));
+ } else {
+ detachSplashContainer(entry);
+ }
+ }
+ });
+ }
+
+ private static void detachSplashContainer(AdEntry entry) {
+ if (entry == null) {
+ return;
+ }
+
+ FrameLayout container = entry.splashContainer;
+ if (container == null) {
+ return;
+ }
+
+ entry.splashContainer = null;
+ ViewParent parent = container.getParent();
+ if (parent instanceof ViewGroup) {
+ ((ViewGroup) parent).removeView(container);
+ }
+ }
+
+ /**
+ * Checks if an ad is still valid and can be shown.
+ * This should be called before show() to ensure the ad hasn't expired.
+ *
+ * @param handle The handle ID returned from the load method
+ * @return true if the ad is valid and can be shown, false otherwise
+ */
+ public static boolean isAdValid(String handle) {
+ AdEntry entry = AD_CACHE.get(handle);
+ if (entry == null) {
+ Log.w(TAG, "isAdValid: handle not found " + handle);
+ return false;
+ }
+
+ try {
+ switch (entry.type) {
+ case TYPE_REWARD:
+ return entry.rewardAd != null && entry.rewardAd.isValid();
+ case TYPE_INTERSTITIAL:
+ return entry.interstitialAd != null && entry.interstitialAd.isValid();
+ case TYPE_BANNER:
+ return entry.bannerAd != null;
+ case TYPE_SPLASH:
+ return entry.splashAd != null;
+ default:
+ return false;
+ }
+ } catch (Throwable t) {
+ Log.w(TAG, "isAdValid exception", t);
+ return false;
+ }
+ }
+
+ /**
+ * Destroys an ad instance and releases resources.
+ * This matches the native SDK pattern where ad objects have destroy() methods.
+ *
+ * @param handle The handle ID returned from the load method
+ */
+ public static void destroyAd(String handle) {
+ AdEntry entry = AD_CACHE.remove(handle);
+ if (entry == null) {
+ return;
+ }
+
+ Activity activity = UnityPlayer.currentActivity;
+ if (activity == null) {
+ entry.destroy();
+ return;
+ }
+
+ activity.runOnUiThread(entry::destroy);
+ }
+
+ /**
+ * Builds a DirichletAdRequest from JSON extras.
+ * Note: Validation should be done before calling this method.
+ * This method assumes extras is not null and contains valid space_id.
+ */
+ private static DirichletAdRequest buildRequest(AdEntry entry, JSONObject extras) {
+ // Validation already done in loadAdWithEntry, but add defensive check
+ if (extras == null) {
+ throw new IllegalArgumentException("extras cannot be null, must include space_id");
+ }
+
+ long spaceId = extras.optLong("space_id", 0L);
+ if (spaceId <= 0) {
+ throw new IllegalArgumentException("space_id must be provided and greater than zero in extras");
+ }
+
+ DirichletAdRequest.Builder builder = new DirichletAdRequest.Builder()
+ .withSpaceId(spaceId);
+
+ Integer expressWidth = null;
+ Integer expressHeight = null;
+ Integer expressImageWidth = null;
+ Integer expressImageHeight = null;
+
+ Iterator keys = extras.keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ Object value = extras.opt(key);
+ if (value == null || JSONObject.NULL.equals(value)) {
+ continue;
+ }
+
+ String normalizedKey = key == null ? "" : key.trim().toLowerCase(Locale.US);
+ switch (normalizedKey) {
+ case "space_id":
+ // Already set above, skip to avoid duplicate
+ continue;
+ case "express_width":
+ case "express_view_width":
+ expressWidth = toInteger(value);
+ break;
+ case "express_height":
+ case "express_view_height":
+ expressHeight = toInteger(value);
+ break;
+ case "express_image_width":
+ expressImageWidth = toInteger(value);
+ break;
+ case "express_image_height":
+ expressImageHeight = toInteger(value);
+ break;
+ default:
+ applyGenericBuilderValue(builder, normalizedKey, value);
+ break;
+ }
+ }
+
+ if (expressWidth != null || expressHeight != null) {
+ int width = expressWidth != null ? expressWidth : -1;
+ int height = expressHeight != null ? expressHeight : -1;
+ invokeBuilderTwoInts(builder, "withExpressViewAcceptedSize", width, height);
+ if (entry != null && entry.type == TYPE_SPLASH) {
+ entry.splashWidth = width;
+ entry.splashHeight = height;
+ }
+ }
+
+ if (expressImageWidth != null || expressImageHeight != null) {
+ int width = expressImageWidth != null ? expressImageWidth : -1;
+ int height = expressImageHeight != null ? expressImageHeight : -1;
+ invokeBuilderTwoInts(builder, "withExpressImageAcceptedSize", width, height);
+ }
+
+ return builder.build();
+ }
+
+ private static void attachRewardInteraction(final AdEntry entry) {
+ if (entry == null || entry.rewardAd == null) {
+ return;
+ }
+
+ entry.rewardAd.setRewardAdInteractionListener(new DirichletRewardVideoAd.RewardAdInteractionListener() {
+ @Override
+ public void onAdShow() {
+ emitEvent(entry.handle, EVENT_SHOW, resolveAdType(entry.type));
+ }
+
+ @Override
+ public void onAdClose() {
+ emitEvent(entry.handle, EVENT_CLOSE, resolveAdType(entry.type));
+ }
+
+ @Override
+ public void onRewardVerify(boolean rewardVerify, int rewardAmount, String rewardName, int code, String msg) {
+ JSONObject data = new JSONObject();
+ try {
+ data.put("rewardVerify", rewardVerify);
+ data.put("rewardAmount", rewardAmount);
+ data.put("rewardName", rewardName == null ? "" : rewardName);
+ data.put("code", code);
+ data.put("message", msg == null ? "" : msg);
+ } catch (JSONException e) {
+ Log.w(TAG, "Failed to build reward payload", e);
+ }
+ emitEvent(entry.handle, EVENT_REWARD, resolveAdType(entry.type), data);
+ }
+
+ @Override
+ public void onAdClick() {
+ emitEvent(entry.handle, EVENT_CLICK, resolveAdType(entry.type));
+ }
+ });
+ }
+
+ private static void attachInterstitialInteraction(final AdEntry entry) {
+ if (entry == null || entry.interstitialAd == null) {
+ return;
+ }
+
+ entry.interstitialAd.setInteractionListener(new DirichletInterstitialAd.InterstitialAdInteractionListener() {
+ @Override
+ public void onAdShow() {
+ emitEvent(entry.handle, EVENT_SHOW, resolveAdType(entry.type));
+ }
+
+ @Override
+ public void onAdClose() {
+ emitEvent(entry.handle, EVENT_CLOSE, resolveAdType(entry.type));
+ }
+
+ @Override
+ public void onAdClick() {
+ emitEvent(entry.handle, EVENT_CLICK, resolveAdType(entry.type));
+ }
+ });
+ }
+
+ private static void attachBannerInteraction(final AdEntry entry) {
+ if (entry == null || entry.bannerAd == null) {
+ return;
+ }
+
+ entry.bannerAd.setBannerInteractionListener(new DirichletBannerAd.BannerInteractionListener() {
+ @Override
+ public void onAdShow() {
+ emitEvent(entry.handle, EVENT_SHOW, resolveAdType(entry.type));
+ }
+
+ @Override
+ public void onAdClose() {
+ emitEvent(entry.handle, EVENT_CLOSE, resolveAdType(entry.type));
+ }
+
+ @Override
+ public void onAdClick() {
+ emitEvent(entry.handle, EVENT_CLICK, resolveAdType(entry.type));
+ }
+ });
+ }
+
+
+ private static void attachSplashInteraction(final AdEntry entry) {
+ if (entry == null || entry.splashAd == null) {
+ return;
+ }
+
+ entry.splashAd.setSplashInteractionListener(new DirichletSplashAd.AdInteractionListener() {
+ @Override
+ public void onAdClick() {
+ emitEvent(entry.handle, EVENT_CLICK, resolveAdType(entry.type));
+ }
+
+ @Override
+ public void onAdShow() {
+ emitEvent(entry.handle, EVENT_SHOW, resolveAdType(entry.type));
+ }
+
+ @Override
+ public void onAdClose() {
+ emitEvent(entry.handle, EVENT_CLOSE, resolveAdType(entry.type));
+ }
+ });
+ }
+
+ private static void emitEvent(String handle, String eventName, String adType) {
+ emitEvent(handle, eventName, adType, null);
+ }
+
+ private static void emitEvent(String handle, String eventName, String adType, JSONObject data) {
+ if (TextUtils.isEmpty(handle) || TextUtils.isEmpty(eventName)) {
+ return;
+ }
+
+ try {
+ JSONObject message = new JSONObject();
+ message.put("handle", handle);
+ message.put("eventName", eventName);
+ if (!TextUtils.isEmpty(adType)) {
+ message.put("adType", adType);
+ }
+ if (data != null && data.length() > 0) {
+ message.put("data", data);
+ }
+ UnityPlayer.UnitySendMessage(UNITY_CALLBACK_OBJECT, UNITY_CALLBACK_METHOD, message.toString());
+ } catch (Exception ex) {
+ Log.w(TAG, "Failed to emit unity event", ex);
+ }
+ }
+
+ private static String resolveAdType(int type) {
+ switch (type) {
+ case TYPE_REWARD:
+ return "reward";
+ case TYPE_INTERSTITIAL:
+ return "interstitial";
+ case TYPE_BANNER:
+ return "banner";
+ case TYPE_SPLASH:
+ return "splash";
+ case TYPE_EXPRESS_FEED:
+ return "express_feed";
+ case TYPE_NATIVE_FEED:
+ return "native_feed";
+ default:
+ return "unknown";
+ }
+ }
+
+ private static Map buildRequestMethodMap() {
+ Map map = new HashMap<>(16);
+ map.put("space_id", "withSpaceId");
+ map.put("extra1", "withExtra1");
+ map.put("user_id", "withUserId");
+ map.put("reward_name", "withRewardName");
+ map.put("reward_amount", "withRewardAmount");
+ map.put("query", "withQuery");
+ map.put("express_width", "withExpressViewAcceptedSize");
+ map.put("express_height", "withExpressViewAcceptedSize");
+ map.put("express_view_width", "withExpressViewAcceptedSize");
+ map.put("express_view_height", "withExpressViewAcceptedSize");
+ map.put("express_image_width", "withExpressImageAcceptedSize");
+ map.put("express_image_height", "withExpressImageAcceptedSize");
+ map.put("mina_id", "withMinaId");
+ map.put("slide_internal", "withSlideInternal");
+ return Collections.unmodifiableMap(map);
+ }
+
+ private static String mergeDataPayload(String subChannel, String dataJson) {
+ if (!TextUtils.isEmpty(dataJson)) {
+ return dataJson;
+ }
+
+ if (TextUtils.isEmpty(subChannel)) {
+ return null;
+ }
+
+ try {
+ JSONObject json = new JSONObject();
+ json.put("sub_channel", subChannel);
+ return json.toString();
+ } catch (JSONException e) {
+ Log.w(TAG, "mergeDataPayload error", e);
+ return null;
+ }
+ }
+
+ private static long safeParseLong(String value, long fallback) {
+ if (TextUtils.isEmpty(value)) {
+ return fallback;
+ }
+
+ try {
+ return Long.parseLong(value);
+ } catch (NumberFormatException ignore) {
+ return fallback;
+ }
+ }
+
+ /**
+ * Internal data structure to hold ad instances and associated state.
+ *
+ * Since Unity cannot directly hold Java objects, this class serves as a bridge
+ * to store ad instances returned from DirichletAdNative.loadXXXAd() methods.
+ * The handle ID is used as a key in AD_CACHE to map Unity's string reference
+ * to the actual Java ad object.
+ *
+ * This design is necessary because:
+ * 1. Unity can only pass strings between C# and Java
+ * 2. Different ad types have different interfaces (rewardAd.showRewardVideoAd() vs interstitialAd.show())
+ * 3. Splash ads require additional UI state (container, dimensions)
+ * 4. Event callbacks need handle and type information
+ */
+ private static final class AdEntry {
+ final String handle;
+ final int type;
+
+ // Ad object - only one will be set based on type
+ DirichletRewardVideoAd rewardAd;
+ DirichletInterstitialAd interstitialAd;
+ DirichletBannerAd bannerAd;
+ DirichletSplashAd splashAd;
+
+ // Splash-specific state
+ FrameLayout splashContainer;
+ int splashWidth;
+ int splashHeight;
+
+ AdEntry(String handle, int type) {
+ this.handle = handle;
+ this.type = type;
+ }
+
+ /**
+ * Gets the ad object based on type. Used for generic operations.
+ */
+ Object getAdObject() {
+ switch (type) {
+ case TYPE_REWARD:
+ return rewardAd;
+ case TYPE_INTERSTITIAL:
+ return interstitialAd;
+ case TYPE_BANNER:
+ return bannerAd;
+ case TYPE_SPLASH:
+ return splashAd;
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Destroys the ad instance and releases resources.
+ * This matches the native SDK pattern where ad objects have destroy() methods.
+ */
+ void destroy() {
+ try {
+ switch (type) {
+ case TYPE_REWARD:
+ if (rewardAd != null) {
+ rewardAd.destroy();
+ rewardAd = null;
+ }
+ break;
+ case TYPE_INTERSTITIAL:
+ if (interstitialAd != null) {
+ interstitialAd.destroy();
+ interstitialAd = null;
+ }
+ break;
+ case TYPE_BANNER:
+ if (bannerAd != null) {
+ bannerAd.destroy();
+ bannerAd = null;
+ }
+ break;
+ case TYPE_SPLASH:
+ if (splashAd != null) {
+ splashAd.setSplashInteractionListener(null);
+ splashAd.destroy();
+ splashAd = null;
+ }
+ detachSplashContainer(this);
+ break;
+ }
+ } catch (Throwable t) {
+ Log.w(TAG, "destroyAd exception", t);
+ }
+ }
+ }
+
+ private static final class UnityCustomController extends DirichletAdCustomController {
+ }
+}
diff --git a/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet/unity/DirichletUnityBridge.java.meta b/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet/unity/DirichletUnityBridge.java.meta
new file mode 100644
index 0000000..80f94bf
--- /dev/null
+++ b/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet/unity/DirichletUnityBridge.java.meta
@@ -0,0 +1,32 @@
+fileFormatVersion: 2
+guid: 14cb359fe1a3c478ca0969cf714aff16
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 0
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ - first:
+ Android: Android
+ second:
+ enabled: 1
+ settings: {}
+ - first:
+ Any:
+ second:
+ enabled: 0
+ settings: {}
+ - first:
+ Editor: Editor
+ second:
+ enabled: 0
+ settings:
+ DefaultValueInitialized: true
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins/Android/com.tencent.mm.opensdk.wechat-sdk-android-6.8.34.aar b/Plugins/Android/com.tencent.mm.opensdk.wechat-sdk-android-6.8.34.aar
new file mode 100644
index 0000000..cb42934
Binary files /dev/null and b/Plugins/Android/com.tencent.mm.opensdk.wechat-sdk-android-6.8.34.aar differ
diff --git a/Plugins/Android/com.tencent.mm.opensdk.wechat-sdk-android-6.8.34.aar.meta b/Plugins/Android/com.tencent.mm.opensdk.wechat-sdk-android-6.8.34.aar.meta
new file mode 100644
index 0000000..f39d9f8
--- /dev/null
+++ b/Plugins/Android/com.tencent.mm.opensdk.wechat-sdk-android-6.8.34.aar.meta
@@ -0,0 +1,34 @@
+fileFormatVersion: 2
+guid: 8745d7dbdbaeb7948ab964d52eafe76c
+labels:
+- gpsr
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 0
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ - first:
+ Android: Android
+ second:
+ enabled: 1
+ settings: {}
+ - first:
+ Any:
+ second:
+ enabled: 0
+ settings: {}
+ - first:
+ Editor: Editor
+ second:
+ enabled: 0
+ settings:
+ DefaultValueInitialized: true
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins/Android/libs.meta b/Plugins/Android/libs.meta
new file mode 100644
index 0000000..2ee9baa
--- /dev/null
+++ b/Plugins/Android/libs.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e7e8438cf7c294b92988c3830e288191
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins/Android/libs/GDTSDK.unionNormal.4.671.1541.aar b/Plugins/Android/libs/GDTSDK.unionNormal.4.671.1541.aar
new file mode 100644
index 0000000..68322db
Binary files /dev/null and b/Plugins/Android/libs/GDTSDK.unionNormal.4.671.1541.aar differ
diff --git a/Plugins/Android/libs/GDTSDK.unionNormal.4.671.1541.aar.meta b/Plugins/Android/libs/GDTSDK.unionNormal.4.671.1541.aar.meta
new file mode 100644
index 0000000..c0095bf
--- /dev/null
+++ b/Plugins/Android/libs/GDTSDK.unionNormal.4.671.1541.aar.meta
@@ -0,0 +1,33 @@
+fileFormatVersion: 2
+guid: 90b3b17e0788494398662e729c344ee2
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 0
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ - first:
+ Android: Android
+ second:
+ enabled: 1
+ settings: {}
+ - first:
+ Any:
+ second:
+ enabled: 0
+ settings: {}
+ - first:
+ Editor: Editor
+ second:
+ enabled: 0
+ settings:
+ DefaultValueInitialized: true
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+
diff --git a/Plugins/Android/libs/iadsdk-release-2.3.102.110.aar b/Plugins/Android/libs/iadsdk-release-2.3.102.110.aar
new file mode 100644
index 0000000..12d7b79
Binary files /dev/null and b/Plugins/Android/libs/iadsdk-release-2.3.102.110.aar differ
diff --git a/Plugins/Android/libs/iadsdk-release-2.3.102.110.aar.meta b/Plugins/Android/libs/iadsdk-release-2.3.102.110.aar.meta
new file mode 100644
index 0000000..06ccc36
--- /dev/null
+++ b/Plugins/Android/libs/iadsdk-release-2.3.102.110.aar.meta
@@ -0,0 +1,32 @@
+fileFormatVersion: 2
+guid: 92c7680ef2e44d279c23926510865f93
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 0
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ - first:
+ Android: Android
+ second:
+ enabled: 1
+ settings: {}
+ - first:
+ Any:
+ second:
+ enabled: 0
+ settings: {}
+ - first:
+ Editor: Editor
+ second:
+ enabled: 0
+ settings:
+ DefaultValueInitialized: true
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins/Android/libs/open_ad_sdk_7.4.2.2.aar b/Plugins/Android/libs/open_ad_sdk_7.4.2.2.aar
new file mode 100644
index 0000000..a98d09d
Binary files /dev/null and b/Plugins/Android/libs/open_ad_sdk_7.4.2.2.aar differ
diff --git a/Plugins/Android/libs/open_ad_sdk_7.4.2.2.aar.meta b/Plugins/Android/libs/open_ad_sdk_7.4.2.2.aar.meta
new file mode 100644
index 0000000..d3f502a
--- /dev/null
+++ b/Plugins/Android/libs/open_ad_sdk_7.4.2.2.aar.meta
@@ -0,0 +1,33 @@
+fileFormatVersion: 2
+guid: 930136242e574a2b89110e6a25e49065
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 0
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ - first:
+ Android: Android
+ second:
+ enabled: 1
+ settings: {}
+ - first:
+ Any:
+ second:
+ enabled: 0
+ settings: {}
+ - first:
+ Editor: Editor
+ second:
+ enabled: 0
+ settings:
+ DefaultValueInitialized: true
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+
diff --git a/Plugins/Android/proguard-user.txt b/Plugins/Android/proguard-user.txt
new file mode 100644
index 0000000..e69de29
diff --git a/Plugins/Android/proguard-user.txt.meta b/Plugins/Android/proguard-user.txt.meta
new file mode 100644
index 0000000..6be6b78
--- /dev/null
+++ b/Plugins/Android/proguard-user.txt.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: c51429155b4914ffe99f152a28d7d6d4
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins/iOS.meta b/Plugins/iOS.meta
new file mode 100644
index 0000000..f4183df
--- /dev/null
+++ b/Plugins/iOS.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 215b74292b87e41809a9e2c72050a616
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins/iOS/DirichletMediationUnityBridge.h b/Plugins/iOS/DirichletMediationUnityBridge.h
new file mode 100644
index 0000000..436f5b5
--- /dev/null
+++ b/Plugins/iOS/DirichletMediationUnityBridge.h
@@ -0,0 +1,80 @@
+//
+// DirichletMediationUnityBridge.h
+// Dirichlet Mediation Unity Bridge for iOS
+//
+// Created by Dirichlet Unity SDK
+// Copyright © 2025 Dirichlet Inc. All rights reserved.
+//
+
+#import
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/// Initialize the Dirichlet Mediation SDK
+/// @param mediaId Media ID string
+/// @param mediaKey Media key string
+/// @param enableLog Enable debug logging
+/// @param mediaName Media name string (optional)
+/// @param gameChannel Game channel string (optional)
+/// @param shakeEnabled Enable shake interaction (for GDT adapter)
+/// @param allowIDFAAccess Allow IDFA access (for TapADN and GDT adapters)
+/// @param aTags External aTags JSON string (optional)
+/// @return YES if initialization started successfully, NO otherwise
+bool DirichletMediationUnityBridge_Initialize(
+ const char* mediaId,
+ const char* mediaKey,
+ bool enableLog,
+ const char* mediaName,
+ const char* gameChannel,
+ bool shakeEnabled,
+ bool allowIDFAAccess,
+ const char* aTags
+);
+
+/// Request permissions if necessary (ATT for iOS 14+)
+void DirichletMediationUnityBridge_RequestPermissionIfNeeded(void);
+
+/// Get SDK version
+/// @return SDK version string (caller should NOT free this pointer)
+const char* DirichletMediationUnityBridge_GetSdkVersion(void);
+
+/// Load reward video ad
+/// @param spaceId Space/slot ID
+/// @param extras JSON string with additional parameters
+/// @return Handle ID for the ad instance (caller should copy/free)
+const char* DirichletMediationUnityBridge_LoadRewardVideoAd(long long spaceId, const char* extras);
+
+/// Load interstitial ad
+/// @param spaceId Space/slot ID
+/// @param extras JSON string with additional parameters
+/// @return Handle ID for the ad instance
+const char* DirichletMediationUnityBridge_LoadInterstitialAd(long long spaceId, const char* extras);
+
+/// Load banner ad
+/// @param spaceId Space/slot ID
+/// @param extras JSON string with additional parameters
+/// @return Handle ID for the ad instance
+const char* DirichletMediationUnityBridge_LoadBannerAd(long long spaceId, const char* extras);
+
+/// Load splash ad
+/// @param spaceId Space/slot ID
+/// @param extras JSON string with additional parameters
+/// @return Handle ID for the ad instance
+const char* DirichletMediationUnityBridge_LoadSplashAd(long long spaceId, const char* extras);
+
+/// Show ad by handle
+/// @param handleId Handle ID returned from load methods
+/// @param extras JSON string with show options (e.g., banner alignment)
+/// @return YES if show started successfully, NO otherwise
+bool DirichletMediationUnityBridge_ShowAd(const char* handleId, const char* extras);
+
+/// Destroy ad by handle
+/// @param handleId Handle ID returned from load methods
+void DirichletMediationUnityBridge_DestroyAd(const char* handleId);
+
+#ifdef __cplusplus
+}
+#endif
+
diff --git a/Plugins/iOS/DirichletMediationUnityBridge.h.meta b/Plugins/iOS/DirichletMediationUnityBridge.h.meta
new file mode 100644
index 0000000..c27c09d
--- /dev/null
+++ b/Plugins/iOS/DirichletMediationUnityBridge.h.meta
@@ -0,0 +1,33 @@
+fileFormatVersion: 2
+guid: 07aa84d4e2aa748719c5e30912f2cf53
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 0
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ - first:
+ Any:
+ second:
+ enabled: 0
+ settings: {}
+ - first:
+ Editor: Editor
+ second:
+ enabled: 0
+ settings:
+ DefaultValueInitialized: true
+ - first:
+ iPhone: iOS
+ second:
+ enabled: 1
+ settings:
+ AddToEmbeddedBinaries: false
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Plugins/iOS/DirichletMediationUnityBridge.mm b/Plugins/iOS/DirichletMediationUnityBridge.mm
new file mode 100644
index 0000000..381171e
--- /dev/null
+++ b/Plugins/iOS/DirichletMediationUnityBridge.mm
@@ -0,0 +1,682 @@
+//
+// DirichletMediationUnityBridge.mm
+// Dirichlet Mediation Unity Bridge for iOS
+//
+//
+// Created by Dirichlet Unity SDK
+// Copyright © 2025 Dirichlet Inc. All rights reserved.
+//
+
+#import "DirichletMediationUnityBridge.h"
+#import
+#import
+
+// Unity callback interface
+extern "C" void UnitySendMessage(const char* obj, const char* method, const char* msg);
+
+// Constants
+static NSString * const kUnityCallbackObject = @"DirichletMediationEventReceiver";
+static NSString * const kUnityCallbackMethod = @"OnNativeEvent";
+static NSString * const kUnityLoadCallbackObject = @"DirichletMediationIOSLoadCallbackReceiver";
+static NSString * const kUnityLoadCallbackMethod = @"OnLoadCallback";
+static NSString * const kUnityInitCallbackObject = @"DirichletMediationIOSInitCallbackReceiver";
+static NSString * const kUnityInitCallbackMethod = @"OnInitCallback";
+
+// Helper to convert C string to NSString
+static NSString* CreateNSString(const char* cString) {
+ return cString ? [NSString stringWithUTF8String:cString] : @"";
+}
+
+// Helper to convert NSString to C string (caller must free)
+static char* MakeCString(NSString* nsString) {
+ if (nsString == nil) {
+ return NULL;
+ }
+ const char* utf8String = [nsString UTF8String];
+ char* cString = (char*)malloc(strlen(utf8String) + 1);
+ strcpy(cString, utf8String);
+ return cString;
+}
+
+// Helper to send event to Unity
+static void SendEventToUnity(NSString* handleId, NSString* eventName, NSString* adType, NSDictionary* data) {
+ NSMutableDictionary* payload = [NSMutableDictionary dictionary];
+ payload[@"handle"] = handleId ?: @"";
+ payload[@"eventName"] = eventName ?: @"";
+ payload[@"adType"] = adType ?: @"";
+ if (data) {
+ payload[@"data"] = data;
+ }
+
+ NSError* error = nil;
+ NSData* jsonData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&error];
+ if (jsonData && !error) {
+ NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
+ UnitySendMessage([kUnityCallbackObject UTF8String], [kUnityCallbackMethod UTF8String], [jsonString UTF8String]);
+ }
+}
+
+// Helper to send load callback to Unity (separate from ad events)
+static void SendLoadCallbackToUnity(NSString* handleId, NSString* eventName, NSString* adType, NSDictionary* data) {
+ NSMutableDictionary* payload = [NSMutableDictionary dictionary];
+ payload[@"handle"] = handleId ?: @"";
+ payload[@"eventName"] = eventName ?: @"";
+ payload[@"adType"] = adType ?: @"";
+ if (data) {
+ payload[@"data"] = data;
+ }
+
+ NSError* error = nil;
+ NSData* jsonData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&error];
+ if (jsonData && !error) {
+ NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
+ UnitySendMessage([kUnityLoadCallbackObject UTF8String], [kUnityLoadCallbackMethod UTF8String], [jsonString UTF8String]);
+ }
+}
+
+// Helper to send init callback to Unity (async initialization result)
+static void SendInitCallbackToUnity(BOOL success, NSError* error, NSString* extraMessage) {
+ NSMutableDictionary* payload = [NSMutableDictionary dictionary];
+ payload[@"success"] = @(success);
+
+ NSMutableDictionary* data = [NSMutableDictionary dictionary];
+ if (error) {
+ data[@"code"] = @(error.code);
+ data[@"message"] = error.localizedDescription ?: @"";
+ if (error.domain) {
+ data[@"domain"] = error.domain;
+ }
+ } else if (extraMessage.length > 0) {
+ data[@"message"] = extraMessage;
+ }
+
+ if (data.count > 0) {
+ payload[@"data"] = data;
+ }
+
+ NSError* jsonError = nil;
+ NSData* jsonData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&jsonError];
+ if (jsonData && !jsonError) {
+ NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
+ UnitySendMessage([kUnityInitCallbackObject UTF8String], [kUnityInitCallbackMethod UTF8String], [jsonString UTF8String]);
+ }
+}
+
+// Parse JSON extras to dictionary
+static NSDictionary* ParseExtras(const char* extrasJson) {
+ if (!extrasJson || strlen(extrasJson) == 0) {
+ return nil;
+ }
+
+ NSString* jsonString = CreateNSString(extrasJson);
+ NSData* jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
+ if (!jsonData) {
+ return nil;
+ }
+
+ NSError* error = nil;
+ NSDictionary* dict = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
+ return error ? nil : dict;
+}
+
+#pragma mark - Ad Instance Manager
+
+@interface DirichletMediationInstanceManager : NSObject
+
+@property (nonatomic, strong) NSMutableDictionary* adInstances;
+@property (nonatomic, strong) dispatch_queue_t syncQueue;
+
++ (instancetype)shared;
+- (void)storeAd:(id)ad forHandle:(NSString*)handleId;
+- (id)adForHandle:(NSString*)handleId;
+- (void)removeAdForHandle:(NSString*)handleId;
+- (NSString*)generateHandle;
+
+@end
+
+@implementation DirichletMediationInstanceManager
+
++ (instancetype)shared {
+ static DirichletMediationInstanceManager* instance = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ instance = [[DirichletMediationInstanceManager alloc] init];
+ });
+ return instance;
+}
+
+- (instancetype)init {
+ if (self = [super init]) {
+ _adInstances = [NSMutableDictionary dictionary];
+ _syncQueue = dispatch_queue_create("com.dirichlet.mediation.unity.admanager", DISPATCH_QUEUE_SERIAL);
+ }
+ return self;
+}
+
+- (void)storeAd:(id)ad forHandle:(NSString*)handleId {
+ dispatch_sync(self.syncQueue, ^{
+ self.adInstances[handleId] = ad;
+ });
+}
+
+- (id)adForHandle:(NSString*)handleId {
+ __block id ad = nil;
+ dispatch_sync(self.syncQueue, ^{
+ ad = self.adInstances[handleId];
+ });
+ return ad;
+}
+
+- (void)removeAdForHandle:(NSString*)handleId {
+ dispatch_sync(self.syncQueue, ^{
+ [self.adInstances removeObjectForKey:handleId];
+ });
+}
+
+- (NSString*)generateHandle {
+ return [[NSUUID UUID] UUIDString];
+}
+
+@end
+
+#pragma mark - Ad Delegates
+
+// Reward Video Ad Delegate
+@interface DirichletMediationUnityRewardVideoAdDelegate : NSObject
+@property (nonatomic, strong) NSString* handleId;
+@end
+
+@implementation DirichletMediationUnityRewardVideoAdDelegate
+
+- (void)rewardVideoAdDidShow:(DRMRewardVideoAd *)rewardVideoAd {
+ SendEventToUnity(self.handleId, @"show", @"reward_video", nil);
+}
+
+- (void)rewardVideoAdDidFailToShow:(DRMRewardVideoAd *)rewardVideoAd withError:(NSError *)error {
+ NSDictionary* data = @{
+ @"code": @(error.code),
+ @"message": error.localizedDescription ?: @"Unknown error"
+ };
+ SendEventToUnity(self.handleId, @"show_error", @"reward_video", data);
+}
+
+- (void)rewardVideoAdDidClick:(DRMRewardVideoAd *)rewardVideoAd {
+ SendEventToUnity(self.handleId, @"click", @"reward_video", nil);
+}
+
+- (void)rewardVideoAdDidClose:(DRMRewardVideoAd *)rewardVideoAd {
+ SendEventToUnity(self.handleId, @"close", @"reward_video", nil);
+}
+
+- (void)rewardVideoAdDidRewardUser:(DRMRewardVideoAd *)rewardVideoAd {
+ NSDictionary* data = @{
+ @"rewardVerify": @(YES),
+ @"rewardAmount": @(0),
+ @"rewardName": @"",
+ @"code": @(0),
+ @"message": @""
+ };
+ SendEventToUnity(self.handleId, @"reward", @"reward_video", data);
+}
+
+@end
+
+// Interstitial Ad Delegate
+@interface DirichletMediationUnityInterstitialAdDelegate : NSObject
+@property (nonatomic, strong) NSString* handleId;
+@end
+
+@implementation DirichletMediationUnityInterstitialAdDelegate
+
+- (void)interstitialAdDidShow:(DRMInterstitialAd *)interstitialAd {
+ SendEventToUnity(self.handleId, @"show", @"interstitial", nil);
+}
+
+- (void)interstitialAdDidFailToShow:(DRMInterstitialAd *)interstitialAd withError:(NSError *)error {
+ NSDictionary* data = @{
+ @"code": @(error.code),
+ @"message": error.localizedDescription ?: @"Unknown error"
+ };
+ SendEventToUnity(self.handleId, @"show_error", @"interstitial", data);
+}
+
+- (void)interstitialAdDidClick:(DRMInterstitialAd *)interstitialAd {
+ SendEventToUnity(self.handleId, @"click", @"interstitial", nil);
+}
+
+- (void)interstitialAdDidClose:(DRMInterstitialAd *)interstitialAd {
+ SendEventToUnity(self.handleId, @"close", @"interstitial", nil);
+}
+
+@end
+
+// Banner Ad Delegate
+@interface DirichletMediationUnityBannerAdDelegate : NSObject
+@property (nonatomic, strong) NSString* handleId;
+@end
+
+@implementation DirichletMediationUnityBannerAdDelegate
+
+- (void)bannerAdDidShow:(DRMBannerAd *)bannerAd {
+ SendEventToUnity(self.handleId, @"show", @"banner", nil);
+}
+
+- (void)bannerAdDidFailToShow:(DRMBannerAd *)bannerAd withError:(NSError *)error {
+ NSDictionary* data = @{
+ @"code": @(error.code),
+ @"message": error.localizedDescription ?: @"Unknown error"
+ };
+ SendEventToUnity(self.handleId, @"show_error", @"banner", data);
+}
+
+- (void)bannerAdDidClick:(DRMBannerAd *)bannerAd {
+ SendEventToUnity(self.handleId, @"click", @"banner", nil);
+}
+
+- (void)bannerAdDidClose:(DRMBannerAd *)bannerAd {
+ SendEventToUnity(self.handleId, @"close", @"banner", nil);
+}
+
+@end
+
+// Splash Ad Delegate
+@interface DirichletMediationUnitySplashAdDelegate : NSObject
+@property (nonatomic, strong) NSString* handleId;
+@end
+
+@implementation DirichletMediationUnitySplashAdDelegate
+
+- (void)splashAdDidShow:(DRMSplashAd *)splashAd {
+ SendEventToUnity(self.handleId, @"show", @"splash", nil);
+}
+
+- (void)splashAdDidFailToShow:(DRMSplashAd *)splashAd withError:(NSError *)error {
+ NSDictionary* data = @{
+ @"code": @(error.code),
+ @"message": error.localizedDescription ?: @"Unknown error"
+ };
+ SendEventToUnity(self.handleId, @"show_error", @"splash", data);
+}
+
+- (void)splashAdDidClick:(DRMSplashAd *)splashAd {
+ SendEventToUnity(self.handleId, @"click", @"splash", nil);
+}
+
+- (void)splashAdDidClose:(DRMSplashAd *)splashAd {
+ SendEventToUnity(self.handleId, @"close", @"splash", nil);
+}
+
+@end
+
+#pragma mark - Bridge Implementation
+
+extern "C" {
+
+bool DirichletMediationUnityBridge_Initialize(
+ const char* mediaId,
+ const char* mediaKey,
+ bool enableLog,
+ const char* mediaName,
+ const char* gameChannel,
+ bool shakeEnabled,
+ bool allowIDFAAccess,
+ const char* aTags
+) {
+ NSString* nsMediaId = CreateNSString(mediaId);
+ NSString* nsMediaKey = CreateNSString(mediaKey);
+
+ NSLog(@"[DirichletMediationUnityBridge] Initialize called with mediaId=%@, mediaKey=%@, enableLog=%d",
+ nsMediaId, nsMediaKey, enableLog);
+
+ if (nsMediaId.length == 0 || nsMediaKey.length == 0) {
+ NSLog(@"[DirichletMediationUnityBridge] Initialize failed: mediaId and mediaKey are required");
+ return false;
+ }
+
+ // Check if SDK is already initialized
+ if ([DirichletMediation isInitialized]) {
+ NSLog(@"[DirichletMediationUnityBridge] SDK already initialized");
+ return true;
+ }
+
+ DRMSDKConfig* config = [DRMSDKConfig configWithMediaId:nsMediaId mediaKey:nsMediaKey];
+ if (!config) {
+ NSLog(@"[DirichletMediationUnityBridge] Failed to create SDK config");
+ return false;
+ }
+
+ config.isDebug = enableLog;
+ config.shakeEnabled = shakeEnabled;
+ config.allowIDFAAccess = allowIDFAAccess;
+
+ if (mediaName && strlen(mediaName) > 0) {
+ config.mediaName = CreateNSString(mediaName);
+ }
+
+ if (gameChannel && strlen(gameChannel) > 0) {
+ config.gameChannel = CreateNSString(gameChannel);
+ }
+
+ if (aTags && strlen(aTags) > 0) {
+ config.aTags = CreateNSString(aTags);
+ }
+
+ NSLog(@"[DirichletMediationUnityBridge] Starting SDK initialization (async callback)...");
+ NSLog(@"[DirichletMediationUnityBridge] Config details - mediaId:%@, mediaKey:%@, mediaName:%@, gameChannel:%@",
+ config.mediaId, config.mediaKey, config.mediaName, config.gameChannel);
+
+ // Use async callback pattern (aligned with Ad Unity implementation)
+ void (^startBlock)(void) = ^{
+ [DirichletMediation startWithConfig:config completion:^(BOOL success, NSError * _Nullable error) {
+ if (error) {
+ NSLog(@"[DirichletMediationUnityBridge] SDK init callback - success:%d, error:%@ (code:%ld, domain:%@)",
+ success, error.localizedDescription, (long)error.code, error.domain);
+ } else {
+ NSLog(@"[DirichletMediationUnityBridge] SDK init callback - success:%d", success);
+ }
+ SendInitCallbackToUnity(success, error, success ? @"ios_mediation_bridge" : nil);
+ }];
+ };
+
+ if ([NSThread isMainThread]) {
+ startBlock();
+ } else {
+ dispatch_async(dispatch_get_main_queue(), startBlock);
+ }
+
+ return true;
+}
+
+void DirichletMediationUnityBridge_RequestPermissionIfNeeded(void) {
+ // iOS 14+ ATT permission is handled internally by the SDK
+ NSLog(@"[DirichletMediationUnityBridge] RequestPermissionIfNeeded called");
+}
+
+const char* DirichletMediationUnityBridge_GetSdkVersion(void) {
+ static char* versionCString = NULL;
+ if (versionCString == NULL) {
+ versionCString = MakeCString([DirichletMediation sdkVersion]);
+ }
+ return versionCString;
+}
+
+const char* DirichletMediationUnityBridge_LoadRewardVideoAd(long long spaceId, const char* extras) {
+ NSString* handleId = [[DirichletMediationInstanceManager shared] generateHandle];
+ NSDictionary* extrasDict = ParseExtras(extras);
+
+ // Create load request
+ DRMAdLoadRequest* request = [[DRMAdLoadRequest alloc] initWithSpaceId:[NSString stringWithFormat:@"%lld", spaceId]];
+
+ // Apply extras if provided (matching Unity C# ToBridgePayload keys)
+ if (extrasDict[@"user_id"]) {
+ request.rewardUserId = extrasDict[@"user_id"];
+ }
+ if (extrasDict[@"extra1"]) {
+ request.rewardExtra = extrasDict[@"extra1"];
+ }
+ if (extrasDict[@"reward_name"]) {
+ request.rewardName = extrasDict[@"reward_name"];
+ }
+ if (extrasDict[@"reward_amount"]) {
+ request.rewardAmount = [extrasDict[@"reward_amount"] integerValue];
+ }
+ if (extrasDict[@"mina_id"]) {
+ request.minaId = [NSString stringWithFormat:@"%@", extrasDict[@"mina_id"]];
+ }
+
+ [DRMRewardVideoAd loadWithRequest:request completion:^(NSArray * _Nullable ads, NSError * _Nullable error) {
+ if (ads && ads.count > 0) {
+ DRMRewardVideoAd* ad = ads.firstObject;
+ DirichletMediationUnityRewardVideoAdDelegate* delegate = [[DirichletMediationUnityRewardVideoAdDelegate alloc] init];
+ delegate.handleId = handleId;
+ ad.delegate = delegate;
+
+ [[DirichletMediationInstanceManager shared] storeAd:ad forHandle:handleId];
+ [[DirichletMediationInstanceManager shared] storeAd:delegate forHandle:[handleId stringByAppendingString:@"_delegate"]];
+
+ NSLog(@"[DirichletMediationUnityBridge] RewardVideoAd loaded: %@", handleId);
+ SendLoadCallbackToUnity(handleId, @"load_success", @"reward_video", nil);
+ } else {
+ NSLog(@"[DirichletMediationUnityBridge] RewardVideoAd load failed: %@", error.localizedDescription);
+ NSDictionary* errorData = @{
+ @"code": @(error.code),
+ @"message": error.localizedDescription ?: @"Unknown error"
+ };
+ SendLoadCallbackToUnity(handleId, @"load_error", @"reward_video", errorData);
+ }
+ }];
+
+ return MakeCString(handleId);
+}
+
+const char* DirichletMediationUnityBridge_LoadInterstitialAd(long long spaceId, const char* extras) {
+ NSString* handleId = [[DirichletMediationInstanceManager shared] generateHandle];
+ NSDictionary* extrasDict = ParseExtras(extras);
+
+ // Create load request
+ DRMAdLoadRequest* request = [[DRMAdLoadRequest alloc] initWithSpaceId:[NSString stringWithFormat:@"%lld", spaceId]];
+
+ // Apply extras if provided (matching Unity C# ToBridgePayload keys)
+ if (extrasDict[@"mina_id"]) {
+ request.minaId = [NSString stringWithFormat:@"%@", extrasDict[@"mina_id"]];
+ }
+
+ [DRMInterstitialAd loadWithRequest:request completion:^(NSArray * _Nullable ads, NSError * _Nullable error) {
+ if (ads && ads.count > 0) {
+ DRMInterstitialAd* ad = ads.firstObject;
+ DirichletMediationUnityInterstitialAdDelegate* delegate = [[DirichletMediationUnityInterstitialAdDelegate alloc] init];
+ delegate.handleId = handleId;
+ ad.delegate = delegate;
+
+ [[DirichletMediationInstanceManager shared] storeAd:ad forHandle:handleId];
+ [[DirichletMediationInstanceManager shared] storeAd:delegate forHandle:[handleId stringByAppendingString:@"_delegate"]];
+
+ NSLog(@"[DirichletMediationUnityBridge] InterstitialAd loaded: %@", handleId);
+ SendLoadCallbackToUnity(handleId, @"load_success", @"interstitial", nil);
+ } else {
+ NSLog(@"[DirichletMediationUnityBridge] InterstitialAd load failed: %@", error.localizedDescription);
+ NSDictionary* errorData = @{
+ @"code": @(error.code),
+ @"message": error.localizedDescription ?: @"Unknown error"
+ };
+ SendLoadCallbackToUnity(handleId, @"load_error", @"interstitial", errorData);
+ }
+ }];
+
+ return MakeCString(handleId);
+}
+
+const char* DirichletMediationUnityBridge_LoadBannerAd(long long spaceId, const char* extras) {
+ NSString* handleId = [[DirichletMediationInstanceManager shared] generateHandle];
+ NSDictionary* extrasDict = ParseExtras(extras);
+
+ // Create load request
+ DRMAdLoadRequest* request = [[DRMAdLoadRequest alloc] initWithSpaceId:[NSString stringWithFormat:@"%lld", spaceId]];
+
+ // Apply extras if provided (matching Unity C# ToBridgePayload keys)
+ if (extrasDict[@"mina_id"]) {
+ request.minaId = [NSString stringWithFormat:@"%@", extrasDict[@"mina_id"]];
+ }
+ // Set ad size for Banner (CSJ/GDT adapters need this)
+ NSNumber* width = extrasDict[@"express_width"];
+ NSNumber* height = extrasDict[@"express_height"];
+ if (width || height) {
+ CGFloat w = width ? [width floatValue] : 0;
+ CGFloat h = height ? [height floatValue] : 0;
+ request.adSize = CGSizeMake(w, h);
+ }
+
+ [DRMBannerAd loadWithRequest:request completion:^(NSArray * _Nullable ads, NSError * _Nullable error) {
+ if (ads && ads.count > 0) {
+ DRMBannerAd* ad = ads.firstObject;
+ DirichletMediationUnityBannerAdDelegate* delegate = [[DirichletMediationUnityBannerAdDelegate alloc] init];
+ delegate.handleId = handleId;
+ ad.delegate = delegate;
+
+ [[DirichletMediationInstanceManager shared] storeAd:ad forHandle:handleId];
+ [[DirichletMediationInstanceManager shared] storeAd:delegate forHandle:[handleId stringByAppendingString:@"_delegate"]];
+
+ NSLog(@"[DirichletMediationUnityBridge] BannerAd loaded: %@", handleId);
+ SendLoadCallbackToUnity(handleId, @"load_success", @"banner", nil);
+ } else {
+ NSLog(@"[DirichletMediationUnityBridge] BannerAd load failed: %@", error.localizedDescription);
+ NSDictionary* errorData = @{
+ @"code": @(error.code),
+ @"message": error.localizedDescription ?: @"Unknown error"
+ };
+ SendLoadCallbackToUnity(handleId, @"load_error", @"banner", errorData);
+ }
+ }];
+
+ return MakeCString(handleId);
+}
+
+const char* DirichletMediationUnityBridge_LoadSplashAd(long long spaceId, const char* extras) {
+ NSString* handleId = [[DirichletMediationInstanceManager shared] generateHandle];
+ NSDictionary* extrasDict = ParseExtras(extras);
+
+ // Create load request
+ DRMAdLoadRequest* request = [[DRMAdLoadRequest alloc] initWithSpaceId:[NSString stringWithFormat:@"%lld", spaceId]];
+
+ // Apply extras if provided (matching Unity C# ToBridgePayload keys)
+ if (extrasDict[@"mina_id"]) {
+ request.minaId = [NSString stringWithFormat:@"%@", extrasDict[@"mina_id"]];
+ }
+ // Set ad size for Splash (CSJ adapter needs this)
+ NSNumber* width = extrasDict[@"express_width"];
+ NSNumber* height = extrasDict[@"express_height"];
+ if (width || height) {
+ CGFloat w = width ? [width floatValue] : 0;
+ CGFloat h = height ? [height floatValue] : 0;
+ request.adSize = CGSizeMake(w, h);
+ }
+
+ [DRMSplashAd loadWithRequest:request completion:^(NSArray * _Nullable ads, NSError * _Nullable error) {
+ if (ads && ads.count > 0) {
+ DRMSplashAd* ad = ads.firstObject;
+ DirichletMediationUnitySplashAdDelegate* delegate = [[DirichletMediationUnitySplashAdDelegate alloc] init];
+ delegate.handleId = handleId;
+ ad.delegate = delegate;
+
+ [[DirichletMediationInstanceManager shared] storeAd:ad forHandle:handleId];
+ [[DirichletMediationInstanceManager shared] storeAd:delegate forHandle:[handleId stringByAppendingString:@"_delegate"]];
+
+ NSLog(@"[DirichletMediationUnityBridge] SplashAd loaded: %@", handleId);
+ SendLoadCallbackToUnity(handleId, @"load_success", @"splash", nil);
+ } else {
+ NSLog(@"[DirichletMediationUnityBridge] SplashAd load failed: %@", error.localizedDescription);
+ NSDictionary* errorData = @{
+ @"code": @(error.code),
+ @"message": error.localizedDescription ?: @"Unknown error"
+ };
+ SendLoadCallbackToUnity(handleId, @"load_error", @"splash", errorData);
+ }
+ }];
+
+ return MakeCString(handleId);
+}
+
+bool DirichletMediationUnityBridge_ShowAd(const char* handleId, const char* extras) {
+ NSString* nsHandleId = CreateNSString(handleId);
+ id ad = [[DirichletMediationInstanceManager shared] adForHandle:nsHandleId];
+
+ if (!ad) {
+ NSLog(@"[DirichletMediationUnityBridge] ShowAd failed: Ad not found for handle %@", nsHandleId);
+ return false;
+ }
+
+ // Ensure show is always called on main thread (aligned with Ad Unity implementation)
+ dispatch_async(dispatch_get_main_queue(), ^{
+ UIViewController* rootVC = [[[UIApplication sharedApplication] keyWindow] rootViewController];
+ if (!rootVC) {
+ NSLog(@"[DirichletMediationUnityBridge] ShowAd failed: Root view controller not found");
+ return;
+ }
+
+ if ([ad isKindOfClass:[DRMRewardVideoAd class]]) {
+ DRMRewardVideoAd* rewardAd = (DRMRewardVideoAd*)ad;
+ if ([rewardAd isReady]) {
+ [rewardAd showFromViewController:rootVC];
+ NSLog(@"[DirichletMediationUnityBridge] Showing reward video ad: %@", nsHandleId);
+ }
+ } else if ([ad isKindOfClass:[DRMInterstitialAd class]]) {
+ DRMInterstitialAd* interstitialAd = (DRMInterstitialAd*)ad;
+ if ([interstitialAd isReady]) {
+ [interstitialAd showFromViewController:rootVC];
+ NSLog(@"[DirichletMediationUnityBridge] Showing interstitial ad: %@", nsHandleId);
+ }
+ } else if ([ad isKindOfClass:[DRMBannerAd class]]) {
+ DRMBannerAd* bannerAd = (DRMBannerAd*)ad;
+ UIView* bannerView = bannerAd.view;
+ if (bannerView) {
+ // Banner 广告需要将 view 添加到视图控制器上
+ // 注意:Unity 侧需要通过 Unity UI 系统来处理 Banner 视图
+ // 这里我们发送一个事件通知 Unity 侧,让 Unity 侧来处理视图的展示
+ // 或者直接将视图添加到根视图控制器上(临时方案)
+ [rootVC.view addSubview:bannerView];
+ bannerView.translatesAutoresizingMaskIntoConstraints = NO;
+ // 设置约束,让 Banner 显示在底部
+ [NSLayoutConstraint activateConstraints:@[
+ [bannerView.leadingAnchor constraintEqualToAnchor:rootVC.view.leadingAnchor],
+ [bannerView.trailingAnchor constraintEqualToAnchor:rootVC.view.trailingAnchor],
+ [bannerView.bottomAnchor constraintEqualToAnchor:rootVC.view.safeAreaLayoutGuide.bottomAnchor],
+ [bannerView.heightAnchor constraintEqualToConstant:bannerAd.size.height > 0 ? bannerAd.size.height : 50]
+ ]];
+ NSLog(@"[DirichletMediationUnityBridge] Showing banner ad: %@", nsHandleId);
+ } else {
+ NSLog(@"[DirichletMediationUnityBridge] Banner ad view not available: %@", nsHandleId);
+ }
+ } else if ([ad isKindOfClass:[DRMSplashAd class]]) {
+ DRMSplashAd* splashAd = (DRMSplashAd*)ad;
+ if ([splashAd isReady]) {
+ [splashAd showFromViewController:rootVC];
+ NSLog(@"[DirichletMediationUnityBridge] Showing splash ad: %@", nsHandleId);
+ }
+ } else {
+ NSLog(@"[DirichletMediationUnityBridge] ShowAd failed: Ad not ready or unknown type");
+ }
+ });
+
+ return true;
+}
+
+void DirichletMediationUnityBridge_DestroyAd(const char* handleId) {
+ NSString* nsHandleId = CreateNSString(handleId);
+ NSLog(@"[DirichletMediationUnityBridge] Destroying ad: %@", nsHandleId);
+
+ // Remove ad instance
+ [[DirichletMediationInstanceManager shared] removeAdForHandle:nsHandleId];
+
+ // Remove delegate
+ NSString* delegateKey = [nsHandleId stringByAppendingString:@"_delegate"];
+ [[DirichletMediationInstanceManager shared] removeAdForHandle:delegateKey];
+}
+
+bool DirichletMediationUnityBridge_IsAdValid(const char* handleId) {
+ NSString* nsHandleId = CreateNSString(handleId);
+ id ad = [[DirichletMediationInstanceManager shared] adForHandle:nsHandleId];
+
+ if (!ad) {
+ NSLog(@"[DirichletMediationUnityBridge] IsAdValid: Ad not found for handle %@", nsHandleId);
+ return false;
+ }
+
+ if ([ad isKindOfClass:[DRMRewardVideoAd class]]) {
+ DRMRewardVideoAd* rewardAd = (DRMRewardVideoAd*)ad;
+ return [rewardAd isReady];
+ } else if ([ad isKindOfClass:[DRMInterstitialAd class]]) {
+ DRMInterstitialAd* interstitialAd = (DRMInterstitialAd*)ad;
+ return [interstitialAd isReady];
+ } else if ([ad isKindOfClass:[DRMBannerAd class]]) {
+ // Banner ads don't have isReady, assume valid if loaded
+ return true;
+ } else if ([ad isKindOfClass:[DRMSplashAd class]]) {
+ DRMSplashAd* splashAd = (DRMSplashAd*)ad;
+ return [splashAd isReady];
+ }
+
+ return false;
+}
+
+} // extern "C"
+
diff --git a/Plugins/iOS/DirichletMediationUnityBridge.mm.meta b/Plugins/iOS/DirichletMediationUnityBridge.mm.meta
new file mode 100644
index 0000000..80912d8
--- /dev/null
+++ b/Plugins/iOS/DirichletMediationUnityBridge.mm.meta
@@ -0,0 +1,33 @@
+fileFormatVersion: 2
+guid: 9f8326f62fa7a44aebc39371a5fcddd0
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 0
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ - first:
+ Any:
+ second:
+ enabled: 0
+ settings: {}
+ - first:
+ Editor: Editor
+ second:
+ enabled: 0
+ settings:
+ DefaultValueInitialized: true
+ - first:
+ iPhone: iOS
+ second:
+ enabled: 1
+ settings:
+ AddToEmbeddedBinaries: false
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Samples~.meta b/Samples~.meta
new file mode 100644
index 0000000..4cecf1c
--- /dev/null
+++ b/Samples~.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 1008dc07e5044140babef9a59886d01b
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file
diff --git a/Samples~/IAAAdDebugSample.meta b/Samples~/IAAAdDebugSample.meta
new file mode 100644
index 0000000..24bdffa
--- /dev/null
+++ b/Samples~/IAAAdDebugSample.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5aae7ed566c84b00b0c42455e7c704b3
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file
diff --git a/Samples~/IAAAdDebugSample/Configs.meta b/Samples~/IAAAdDebugSample/Configs.meta
new file mode 100644
index 0000000..2326693
--- /dev/null
+++ b/Samples~/IAAAdDebugSample/Configs.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 0c80b68eb57e4755b9385e066e926f26
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file
diff --git a/Samples~/IAAAdDebugSample/README.md b/Samples~/IAAAdDebugSample/README.md
new file mode 100644
index 0000000..34262aa
--- /dev/null
+++ b/Samples~/IAAAdDebugSample/README.md
@@ -0,0 +1,5 @@
+# TapADN IAA Ad Debug Sample
+
+This optional sample is intentionally lightweight. Import it in a host project, create an `ADConfig`, and initialize with `TapadnCommercialization.InitADManager(...)` to validate rewarded, interstitial, and splash slots.
+
+The main package does not import debug scenes or editor panels by default.
diff --git a/Samples~/IAAAdDebugSample/README.md.meta b/Samples~/IAAAdDebugSample/README.md.meta
new file mode 100644
index 0000000..cd201b0
--- /dev/null
+++ b/Samples~/IAAAdDebugSample/README.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: f23c6cf6cac240bbaa767a723ae8f313
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file
diff --git a/Samples~/IAAAdDebugSample/Runtime.meta b/Samples~/IAAAdDebugSample/Runtime.meta
new file mode 100644
index 0000000..f197b62
--- /dev/null
+++ b/Samples~/IAAAdDebugSample/Runtime.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5b2644e7dd034f42bc57d2033fd01cda
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file
diff --git a/Tapadn_Adapter.asmdef b/Tapadn_Adapter.asmdef
new file mode 100644
index 0000000..9a4ddcf
--- /dev/null
+++ b/Tapadn_Adapter.asmdef
@@ -0,0 +1,16 @@
+{
+ "name": "Tapadn_Adapter",
+ "references": [
+ "Runtime.Adaggregator",
+ "Dirichlet.Mediation.Runtime"
+ ],
+ "includePlatforms": [],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
diff --git a/Tapadn_Adapter.asmdef.meta b/Tapadn_Adapter.asmdef.meta
new file mode 100644
index 0000000..5b8b61f
--- /dev/null
+++ b/Tapadn_Adapter.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 5346bec840144a6f8b806ea11c6235ba
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file
diff --git a/Tapadn_Adapter.meta b/Tapadn_Adapter.meta
new file mode 100644
index 0000000..86f6264
--- /dev/null
+++ b/Tapadn_Adapter.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 4af77eef1d5f4156b2b96eeb89671fb1
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file
diff --git a/Tapadn_Adapter/Editor.meta b/Tapadn_Adapter/Editor.meta
new file mode 100644
index 0000000..15557b0
--- /dev/null
+++ b/Tapadn_Adapter/Editor.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 95121de0d8184f92a4184be55ea40e5b
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file
diff --git a/Tapadn_Adapter/Editor/TapadnBuildAndroidProcess.cs b/Tapadn_Adapter/Editor/TapadnBuildAndroidProcess.cs
new file mode 100644
index 0000000..c18f47c
--- /dev/null
+++ b/Tapadn_Adapter/Editor/TapadnBuildAndroidProcess.cs
@@ -0,0 +1,285 @@
+#if UNITY_ANDROID
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Xml.Linq;
+using UnityEditor;
+using UnityEditor.Android;
+using UnityEngine;
+
+namespace Tapadn_Adapter.Editor
+{
+ public sealed class TapadnBuildAndroidProcess : IPostGenerateGradleAndroidProject
+ {
+ private static readonly XNamespace AndroidNamespace = "http://schemas.android.com/apk/res/android";
+ private static readonly XNamespace ToolsNamespace = "http://schemas.android.com/tools";
+
+ public int callbackOrder => 180;
+
+ public void OnPostGenerateGradleAndroidProject(string path)
+ {
+ ProcessGradleProperties(path);
+ ProcessAndroidManifest(path);
+ }
+
+ public static void ProcessAndroidManifest(string path)
+ {
+ var unityManifestPath = Path.Combine(path, "src/main/AndroidManifest.xml");
+ var launcherManifestPath = Path.Combine(path, "../launcher/src/main/AndroidManifest.xml");
+ var providerTargetManifestPath = File.Exists(launcherManifestPath) ? launcherManifestPath : unityManifestPath;
+
+ if (!File.Exists(unityManifestPath))
+ {
+ Debug.LogWarning($"[TapADN] AndroidManifest.xml is missing: {unityManifestPath}");
+ return;
+ }
+
+ var unityManifest = XDocument.Load(unityManifestPath);
+ EnsureManifestPatch(unityManifest, true, false);
+ unityManifest.Save(unityManifestPath);
+
+ if (File.Exists(providerTargetManifestPath))
+ {
+ var providerManifest = XDocument.Load(providerTargetManifestPath);
+ EnsureManifestPatch(providerManifest, false, true);
+ providerManifest.Save(providerTargetManifestPath);
+ }
+
+ var resXmlPath = Path.Combine(path, "src/main/res/xml");
+ Directory.CreateDirectory(resXmlPath);
+ var sourceXmlPath = GetEditorAssetPath("tapad_ad_file_path.xml");
+ if (!string.IsNullOrEmpty(sourceXmlPath) && File.Exists(sourceXmlPath))
+ {
+ File.Copy(sourceXmlPath, Path.Combine(resXmlPath, "tapad_ad_file_path.xml"), true);
+ }
+ }
+
+ private static void EnsureManifestPatch(XDocument manifest, bool includeWechatActivity, bool includeTapadProvider)
+ {
+ var manifestElement = manifest.Element("manifest");
+ if (manifestElement == null)
+ {
+ return;
+ }
+
+ EnsureNamespace(manifestElement, "tools", ToolsNamespace.NamespaceName);
+
+ EnsurePermission(manifestElement, "android.permission.INTERNET");
+ EnsurePermission(manifestElement, "android.permission.ACCESS_NETWORK_STATE");
+ EnsurePermission(manifestElement, "android.permission.READ_PHONE_STATE");
+ EnsurePermission(manifestElement, "android.permission.QUERY_ALL_PACKAGES");
+ EnsurePermission(manifestElement, "android.permission.REQUEST_INSTALL_PACKAGES");
+ EnsurePermission(manifestElement, "android.permission.BLUETOOTH");
+ EnsurePermission(manifestElement, "android.permission.BLUETOOTH_CONNECT");
+ EnsurePermission(manifestElement, "android.permission.ACCESS_FINE_LOCATION");
+ EnsurePermission(manifestElement, "android.permission.ACCESS_COARSE_LOCATION");
+ EnsurePermission(manifestElement, "android.permission.POST_NOTIFICATIONS");
+ EnsureWechatQueries(manifestElement);
+
+ var applicationElement = manifestElement.Element("application");
+ if (applicationElement == null)
+ {
+ applicationElement = new XElement("application");
+ manifestElement.Add(applicationElement);
+ }
+
+ SetUnityActivitySingleTop(applicationElement);
+
+ if (includeWechatActivity)
+ {
+ EnsureWechatEntryActivity(applicationElement);
+ }
+
+ if (includeTapadProvider)
+ {
+ EnsureTapadFileProvider(applicationElement);
+ SetOrAddAttribute(applicationElement, AndroidNamespace + "allowBackup", "false");
+ MergeToolsReplaceAttribute(applicationElement, "android:allowBackup");
+ }
+ }
+
+ private static void EnsurePermission(XElement manifestElement, string permissionName)
+ {
+ var exists = manifestElement.Elements("uses-permission")
+ .Any(element => element.Attribute(AndroidNamespace + "name")?.Value == permissionName);
+ if (exists)
+ {
+ return;
+ }
+
+ var permission = new XElement("uses-permission");
+ permission.Add(new XAttribute(AndroidNamespace + "name", permissionName));
+ if (permissionName == "android.permission.BLUETOOTH_CONNECT" ||
+ permissionName == "android.permission.POST_NOTIFICATIONS")
+ {
+ permission.Add(new XAttribute(ToolsNamespace + "targetApi", permissionName.EndsWith("CONNECT") ? "s" : "33"));
+ }
+
+ manifestElement.Add(permission);
+ }
+
+ private static void EnsureWechatQueries(XElement manifestElement)
+ {
+ var queries = manifestElement.Elements("queries").FirstOrDefault();
+ if (queries == null)
+ {
+ queries = new XElement("queries");
+ manifestElement.Add(queries);
+ }
+
+ var exists = queries.Elements("package")
+ .Any(element => element.Attribute(AndroidNamespace + "name")?.Value == "com.tencent.mm");
+ if (!exists)
+ {
+ var packageElement = new XElement("package");
+ packageElement.Add(new XAttribute(AndroidNamespace + "name", "com.tencent.mm"));
+ queries.Add(packageElement);
+ }
+ }
+
+ private static void EnsureWechatEntryActivity(XElement applicationElement)
+ {
+ RemoveElementsByAndroidName(applicationElement, "activity", ".wxapi.WXEntryActivity");
+
+ var activity = new XElement("activity");
+ activity.Add(new XAttribute(AndroidNamespace + "name", ".wxapi.WXEntryActivity"));
+ activity.Add(new XAttribute(AndroidNamespace + "label", "@string/app_name"));
+ activity.Add(new XAttribute(AndroidNamespace + "theme", "@android:style/Theme.Translucent.NoTitleBar"));
+ activity.Add(new XAttribute(AndroidNamespace + "exported", "true"));
+ activity.Add(new XAttribute(AndroidNamespace + "taskAffinity", Application.identifier));
+ activity.Add(new XAttribute(AndroidNamespace + "launchMode", "singleTop"));
+ applicationElement.Add(activity);
+ }
+
+ private static void EnsureTapadFileProvider(XElement applicationElement)
+ {
+ RemoveElementsByAndroidName(applicationElement, "provider", "com.tapsdk.tapad.internal.TapADFileProvider");
+
+ var provider = new XElement("provider");
+ provider.Add(new XAttribute(AndroidNamespace + "name", "com.tapsdk.tapad.internal.TapADFileProvider"));
+ provider.Add(new XAttribute(AndroidNamespace + "authorities", "${applicationId}.com.tds.ad.fileprovider"));
+ provider.Add(new XAttribute(AndroidNamespace + "exported", "false"));
+ provider.Add(new XAttribute(AndroidNamespace + "grantUriPermissions", "true"));
+ provider.Add(new XAttribute(ToolsNamespace + "replace", "android:authorities"));
+
+ var metadata = new XElement("meta-data");
+ metadata.Add(new XAttribute(AndroidNamespace + "name", "android.support.FILE_PROVIDER_PATHS"));
+ metadata.Add(new XAttribute(AndroidNamespace + "resource", "@xml/tapad_ad_file_path"));
+ provider.Add(metadata);
+
+ applicationElement.Add(provider);
+ }
+
+ private static void SetUnityActivitySingleTop(XElement applicationElement)
+ {
+ foreach (var activity in applicationElement.Elements("activity"))
+ {
+ var name = activity.Attribute(AndroidNamespace + "name")?.Value;
+ if (name == "com.unity3d.player.UnityPlayerActivity")
+ {
+ SetOrAddAttribute(activity, AndroidNamespace + "launchMode", "singleTop");
+ }
+ }
+ }
+
+ private static void RemoveElementsByAndroidName(XElement parent, string localName, string androidName)
+ {
+ parent.Elements(localName)
+ .Where(element => element.Attribute(AndroidNamespace + "name")?.Value == androidName)
+ .ToList()
+ .ForEach(element => element.Remove());
+ }
+
+ private static void ProcessGradleProperties(string path)
+ {
+ var root = Directory.GetParent(path)?.FullName;
+ if (string.IsNullOrEmpty(root))
+ {
+ return;
+ }
+
+ var gradlePropertiesPath = Path.Combine(root, "gradle.properties");
+ var lines = File.Exists(gradlePropertiesPath)
+ ? File.ReadAllLines(gradlePropertiesPath).ToList()
+ : new List();
+
+ UpsertProperty(lines, "android.useAndroidX", "true");
+ UpsertProperty(lines, "android.enableJetifier", "true");
+
+ File.WriteAllText(gradlePropertiesPath, string.Join("\n", lines) + "\n");
+ }
+
+ private static void UpsertProperty(List lines, string key, string value)
+ {
+ var prefix = key + "=";
+ var index = lines.FindIndex(line => line.TrimStart().StartsWith(prefix));
+ if (index >= 0)
+ {
+ lines[index] = prefix + value;
+ return;
+ }
+
+ lines.Add(prefix + value);
+ }
+
+ private static void SetOrAddAttribute(XElement element, XName attributeName, string value)
+ {
+ var attribute = element.Attribute(attributeName);
+ if (attribute == null)
+ {
+ element.Add(new XAttribute(attributeName, value));
+ return;
+ }
+
+ attribute.SetValue(value);
+ }
+
+ private static void MergeToolsReplaceAttribute(XElement element, params string[] replaceValues)
+ {
+ var replaceAttributeName = ToolsNamespace + "replace";
+ var mergedValues = new List();
+ var currentValue = element.Attribute(replaceAttributeName)?.Value;
+ if (!string.IsNullOrWhiteSpace(currentValue))
+ {
+ mergedValues.AddRange(currentValue.Split(',')
+ .Select(value => value.Trim())
+ .Where(value => !string.IsNullOrWhiteSpace(value)));
+ }
+
+ foreach (var replaceValue in replaceValues)
+ {
+ if (!mergedValues.Contains(replaceValue))
+ {
+ mergedValues.Add(replaceValue);
+ }
+ }
+
+ element.SetAttributeValue(replaceAttributeName, string.Join(",", mergedValues));
+ }
+
+ private static void EnsureNamespace(XElement element, string prefix, string namespaceName)
+ {
+ var namespaceAttributeName = XNamespace.Xmlns + prefix;
+ if (element.Attribute(namespaceAttributeName) == null)
+ {
+ element.Add(new XAttribute(namespaceAttributeName, namespaceName));
+ }
+ }
+
+ private static string GetEditorAssetPath(string fileName)
+ {
+ var guids = AssetDatabase.FindAssets(Path.GetFileNameWithoutExtension(fileName));
+ foreach (var guid in guids)
+ {
+ var path = AssetDatabase.GUIDToAssetPath(guid);
+ if (Path.GetFileName(path) == fileName)
+ {
+ return path;
+ }
+ }
+
+ return null;
+ }
+ }
+}
+#endif
diff --git a/Tapadn_Adapter/Editor/TapadnBuildAndroidProcess.cs.meta b/Tapadn_Adapter/Editor/TapadnBuildAndroidProcess.cs.meta
new file mode 100644
index 0000000..0bc3446
--- /dev/null
+++ b/Tapadn_Adapter/Editor/TapadnBuildAndroidProcess.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0ab0342739d64cabbf5360a6f49250a1
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file
diff --git a/Tapadn_Adapter/Editor/Tapadn_Adapter.Editor.asmdef b/Tapadn_Adapter/Editor/Tapadn_Adapter.Editor.asmdef
new file mode 100644
index 0000000..876b75e
--- /dev/null
+++ b/Tapadn_Adapter/Editor/Tapadn_Adapter.Editor.asmdef
@@ -0,0 +1,15 @@
+{
+ "name": "Tapadn_Adapter.Editor",
+ "references": [],
+ "includePlatforms": [
+ "Editor"
+ ],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
diff --git a/Tapadn_Adapter/Editor/Tapadn_Adapter.Editor.asmdef.meta b/Tapadn_Adapter/Editor/Tapadn_Adapter.Editor.asmdef.meta
new file mode 100644
index 0000000..69582dd
--- /dev/null
+++ b/Tapadn_Adapter/Editor/Tapadn_Adapter.Editor.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 0d92d8c493204ca298403fceb36f5a71
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file
diff --git a/Tapadn_Adapter/Editor/WXDependencies.xml b/Tapadn_Adapter/Editor/WXDependencies.xml
new file mode 100644
index 0000000..ece3676
--- /dev/null
+++ b/Tapadn_Adapter/Editor/WXDependencies.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+ https://maven.aliyun.com/repository/public
+ https://repo.maven.apache.org/maven2
+
+
+
+
diff --git a/Tapadn_Adapter/Editor/WXDependencies.xml.meta b/Tapadn_Adapter/Editor/WXDependencies.xml.meta
new file mode 100644
index 0000000..a65ec31
--- /dev/null
+++ b/Tapadn_Adapter/Editor/WXDependencies.xml.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: ea7cb8d4cc2e4dce8859500b44a86028
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file
diff --git a/Tapadn_Adapter/Editor/tapad_ad_file_path.xml b/Tapadn_Adapter/Editor/tapad_ad_file_path.xml
new file mode 100644
index 0000000..304aad0
--- /dev/null
+++ b/Tapadn_Adapter/Editor/tapad_ad_file_path.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/Tapadn_Adapter/Editor/tapad_ad_file_path.xml.meta b/Tapadn_Adapter/Editor/tapad_ad_file_path.xml.meta
new file mode 100644
index 0000000..324e679
--- /dev/null
+++ b/Tapadn_Adapter/Editor/tapad_ad_file_path.xml.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: dcb4be7a15dd43aaa4862ab35f8b309b
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file
diff --git a/Tapadn_Adapter/Runtime.meta b/Tapadn_Adapter/Runtime.meta
new file mode 100644
index 0000000..c257b52
--- /dev/null
+++ b/Tapadn_Adapter/Runtime.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a9c09f845d424a529010aa7f15b05e9c
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file
diff --git a/Tapadn_Adapter/Runtime/Scripts.meta b/Tapadn_Adapter/Runtime/Scripts.meta
new file mode 100644
index 0000000..1a88e9e
--- /dev/null
+++ b/Tapadn_Adapter/Runtime/Scripts.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a07b2d839f4748b0b52bd5bcb950f111
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file
diff --git a/Tapadn_Adapter/Runtime/Scripts/TapadnAdController.cs b/Tapadn_Adapter/Runtime/Scripts/TapadnAdController.cs
new file mode 100644
index 0000000..2ff8d1c
--- /dev/null
+++ b/Tapadn_Adapter/Runtime/Scripts/TapadnAdController.cs
@@ -0,0 +1,75 @@
+using System;
+using Dirichlet.Mediation;
+using Runtime.ADAggregator;
+using UnityEngine;
+
+public sealed class TapadnAdController : IAdController
+{
+ public static TapadnControllerOptions CurrentOptions { get; private set; }
+ public static string LastSdkVersion { get; private set; }
+ public static string LastInitError { get; private set; }
+
+ private Action _maskAction;
+ private Action _logEventAction;
+ private ADConfig _adConfig;
+ private TapadnControllerOptions _options;
+
+ public void Init(ADConfig adConfig, object[] args)
+ {
+ _adConfig = adConfig;
+ _options = TapadnControllerOptions.Resolve(adConfig, args);
+ CurrentOptions = _options;
+
+ var sdkConfig = _options.BuildSdkConfig();
+ DirichletSdk.Init(
+ sdkConfig,
+ result =>
+ {
+ LastInitError = null;
+ LastSdkVersion = DirichletSdk.GetVersion();
+ EventLog("tapadn_init", "success", result?.Message);
+ Debug.Log($"[TapADN] Init success. version={LastSdkVersion}, message={result?.Message}");
+ if (_options.RequestPermissionOnInit)
+ {
+ DirichletSdk.RequestPermissionIfNecessary();
+ }
+ },
+ error =>
+ {
+ LastInitError = $"{error?.Code}:{error?.Message}";
+ EventLog("tapadn_init_error", error?.Code ?? "unknown", error?.Message);
+ Debug.LogError($"[TapADN] Init failed. code={error?.Code}, message={error?.Message}");
+ });
+ }
+
+ public void SetListener(Action adMaskAction, Action logEventAction)
+ {
+ _maskAction = adMaskAction;
+ _logEventAction = logEventAction;
+ }
+
+ public ADPlayer CreateAdPlayer(AD_Type type)
+ {
+ switch (type)
+ {
+ case AD_Type.AwardVideo:
+ return new TapadnAwardVideoPlayer().Init(_adConfig?.BaseAwardAdKeyValue?.value);
+ case AD_Type.Interaction:
+ return new TapadnInteractionPlayer().Init(_adConfig?.BaseInteractionAdKeyValue?.value);
+ case AD_Type.Splash:
+ return new TapadnSplashPlayer().Init(_adConfig?.BaseSplashAdKeyValue?.value);
+ default:
+ return null;
+ }
+ }
+
+ public void EventLog(string eventTable, string eventValue, string eventMessage = null)
+ {
+ _logEventAction?.Invoke(eventTable, eventValue);
+ }
+
+ public void SetMask(bool isOpen)
+ {
+ _maskAction?.Invoke(isOpen);
+ }
+}
diff --git a/Tapadn_Adapter/Runtime/Scripts/TapadnAdController.cs.meta b/Tapadn_Adapter/Runtime/Scripts/TapadnAdController.cs.meta
new file mode 100644
index 0000000..dfd8a5c
--- /dev/null
+++ b/Tapadn_Adapter/Runtime/Scripts/TapadnAdController.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a08780bf18a349b58a35e2dfb79d0743
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file
diff --git a/Tapadn_Adapter/Runtime/Scripts/TapadnAdRequestFactory.cs b/Tapadn_Adapter/Runtime/Scripts/TapadnAdRequestFactory.cs
new file mode 100644
index 0000000..4dab42a
--- /dev/null
+++ b/Tapadn_Adapter/Runtime/Scripts/TapadnAdRequestFactory.cs
@@ -0,0 +1,55 @@
+using System;
+using Dirichlet.Mediation;
+using Runtime.ADAggregator;
+using UnityEngine;
+
+internal static class TapadnAdRequestFactory
+{
+ public static bool TryParseSlotId(string slotId, out long parsed)
+ {
+ return long.TryParse(slotId, out parsed) && parsed > 0;
+ }
+
+ public static DirichletAdRequest BuildRewarded(string slotId, TapadnControllerOptions options)
+ {
+ var builder = CreateBaseBuilder(slotId)
+ .WithUserId(ADManager.Instance.UserId)
+ .WithRewardName(options?.RewardName ?? "reward")
+ .WithRewardAmount(options?.RewardAmount ?? 1);
+
+ return builder.Build();
+ }
+
+ public static DirichletAdRequest BuildInterstitial(string slotId, TapadnControllerOptions options)
+ {
+ return ApplyExpressSize(CreateBaseBuilder(slotId), options).Build();
+ }
+
+ public static DirichletAdRequest BuildSplash(string slotId, TapadnControllerOptions options)
+ {
+ var builder = CreateBaseBuilder(slotId);
+ var width = options?.ExpressWidth ?? Screen.width;
+ var height = options?.ExpressHeight ?? Screen.height;
+ return builder.WithExpressViewSize(width, height).Build();
+ }
+
+ private static DirichletAdRequest.Builder CreateBaseBuilder(string slotId)
+ {
+ if (!TryParseSlotId(slotId, out var parsed))
+ {
+ throw new ArgumentException($"TapADN slot id must be a positive integer. Current value: {slotId}", nameof(slotId));
+ }
+
+ return new DirichletAdRequest.Builder().WithSpaceId(parsed);
+ }
+
+ private static DirichletAdRequest.Builder ApplyExpressSize(DirichletAdRequest.Builder builder, TapadnControllerOptions options)
+ {
+ if (options?.ExpressWidth != null || options?.ExpressHeight != null)
+ {
+ builder.WithExpressViewSize(options?.ExpressWidth ?? Screen.width, options?.ExpressHeight ?? Screen.height);
+ }
+
+ return builder;
+ }
+}
diff --git a/Tapadn_Adapter/Runtime/Scripts/TapadnAdRequestFactory.cs.meta b/Tapadn_Adapter/Runtime/Scripts/TapadnAdRequestFactory.cs.meta
new file mode 100644
index 0000000..a0e1cd0
--- /dev/null
+++ b/Tapadn_Adapter/Runtime/Scripts/TapadnAdRequestFactory.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 01ac342b5e1c49cf87b649eddcd34f7c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file
diff --git a/Tapadn_Adapter/Runtime/Scripts/TapadnAwardVideoPlayer.cs b/Tapadn_Adapter/Runtime/Scripts/TapadnAwardVideoPlayer.cs
new file mode 100644
index 0000000..1bc9a83
--- /dev/null
+++ b/Tapadn_Adapter/Runtime/Scripts/TapadnAwardVideoPlayer.cs
@@ -0,0 +1,159 @@
+using System;
+using Dirichlet.Mediation;
+using Runtime.ADAggregator;
+using UnityEngine;
+
+public sealed class TapadnAwardVideoPlayer : ADPlayer, IDirichletRewardVideoAutoAdListener
+{
+ private DirichletAdNative _adNative;
+ private DirichletRewardVideoAd _loadedAd;
+ private bool _rewardVerified;
+
+ public override int MaxLoadAttempts => TapadnAdController.CurrentOptions?.RewardedMaxLoadAttempts ?? base.MaxLoadAttempts;
+ public override float LoadRetryDelaySeconds => Math.Max(0f, (TapadnAdController.CurrentOptions?.RewardedLoadRetryDelayMs ?? 500) / 1000f);
+ public override float ShowPendingTimeoutSeconds => Math.Max(1f, (TapadnAdController.CurrentOptions?.RewardedShowTimeoutMs ?? 20000) / 1000f);
+ public override bool AutoPreloadOnInit => TapadnAdController.CurrentOptions?.RewardedPrewarmOnInit ?? false;
+
+ public override void OnInit()
+ {
+ _adNative = DirichletAdManager.CreateAdNative();
+ }
+
+ public override bool IsReadly()
+ {
+ if (UseAutoLoad())
+ {
+ return TapadnAdRequestFactory.TryParseSlotId(Key, out _);
+ }
+
+ if (_loadedAd != null && _loadedAd.IsLoaded && _loadedAd.IsValid)
+ {
+ curState = 2;
+ return true;
+ }
+
+ return false;
+ }
+
+ public override void LoadAD()
+ {
+ if (!TapadnAdRequestFactory.TryParseSlotId(Key, out _))
+ {
+ Debug.LogError($"[TapADN] Invalid rewarded slot id: {Key}");
+ curState = 0;
+ return;
+ }
+
+ if (UseAutoLoad())
+ {
+ curState = 2;
+ try
+ {
+ _adNative.PreLoad(TapadnAdRequestFactory.BuildRewarded(Key, TapadnAdController.CurrentOptions), 3);
+ }
+ catch (Exception exception)
+ {
+ Debug.LogWarning($"[TapADN] Rewarded preload skipped: {exception.Message}");
+ }
+ return;
+ }
+
+ if (curState == 1 || IsReadly())
+ {
+ return;
+ }
+
+ curState = 1;
+ _adNative.LoadRewardVideoAd(
+ TapadnAdRequestFactory.BuildRewarded(Key, TapadnAdController.CurrentOptions),
+ ad =>
+ {
+ _loadedAd?.Destroy();
+ _loadedAd = ad;
+ _loadedAd.Shown += OnManualShown;
+ _loadedAd.Clicked += OnManualClicked;
+ _loadedAd.RewardVerified += OnManualRewardVerify;
+ _loadedAd.Closed += OnManualClosed;
+ curState = 2;
+ Debug.Log($"[TapADN] Rewarded loaded. slot={Key}");
+ },
+ error =>
+ {
+ curState = 0;
+ Debug.LogError($"[TapADN] Rewarded load failed. code={error.Code}, message={error.Message}");
+ });
+ }
+
+ public override void ShowAD(Action onClose, Action onVideoComplete)
+ {
+ adListener.onClose = onClose;
+ adListener.onVideoComplete = onVideoComplete;
+ _rewardVerified = false;
+ curState = 0;
+
+ if (UseAutoLoad())
+ {
+ _adNative.ShowRewardVideoAutoAd(TapadnAdRequestFactory.BuildRewarded(Key, TapadnAdController.CurrentOptions), this);
+ return;
+ }
+
+ if (_loadedAd == null || !_loadedAd.Show())
+ {
+ adListener.OnShowError();
+ return;
+ }
+ }
+
+ public void OnError(DirichletError error)
+ {
+ curState = 0;
+ Debug.LogError($"[TapADN] Rewarded show failed. code={error?.Code}, message={error?.Message}");
+ adListener.OnShowError();
+ }
+
+ public void OnAdShow()
+ {
+ NotifyShowStarted();
+ }
+
+ public void OnAdClose()
+ {
+ adListener.OnRewardVerify(_rewardVerified, TapadnAdController.CurrentOptions?.RewardAmount ?? 1, TapadnAdController.CurrentOptions?.RewardName ?? string.Empty);
+ adListener.OnAdClose();
+ }
+
+ public void OnRewardVerify(DirichletRewardVerificationEventArgs args)
+ {
+ _rewardVerified = args != null && args.IsVerified;
+ }
+
+ public void OnAdClick()
+ {
+ }
+
+ private bool UseAutoLoad()
+ {
+ return TapadnAdController.CurrentOptions?.RewardedAutoLoad ?? true;
+ }
+
+ private void OnManualShown()
+ {
+ NotifyShowStarted();
+ }
+
+ private void OnManualClicked()
+ {
+ }
+
+ private void OnManualRewardVerify(DirichletRewardVerificationEventArgs args)
+ {
+ _rewardVerified = args != null && args.IsVerified;
+ }
+
+ private void OnManualClosed()
+ {
+ _loadedAd?.Destroy();
+ _loadedAd = null;
+ OnAdClose();
+ }
+}
diff --git a/Tapadn_Adapter/Runtime/Scripts/TapadnAwardVideoPlayer.cs.meta b/Tapadn_Adapter/Runtime/Scripts/TapadnAwardVideoPlayer.cs.meta
new file mode 100644
index 0000000..10f606c
--- /dev/null
+++ b/Tapadn_Adapter/Runtime/Scripts/TapadnAwardVideoPlayer.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: fb7643101da94ca69dc8f99128d4e759
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file
diff --git a/Tapadn_Adapter/Runtime/Scripts/TapadnCommercialization.cs b/Tapadn_Adapter/Runtime/Scripts/TapadnCommercialization.cs
new file mode 100644
index 0000000..bfc1a0c
--- /dev/null
+++ b/Tapadn_Adapter/Runtime/Scripts/TapadnCommercialization.cs
@@ -0,0 +1,34 @@
+using System;
+using Runtime.ADAggregator;
+
+public static class TapadnCommercialization
+{
+ public static TapadnAdController CreateController()
+ {
+ return new TapadnAdController();
+ }
+
+ public static void InitADManager(Action onCallback, string userId, ADConfig adConfig, params object[] args)
+ {
+ ADManager.Instance.Init(onCallback, userId, adConfig, CreateController(), args);
+ }
+
+ public static ADConfig CreateConfig(
+ string mediaId,
+ string mediaKey,
+ string mediaName,
+ string rewardSlotId,
+ string interstitialSlotId = null,
+ string splashSlotId = null)
+ {
+ var config = UnityEngine.ScriptableObject.CreateInstance();
+ config.ConfigName = "TapADN";
+ config.Id = mediaId;
+ config.Key = mediaKey;
+ config.Key2 = mediaName;
+ config.BaseAwardAdKeyValue = new AdKeyValue { key = "reward", value = rewardSlotId };
+ config.BaseInteractionAdKeyValue = new AdKeyValue { key = "interaction", value = interstitialSlotId };
+ config.BaseSplashAdKeyValue = new AdKeyValue { key = "splash", value = splashSlotId };
+ return config;
+ }
+}
diff --git a/Tapadn_Adapter/Runtime/Scripts/TapadnCommercialization.cs.meta b/Tapadn_Adapter/Runtime/Scripts/TapadnCommercialization.cs.meta
new file mode 100644
index 0000000..323c287
--- /dev/null
+++ b/Tapadn_Adapter/Runtime/Scripts/TapadnCommercialization.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 295d10d222a8496e872b02eff3556e86
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file
diff --git a/Tapadn_Adapter/Runtime/Scripts/TapadnControllerOptions.cs b/Tapadn_Adapter/Runtime/Scripts/TapadnControllerOptions.cs
new file mode 100644
index 0000000..49915a4
--- /dev/null
+++ b/Tapadn_Adapter/Runtime/Scripts/TapadnControllerOptions.cs
@@ -0,0 +1,370 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using Dirichlet.Mediation;
+using Runtime.ADAggregator;
+using UnityEngine;
+
+public sealed class TapadnControllerOptions
+{
+ public const string MediaNameKey = "tapadn.media_name";
+ public const string MediaIdKey = "tapadn.media_id";
+ public const string MediaKeyKey = "tapadn.media_key";
+ public const string ChannelKey = "tapadn.channel";
+ public const string SubChannelKey = "tapadn.sub_channel";
+ public const string DebugKey = "tapadn.debug";
+ public const string TapClientIdKey = "tapadn.tap_client_id";
+ public const string ShakeEnabledKey = "tapadn.shake_enabled";
+ public const string CustomConfigJsonKey = "tapadn.custom_config_json";
+ public const string DataJsonKey = "tapadn.data_json";
+ public const string ATagsKey = "tapadn.atags";
+ public const string AllowIdfaAccessKey = "tapadn.allow_idfa_access";
+ public const string RequestPermissionOnInitKey = "tapadn.request_permission_on_init";
+ public const string RewardedAutoLoadKey = "tapadn.rewarded_auto_load";
+ public const string RewardedPrewarmOnInitKey = "tapadn.rewarded_prewarm_on_init";
+ public const string RewardedMaxLoadAttemptsKey = "tapadn.rewarded_max_load_attempts";
+ public const string RewardedLoadRetryDelayMsKey = "tapadn.rewarded_load_retry_delay_ms";
+ public const string RewardedShowTimeoutMsKey = "tapadn.rewarded_show_timeout_ms";
+ public const string RewardNameKey = "tapadn.reward_name";
+ public const string RewardAmountKey = "tapadn.reward_amount";
+ public const string InterstitialAutoLoadKey = "tapadn.interstitial_auto_load";
+ public const string InterstitialPrewarmOnInitKey = "tapadn.interstitial_prewarm_on_init";
+ public const string InterstitialMaxLoadAttemptsKey = "tapadn.interstitial_max_load_attempts";
+ public const string InterstitialLoadRetryDelayMsKey = "tapadn.interstitial_load_retry_delay_ms";
+ public const string InterstitialShowTimeoutMsKey = "tapadn.interstitial_show_timeout_ms";
+ public const string SplashAutoLoadKey = "tapadn.splash_auto_load";
+ public const string SplashPrewarmOnInitKey = "tapadn.splash_prewarm_on_init";
+ public const string SplashMaxLoadAttemptsKey = "tapadn.splash_max_load_attempts";
+ public const string SplashLoadRetryDelayMsKey = "tapadn.splash_load_retry_delay_ms";
+ public const string SplashShowTimeoutMsKey = "tapadn.splash_show_timeout_ms";
+ public const string ExpressWidthKey = "tapadn.express_width";
+ public const string ExpressHeightKey = "tapadn.express_height";
+
+ public long MediaId { get; set; }
+ public string MediaName { get; set; }
+ public string MediaKey { get; set; }
+ public string Channel { get; set; } = "default";
+ public string SubChannel { get; set; }
+ public bool Debug { get; set; }
+ public string TapClientId { get; set; }
+ public bool ShakeEnabled { get; set; } = true;
+ public string CustomConfigJson { get; set; }
+ public string DataJson { get; set; }
+ public string ATags { get; set; }
+ public bool AllowIDFAAccess { get; set; } = true;
+ public bool RequestPermissionOnInit { get; set; }
+ public bool RewardedAutoLoad { get; set; } = true;
+ public bool RewardedPrewarmOnInit { get; set; }
+ public int RewardedMaxLoadAttempts { get; set; } = 1;
+ public int RewardedLoadRetryDelayMs { get; set; } = 500;
+ public int RewardedShowTimeoutMs { get; set; } = 20000;
+ public string RewardName { get; set; } = "reward";
+ public int RewardAmount { get; set; } = 1;
+ public bool InterstitialAutoLoad { get; set; } = true;
+ public bool InterstitialPrewarmOnInit { get; set; }
+ public int InterstitialMaxLoadAttempts { get; set; } = 1;
+ public int InterstitialLoadRetryDelayMs { get; set; } = 500;
+ public int InterstitialShowTimeoutMs { get; set; } = 20000;
+ public bool SplashAutoLoad { get; set; } = true;
+ public bool SplashPrewarmOnInit { get; set; }
+ public int SplashMaxLoadAttempts { get; set; } = 1;
+ public int SplashLoadRetryDelayMs { get; set; } = 500;
+ public int SplashShowTimeoutMs { get; set; } = 20000;
+ public int? ExpressWidth { get; set; }
+ public int? ExpressHeight { get; set; }
+
+ public static TapadnControllerOptions Resolve(ADConfig adConfig, object[] args)
+ {
+ var options = new TapadnControllerOptions();
+ options.ApplyAdConfig(adConfig);
+ options.ApplyLegacyArgs(args);
+
+ if (args == null)
+ {
+ return options;
+ }
+
+ foreach (var arg in args)
+ {
+ switch (arg)
+ {
+ case TapadnControllerOptions explicitOptions:
+ options.ApplyExplicitOptions(explicitOptions);
+ break;
+ case IDictionary dictionary:
+ options.ApplyDictionary(dictionary);
+ break;
+ }
+ }
+
+ return options;
+ }
+
+ public DirichletAdConfig BuildSdkConfig()
+ {
+ return new DirichletAdConfig.Builder()
+ .WithMediaId(MediaId)
+ .WithMediaName(string.IsNullOrWhiteSpace(MediaName) ? Application.productName : MediaName)
+ .WithMediaKey(MediaKey)
+ .WithGameChannel(string.IsNullOrWhiteSpace(Channel) ? "default" : Channel)
+ .WithSubChannel(SubChannel)
+ .EnableDebug(Debug)
+ .WithTapClientId(TapClientId)
+ .ShakeEnabled(ShakeEnabled)
+ .WithCustomConfigJson(CustomConfigJson)
+ .WithDataJson(DataJson)
+ .AllowIDFAAccess(AllowIDFAAccess)
+ .WithATags(ATags)
+ .Build();
+ }
+
+ private void ApplyAdConfig(ADConfig adConfig)
+ {
+ if (adConfig == null)
+ {
+ return;
+ }
+
+ MediaId = ParseLong(adConfig.Id) ?? MediaId;
+ MediaKey = string.IsNullOrWhiteSpace(adConfig.Key) ? MediaKey : adConfig.Key.Trim();
+ MediaName = string.IsNullOrWhiteSpace(adConfig.Key2) ? MediaName : adConfig.Key2.Trim();
+ ApplyCommonKeyValues(adConfig.CommonKeyValues);
+ }
+
+ private void ApplyCommonKeyValues(List keyValues)
+ {
+ if (keyValues == null || keyValues.Count == 0)
+ {
+ return;
+ }
+
+ var map = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (var keyValue in keyValues)
+ {
+ if (keyValue == null || string.IsNullOrWhiteSpace(keyValue.key))
+ {
+ continue;
+ }
+
+ map[keyValue.key.Trim()] = keyValue.value;
+ }
+
+ ApplyMap(map);
+ }
+
+ private void ApplyLegacyArgs(object[] args)
+ {
+ if (args == null || args.Length == 0)
+ {
+ return;
+ }
+
+ if (args.Length > 0 && args[0] is string channel)
+ {
+ Channel = channel;
+ }
+
+ if (args.Length > 1 && TryConvertBool(args[1], out var debug))
+ {
+ Debug = debug;
+ }
+ }
+
+ private void ApplyExplicitOptions(TapadnControllerOptions incoming)
+ {
+ if (incoming == null)
+ {
+ return;
+ }
+
+ MediaId = incoming.MediaId > 0 ? incoming.MediaId : MediaId;
+ MediaName = incoming.MediaName ?? MediaName;
+ MediaKey = incoming.MediaKey ?? MediaKey;
+ Channel = incoming.Channel ?? Channel;
+ SubChannel = incoming.SubChannel ?? SubChannel;
+ Debug = incoming.Debug;
+ TapClientId = incoming.TapClientId ?? TapClientId;
+ ShakeEnabled = incoming.ShakeEnabled;
+ CustomConfigJson = incoming.CustomConfigJson ?? CustomConfigJson;
+ DataJson = incoming.DataJson ?? DataJson;
+ ATags = incoming.ATags ?? ATags;
+ AllowIDFAAccess = incoming.AllowIDFAAccess;
+ RequestPermissionOnInit = incoming.RequestPermissionOnInit;
+ RewardedAutoLoad = incoming.RewardedAutoLoad;
+ RewardedPrewarmOnInit = incoming.RewardedPrewarmOnInit;
+ RewardedMaxLoadAttempts = incoming.RewardedMaxLoadAttempts;
+ RewardedLoadRetryDelayMs = incoming.RewardedLoadRetryDelayMs;
+ RewardedShowTimeoutMs = incoming.RewardedShowTimeoutMs;
+ RewardName = incoming.RewardName ?? RewardName;
+ RewardAmount = incoming.RewardAmount;
+ InterstitialAutoLoad = incoming.InterstitialAutoLoad;
+ InterstitialPrewarmOnInit = incoming.InterstitialPrewarmOnInit;
+ InterstitialMaxLoadAttempts = incoming.InterstitialMaxLoadAttempts;
+ InterstitialLoadRetryDelayMs = incoming.InterstitialLoadRetryDelayMs;
+ InterstitialShowTimeoutMs = incoming.InterstitialShowTimeoutMs;
+ SplashAutoLoad = incoming.SplashAutoLoad;
+ SplashPrewarmOnInit = incoming.SplashPrewarmOnInit;
+ SplashMaxLoadAttempts = incoming.SplashMaxLoadAttempts;
+ SplashLoadRetryDelayMs = incoming.SplashLoadRetryDelayMs;
+ SplashShowTimeoutMs = incoming.SplashShowTimeoutMs;
+ ExpressWidth = incoming.ExpressWidth ?? ExpressWidth;
+ ExpressHeight = incoming.ExpressHeight ?? ExpressHeight;
+ }
+
+ private void ApplyDictionary(IDictionary dictionary)
+ {
+ if (dictionary == null || dictionary.Count == 0)
+ {
+ return;
+ }
+
+ var map = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (DictionaryEntry entry in dictionary)
+ {
+ if (entry.Key == null)
+ {
+ continue;
+ }
+
+ map[entry.Key.ToString()] = ConvertDictionaryValue(entry.Value);
+ }
+
+ ApplyMap(map);
+ }
+
+ private void ApplyMap(IDictionary map)
+ {
+ MediaId = GetLong(map, MediaIdKey, "media_id") ?? MediaId;
+ MediaName = GetString(map, MediaNameKey, "media_name") ?? MediaName;
+ MediaKey = GetString(map, MediaKeyKey, "media_key") ?? MediaKey;
+ Channel = GetString(map, ChannelKey, "channel") ?? Channel;
+ SubChannel = GetString(map, SubChannelKey, "sub_channel") ?? SubChannel;
+ Debug = GetBool(map, DebugKey, "debug") ?? Debug;
+ TapClientId = GetString(map, TapClientIdKey, "tap_client_id") ?? TapClientId;
+ ShakeEnabled = GetBool(map, ShakeEnabledKey) ?? ShakeEnabled;
+ CustomConfigJson = GetString(map, CustomConfigJsonKey) ?? CustomConfigJson;
+ DataJson = GetString(map, DataJsonKey) ?? DataJson;
+ ATags = GetString(map, ATagsKey) ?? ATags;
+ AllowIDFAAccess = GetBool(map, AllowIdfaAccessKey) ?? AllowIDFAAccess;
+ RequestPermissionOnInit = GetBool(map, RequestPermissionOnInitKey) ?? RequestPermissionOnInit;
+ RewardedAutoLoad = GetBool(map, RewardedAutoLoadKey) ?? RewardedAutoLoad;
+ RewardedPrewarmOnInit = GetBool(map, RewardedPrewarmOnInitKey) ?? RewardedPrewarmOnInit;
+ RewardedMaxLoadAttempts = GetInt(map, RewardedMaxLoadAttemptsKey) ?? RewardedMaxLoadAttempts;
+ RewardedLoadRetryDelayMs = GetInt(map, RewardedLoadRetryDelayMsKey) ?? RewardedLoadRetryDelayMs;
+ RewardedShowTimeoutMs = GetInt(map, RewardedShowTimeoutMsKey) ?? RewardedShowTimeoutMs;
+ RewardName = GetString(map, RewardNameKey) ?? RewardName;
+ RewardAmount = GetInt(map, RewardAmountKey) ?? RewardAmount;
+ InterstitialAutoLoad = GetBool(map, InterstitialAutoLoadKey) ?? InterstitialAutoLoad;
+ InterstitialPrewarmOnInit = GetBool(map, InterstitialPrewarmOnInitKey) ?? InterstitialPrewarmOnInit;
+ InterstitialMaxLoadAttempts = GetInt(map, InterstitialMaxLoadAttemptsKey) ?? InterstitialMaxLoadAttempts;
+ InterstitialLoadRetryDelayMs = GetInt(map, InterstitialLoadRetryDelayMsKey) ?? InterstitialLoadRetryDelayMs;
+ InterstitialShowTimeoutMs = GetInt(map, InterstitialShowTimeoutMsKey) ?? InterstitialShowTimeoutMs;
+ SplashAutoLoad = GetBool(map, SplashAutoLoadKey) ?? SplashAutoLoad;
+ SplashPrewarmOnInit = GetBool(map, SplashPrewarmOnInitKey) ?? SplashPrewarmOnInit;
+ SplashMaxLoadAttempts = GetInt(map, SplashMaxLoadAttemptsKey) ?? SplashMaxLoadAttempts;
+ SplashLoadRetryDelayMs = GetInt(map, SplashLoadRetryDelayMsKey) ?? SplashLoadRetryDelayMs;
+ SplashShowTimeoutMs = GetInt(map, SplashShowTimeoutMsKey) ?? SplashShowTimeoutMs;
+ ExpressWidth = GetInt(map, ExpressWidthKey) ?? ExpressWidth;
+ ExpressHeight = GetInt(map, ExpressHeightKey) ?? ExpressHeight;
+ }
+
+ private static string GetString(IDictionary map, params string[] keys)
+ {
+ foreach (var key in keys)
+ {
+ if (!string.IsNullOrWhiteSpace(key) &&
+ map.TryGetValue(key, out var value) &&
+ !string.IsNullOrWhiteSpace(value))
+ {
+ return value.Trim();
+ }
+ }
+
+ return null;
+ }
+
+ private static bool? GetBool(IDictionary map, params string[] keys)
+ {
+ var value = GetString(map, keys);
+ return TryConvertBool(value, out var result) ? result : null;
+ }
+
+ private static int? GetInt(IDictionary map, params string[] keys)
+ {
+ var value = GetString(map, keys);
+ return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? result : (int?)null;
+ }
+
+ private static long? GetLong(IDictionary map, params string[] keys)
+ {
+ return ParseLong(GetString(map, keys));
+ }
+
+ private static long? ParseLong(string value)
+ {
+ return long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? result : (long?)null;
+ }
+
+ private static bool TryConvertBool(object value, out bool result)
+ {
+ switch (value)
+ {
+ case bool boolValue:
+ result = boolValue;
+ return true;
+ case string stringValue:
+ if (bool.TryParse(stringValue, out result))
+ {
+ return true;
+ }
+
+ if (string.Equals(stringValue, "1", StringComparison.OrdinalIgnoreCase))
+ {
+ result = true;
+ return true;
+ }
+
+ if (string.Equals(stringValue, "0", StringComparison.OrdinalIgnoreCase))
+ {
+ result = false;
+ return true;
+ }
+
+ break;
+ }
+
+ result = false;
+ return false;
+ }
+
+ private static string ConvertDictionaryValue(object value)
+ {
+ if (value == null)
+ {
+ return null;
+ }
+
+ if (value is string stringValue)
+ {
+ return stringValue;
+ }
+
+ if (value is IEnumerable enumerable)
+ {
+ var items = new List();
+ foreach (var item in enumerable)
+ {
+ if (item != null)
+ {
+ items.Add(item.ToString());
+ }
+ }
+
+ return string.Join(",", items.ToArray());
+ }
+
+ return value.ToString();
+ }
+}
diff --git a/Tapadn_Adapter/Runtime/Scripts/TapadnControllerOptions.cs.meta b/Tapadn_Adapter/Runtime/Scripts/TapadnControllerOptions.cs.meta
new file mode 100644
index 0000000..2670b5e
--- /dev/null
+++ b/Tapadn_Adapter/Runtime/Scripts/TapadnControllerOptions.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: cb9de9cc991b4649826dc9683c8ec78e
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file
diff --git a/Tapadn_Adapter/Runtime/Scripts/TapadnInteractionPlayer.cs b/Tapadn_Adapter/Runtime/Scripts/TapadnInteractionPlayer.cs
new file mode 100644
index 0000000..f276348
--- /dev/null
+++ b/Tapadn_Adapter/Runtime/Scripts/TapadnInteractionPlayer.cs
@@ -0,0 +1,137 @@
+using System;
+using Dirichlet.Mediation;
+using Runtime.ADAggregator;
+using UnityEngine;
+
+public sealed class TapadnInteractionPlayer : ADPlayer, IDirichletInterstitialAutoAdListener
+{
+ private DirichletAdNative _adNative;
+ private DirichletInterstitialAd _loadedAd;
+
+ public override int MaxLoadAttempts => TapadnAdController.CurrentOptions?.InterstitialMaxLoadAttempts ?? base.MaxLoadAttempts;
+ public override float LoadRetryDelaySeconds => Math.Max(0f, (TapadnAdController.CurrentOptions?.InterstitialLoadRetryDelayMs ?? 500) / 1000f);
+ public override float ShowPendingTimeoutSeconds => Math.Max(1f, (TapadnAdController.CurrentOptions?.InterstitialShowTimeoutMs ?? 20000) / 1000f);
+ public override bool AutoPreloadOnInit => TapadnAdController.CurrentOptions?.InterstitialPrewarmOnInit ?? false;
+
+ public override void OnInit()
+ {
+ _adNative = DirichletAdManager.CreateAdNative();
+ }
+
+ public override bool IsReadly()
+ {
+ if (UseAutoLoad())
+ {
+ return TapadnAdRequestFactory.TryParseSlotId(Key, out _);
+ }
+
+ if (_loadedAd != null && _loadedAd.IsLoaded && _loadedAd.IsValid)
+ {
+ curState = 2;
+ return true;
+ }
+
+ return false;
+ }
+
+ public override void LoadAD()
+ {
+ if (!TapadnAdRequestFactory.TryParseSlotId(Key, out _))
+ {
+ Debug.LogError($"[TapADN] Invalid interstitial slot id: {Key}");
+ curState = 0;
+ return;
+ }
+
+ if (UseAutoLoad())
+ {
+ curState = 2;
+ return;
+ }
+
+ if (curState == 1 || IsReadly())
+ {
+ return;
+ }
+
+ curState = 1;
+ _adNative.LoadInterstitialAd(
+ TapadnAdRequestFactory.BuildInterstitial(Key, TapadnAdController.CurrentOptions),
+ ad =>
+ {
+ _loadedAd?.Destroy();
+ _loadedAd = ad;
+ _loadedAd.Shown += OnManualShown;
+ _loadedAd.Clicked += OnManualClicked;
+ _loadedAd.Closed += OnManualClosed;
+ curState = 2;
+ Debug.Log($"[TapADN] Interstitial loaded. slot={Key}");
+ },
+ error =>
+ {
+ curState = 0;
+ Debug.LogError($"[TapADN] Interstitial load failed. code={error.Code}, message={error.Message}");
+ });
+ }
+
+ public override void ShowAD(Action onClose, Action onVideoComplete)
+ {
+ adListener.onClose = onClose;
+ adListener.onVideoComplete = onVideoComplete;
+ curState = 0;
+
+ if (UseAutoLoad())
+ {
+ _adNative.ShowInterstitialAutoAd(TapadnAdRequestFactory.BuildInterstitial(Key, TapadnAdController.CurrentOptions), this);
+ return;
+ }
+
+ if (_loadedAd == null || !_loadedAd.Show())
+ {
+ adListener.OnShowError();
+ }
+ }
+
+ public void OnError(DirichletError error)
+ {
+ curState = 0;
+ Debug.LogError($"[TapADN] Interstitial show failed. code={error?.Code}, message={error?.Message}");
+ adListener.OnShowError();
+ }
+
+ public void OnAdShow()
+ {
+ NotifyShowStarted();
+ }
+
+ public void OnAdClose()
+ {
+ adListener.OnAdClose();
+ adListener.OnShowComplete();
+ }
+
+ public void OnAdClick()
+ {
+ }
+
+ private bool UseAutoLoad()
+ {
+ return TapadnAdController.CurrentOptions?.InterstitialAutoLoad ?? true;
+ }
+
+ private void OnManualShown()
+ {
+ NotifyShowStarted();
+ }
+
+ private void OnManualClicked()
+ {
+ }
+
+ private void OnManualClosed()
+ {
+ _loadedAd?.Destroy();
+ _loadedAd = null;
+ OnAdClose();
+ }
+}
diff --git a/Tapadn_Adapter/Runtime/Scripts/TapadnInteractionPlayer.cs.meta b/Tapadn_Adapter/Runtime/Scripts/TapadnInteractionPlayer.cs.meta
new file mode 100644
index 0000000..02e391f
--- /dev/null
+++ b/Tapadn_Adapter/Runtime/Scripts/TapadnInteractionPlayer.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b422dcac50a0451a92815d54329fce8b
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file
diff --git a/Tapadn_Adapter/Runtime/Scripts/TapadnSplashPlayer.cs b/Tapadn_Adapter/Runtime/Scripts/TapadnSplashPlayer.cs
new file mode 100644
index 0000000..4dd9f24
--- /dev/null
+++ b/Tapadn_Adapter/Runtime/Scripts/TapadnSplashPlayer.cs
@@ -0,0 +1,137 @@
+using System;
+using Dirichlet.Mediation;
+using Runtime.ADAggregator;
+using UnityEngine;
+
+public sealed class TapadnSplashPlayer : ADPlayer, IDirichletSplashAutoAdListener
+{
+ private DirichletAdNative _adNative;
+ private DirichletSplashAd _loadedAd;
+
+ public override int MaxLoadAttempts => TapadnAdController.CurrentOptions?.SplashMaxLoadAttempts ?? base.MaxLoadAttempts;
+ public override float LoadRetryDelaySeconds => Math.Max(0f, (TapadnAdController.CurrentOptions?.SplashLoadRetryDelayMs ?? 500) / 1000f);
+ public override float ShowPendingTimeoutSeconds => Math.Max(1f, (TapadnAdController.CurrentOptions?.SplashShowTimeoutMs ?? 20000) / 1000f);
+ public override bool AutoPreloadOnInit => TapadnAdController.CurrentOptions?.SplashPrewarmOnInit ?? false;
+
+ public override void OnInit()
+ {
+ _adNative = DirichletAdManager.CreateAdNative();
+ }
+
+ public override bool IsReadly()
+ {
+ if (UseAutoLoad())
+ {
+ return TapadnAdRequestFactory.TryParseSlotId(Key, out _);
+ }
+
+ if (_loadedAd != null && _loadedAd.IsLoaded && _loadedAd.IsValid)
+ {
+ curState = 2;
+ return true;
+ }
+
+ return false;
+ }
+
+ public override void LoadAD()
+ {
+ if (!TapadnAdRequestFactory.TryParseSlotId(Key, out _))
+ {
+ Debug.LogError($"[TapADN] Invalid splash slot id: {Key}");
+ curState = 0;
+ return;
+ }
+
+ if (UseAutoLoad())
+ {
+ curState = 2;
+ return;
+ }
+
+ if (curState == 1 || IsReadly())
+ {
+ return;
+ }
+
+ curState = 1;
+ _adNative.LoadSplashAd(
+ TapadnAdRequestFactory.BuildSplash(Key, TapadnAdController.CurrentOptions),
+ ad =>
+ {
+ _loadedAd?.Destroy();
+ _loadedAd = ad;
+ _loadedAd.Shown += OnManualShown;
+ _loadedAd.Clicked += OnManualClicked;
+ _loadedAd.Closed += OnManualClosed;
+ curState = 2;
+ Debug.Log($"[TapADN] Splash loaded. slot={Key}");
+ },
+ error =>
+ {
+ curState = 0;
+ Debug.LogError($"[TapADN] Splash load failed. code={error.Code}, message={error.Message}");
+ });
+ }
+
+ public override void ShowAD(Action onClose, Action onVideoComplete)
+ {
+ adListener.onClose = onClose;
+ adListener.onVideoComplete = onVideoComplete;
+ curState = 0;
+
+ if (UseAutoLoad())
+ {
+ _adNative.ShowSplashAutoAd(TapadnAdRequestFactory.BuildSplash(Key, TapadnAdController.CurrentOptions), this);
+ return;
+ }
+
+ if (_loadedAd == null || !_loadedAd.Show())
+ {
+ adListener.OnShowError();
+ }
+ }
+
+ public void OnError(DirichletError error)
+ {
+ curState = 0;
+ Debug.LogError($"[TapADN] Splash show failed. code={error?.Code}, message={error?.Message}");
+ adListener.OnShowError();
+ }
+
+ public void OnAdShow()
+ {
+ NotifyShowStarted();
+ }
+
+ public void OnAdClose()
+ {
+ adListener.OnAdClose();
+ adListener.OnShowComplete();
+ }
+
+ public void OnAdClick()
+ {
+ }
+
+ private bool UseAutoLoad()
+ {
+ return TapadnAdController.CurrentOptions?.SplashAutoLoad ?? true;
+ }
+
+ private void OnManualShown()
+ {
+ NotifyShowStarted();
+ }
+
+ private void OnManualClicked()
+ {
+ }
+
+ private void OnManualClosed()
+ {
+ _loadedAd?.Destroy();
+ _loadedAd = null;
+ OnAdClose();
+ }
+}
diff --git a/Tapadn_Adapter/Runtime/Scripts/TapadnSplashPlayer.cs.meta b/Tapadn_Adapter/Runtime/Scripts/TapadnSplashPlayer.cs.meta
new file mode 100644
index 0000000..c272c6d
--- /dev/null
+++ b/Tapadn_Adapter/Runtime/Scripts/TapadnSplashPlayer.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 372f8c753ddd48d9a9e08012999a05f4
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..f2abc3e
--- /dev/null
+++ b/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "com.commercialization.tapadn",
+ "displayName": "Commercialization.tapadn",
+ "description": "TapADN / Dirichlet mediation implementation for CC-Framework.Commercialization.",
+ "version": "1.0.0",
+ "unity": "2021.1",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "http://private.lightyears.ltd:18650/foldcc/Commercialization.tapadn"
+ },
+ "author": {
+ "name": "foldcc",
+ "email": "lhyuau@qq.com",
+ "url": "https://gitee.com/foldcc"
+ },
+ "dependencies": {
+ "com.foldcc.cc-framework.commercialization": "http://private.lightyears.ltd:18650/foldcc/CC-Framework.Commercialization.git#1.0.14"
+ },
+ "samples": [
+ {
+ "displayName": "IAA Ad Debug Sample",
+ "description": "Optional debug scene/config helper for validating TapADN rewarded, interstitial, and splash flows.",
+ "path": "Samples~/IAAAdDebugSample"
+ }
+ ],
+ "keywords": [
+ "Framework",
+ "Commercialization",
+ "TapADN",
+ "Dirichlet"
+ ]
+}
diff --git a/package.json.meta b/package.json.meta
new file mode 100644
index 0000000..1558e02
--- /dev/null
+++ b/package.json.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: c3f50282dea34d8cbc525c5becdd3b91
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
\ No newline at end of file