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