2026-03-18 16:48:32 +08:00
|
|
|
using System;
|
|
|
|
|
using System.Collections;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Globalization;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using Runtime.ADAggregator;
|
|
|
|
|
|
|
|
|
|
public sealed class ToponControllerOptions
|
|
|
|
|
{
|
|
|
|
|
public const string ChannelKey = "topon.channel";
|
|
|
|
|
public const string SubChannelKey = "topon.sub_channel";
|
|
|
|
|
public const string DebugKey = "topon.debug";
|
|
|
|
|
public const string DebuggerKeyKey = "topon.debugger_key";
|
|
|
|
|
public const string SDKAreaKey = "topon.sdk_area";
|
|
|
|
|
public const string LongitudeKey = "topon.longitude";
|
|
|
|
|
public const string LatitudeKey = "topon.latitude";
|
|
|
|
|
public const string ExcludeBundleIdsKey = "topon.exclude_bundle_ids";
|
|
|
|
|
public const string RewardedExcludeAdSourceIdsKey = "topon.rewarded_exclude_ad_source_ids";
|
|
|
|
|
public const string QueryAreaOnInitKey = "topon.query_area_on_init";
|
2026-03-18 17:16:40 +08:00
|
|
|
public const string InitCustomMapKey = "topon.custom_map";
|
|
|
|
|
public const string RewardedCustomDataKey = "topon.rewarded_custom_data";
|
2026-03-18 16:48:32 +08:00
|
|
|
|
|
|
|
|
public string Channel { get; set; }
|
|
|
|
|
public string SubChannel { get; set; }
|
|
|
|
|
public bool? Debug { get; set; }
|
|
|
|
|
public string DebuggerKey { get; set; }
|
|
|
|
|
public int? SDKArea { get; set; }
|
|
|
|
|
public double? Longitude { get; set; }
|
|
|
|
|
public double? Latitude { get; set; }
|
|
|
|
|
public string[] ExcludeBundleIds { get; set; }
|
|
|
|
|
public string[] RewardedExcludeAdSourceIds { get; set; }
|
|
|
|
|
public bool QueryAreaOnInit { get; set; }
|
2026-03-18 17:16:40 +08:00
|
|
|
public Dictionary<string, string> InitCustomMap { get; set; }
|
|
|
|
|
public Dictionary<string, string> RewardedCustomData { get; set; }
|
2026-03-18 16:48:32 +08:00
|
|
|
public Action<string> OnAreaReceived { get; set; }
|
|
|
|
|
public Action<string> OnAreaError { get; set; }
|
|
|
|
|
|
|
|
|
|
public static ToponControllerOptions Resolve(ADConfig adConfig, object[] args)
|
|
|
|
|
{
|
|
|
|
|
var options = new ToponControllerOptions();
|
|
|
|
|
|
|
|
|
|
options.ApplyCommonKeyValues(adConfig?.CommonKeyValues);
|
|
|
|
|
options.ApplyLegacyArgs(args);
|
|
|
|
|
|
|
|
|
|
if (args == null)
|
|
|
|
|
{
|
|
|
|
|
return options;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var arg in args)
|
|
|
|
|
{
|
|
|
|
|
switch (arg)
|
|
|
|
|
{
|
|
|
|
|
case ToponControllerOptions explicitOptions:
|
|
|
|
|
options.ApplyExplicitOptions(explicitOptions);
|
|
|
|
|
break;
|
|
|
|
|
case IDictionary dictionary:
|
|
|
|
|
options.ApplyDictionary(dictionary);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return options;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ApplyCommonKeyValues(List<AdKeyValue> keyValues)
|
|
|
|
|
{
|
|
|
|
|
if (keyValues == null || keyValues.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
foreach (var keyValue in keyValues)
|
|
|
|
|
{
|
|
|
|
|
if (keyValue == null || string.IsNullOrWhiteSpace(keyValue.key))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
map[keyValue.key] = keyValue.value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Channel = GetString(map, ChannelKey, "channel") ?? Channel;
|
|
|
|
|
SubChannel = GetString(map, SubChannelKey, "sub_channel") ?? SubChannel;
|
|
|
|
|
Debug = GetBool(map, DebugKey, "debug") ?? Debug;
|
|
|
|
|
DebuggerKey = GetString(map, DebuggerKeyKey) ?? DebuggerKey;
|
|
|
|
|
SDKArea = GetInt(map, SDKAreaKey) ?? SDKArea;
|
|
|
|
|
Longitude = GetDouble(map, LongitudeKey) ?? Longitude;
|
|
|
|
|
Latitude = GetDouble(map, LatitudeKey) ?? Latitude;
|
|
|
|
|
ExcludeBundleIds = GetStringArray(map, ExcludeBundleIdsKey) ?? ExcludeBundleIds;
|
|
|
|
|
RewardedExcludeAdSourceIds = GetStringArray(map, RewardedExcludeAdSourceIdsKey) ?? RewardedExcludeAdSourceIds;
|
|
|
|
|
QueryAreaOnInit = GetBool(map, QueryAreaOnInitKey) ?? QueryAreaOnInit;
|
2026-03-18 17:16:40 +08:00
|
|
|
InitCustomMap = MergeMaps(InitCustomMap, GetPrefixedMap(map, InitCustomMapKey + "."));
|
|
|
|
|
RewardedCustomData = MergeMaps(RewardedCustomData, GetPrefixedMap(map, RewardedCustomDataKey + "."));
|
2026-03-18 16:48:32 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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(ToponControllerOptions explicitOptions)
|
|
|
|
|
{
|
|
|
|
|
if (explicitOptions == null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Channel = explicitOptions.Channel ?? Channel;
|
|
|
|
|
SubChannel = explicitOptions.SubChannel ?? SubChannel;
|
|
|
|
|
Debug = explicitOptions.Debug ?? Debug;
|
|
|
|
|
DebuggerKey = explicitOptions.DebuggerKey ?? DebuggerKey;
|
|
|
|
|
SDKArea = explicitOptions.SDKArea ?? SDKArea;
|
|
|
|
|
Longitude = explicitOptions.Longitude ?? Longitude;
|
|
|
|
|
Latitude = explicitOptions.Latitude ?? Latitude;
|
|
|
|
|
ExcludeBundleIds = explicitOptions.ExcludeBundleIds ?? ExcludeBundleIds;
|
|
|
|
|
RewardedExcludeAdSourceIds = explicitOptions.RewardedExcludeAdSourceIds ?? RewardedExcludeAdSourceIds;
|
|
|
|
|
QueryAreaOnInit = explicitOptions.QueryAreaOnInit || QueryAreaOnInit;
|
2026-03-18 17:16:40 +08:00
|
|
|
InitCustomMap = MergeMaps(InitCustomMap, explicitOptions.InitCustomMap);
|
|
|
|
|
RewardedCustomData = MergeMaps(RewardedCustomData, explicitOptions.RewardedCustomData);
|
2026-03-18 16:48:32 +08:00
|
|
|
OnAreaReceived = explicitOptions.OnAreaReceived ?? OnAreaReceived;
|
|
|
|
|
OnAreaError = explicitOptions.OnAreaError ?? OnAreaError;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ApplyDictionary(IDictionary dictionary)
|
|
|
|
|
{
|
|
|
|
|
if (dictionary == null || dictionary.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
foreach (DictionaryEntry entry in dictionary)
|
|
|
|
|
{
|
|
|
|
|
if (entry.Key == null)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 17:16:40 +08:00
|
|
|
var key = entry.Key.ToString();
|
|
|
|
|
if (entry.Value is IDictionary nestedDictionary)
|
|
|
|
|
{
|
|
|
|
|
if (string.Equals(key, InitCustomMapKey, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
InitCustomMap = MergeMaps(InitCustomMap, ToStringMap(nestedDictionary));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (string.Equals(key, RewardedCustomDataKey, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
RewardedCustomData = MergeMaps(RewardedCustomData, ToStringMap(nestedDictionary));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
map[key] = ConvertDictionaryValue(entry.Value);
|
2026-03-18 16:48:32 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Channel = GetString(map, ChannelKey, "channel") ?? Channel;
|
|
|
|
|
SubChannel = GetString(map, SubChannelKey, "sub_channel") ?? SubChannel;
|
|
|
|
|
Debug = GetBool(map, DebugKey, "debug") ?? Debug;
|
|
|
|
|
DebuggerKey = GetString(map, DebuggerKeyKey) ?? DebuggerKey;
|
|
|
|
|
SDKArea = GetInt(map, SDKAreaKey) ?? SDKArea;
|
|
|
|
|
Longitude = GetDouble(map, LongitudeKey) ?? Longitude;
|
|
|
|
|
Latitude = GetDouble(map, LatitudeKey) ?? Latitude;
|
|
|
|
|
ExcludeBundleIds = GetStringArray(map, ExcludeBundleIdsKey) ?? ExcludeBundleIds;
|
|
|
|
|
RewardedExcludeAdSourceIds = GetStringArray(map, RewardedExcludeAdSourceIdsKey) ?? RewardedExcludeAdSourceIds;
|
|
|
|
|
QueryAreaOnInit = GetBool(map, QueryAreaOnInitKey) ?? QueryAreaOnInit;
|
2026-03-18 17:16:40 +08:00
|
|
|
InitCustomMap = MergeMaps(InitCustomMap, GetPrefixedMap(map, InitCustomMapKey + "."));
|
|
|
|
|
RewardedCustomData = MergeMaps(RewardedCustomData, GetPrefixedMap(map, RewardedCustomDataKey + "."));
|
2026-03-18 16:48:32 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string GetString(IDictionary<string, string> 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<string, string> map, params string[] keys)
|
|
|
|
|
{
|
|
|
|
|
var value = GetString(map, keys);
|
|
|
|
|
return TryConvertBool(value, out var result) ? result : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static int? GetInt(IDictionary<string, string> map, params string[] keys)
|
|
|
|
|
{
|
|
|
|
|
var value = GetString(map, keys);
|
|
|
|
|
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
|
|
|
|
|
{
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static double? GetDouble(IDictionary<string, string> map, params string[] keys)
|
|
|
|
|
{
|
|
|
|
|
var value = GetString(map, keys);
|
|
|
|
|
if (double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture,
|
|
|
|
|
out var result))
|
|
|
|
|
{
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string[] GetStringArray(IDictionary<string, string> map, params string[] keys)
|
|
|
|
|
{
|
|
|
|
|
var value = GetString(map, keys);
|
|
|
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var normalized = value.Replace("[", string.Empty)
|
|
|
|
|
.Replace("]", string.Empty)
|
|
|
|
|
.Replace("\"", string.Empty)
|
|
|
|
|
.Replace("'", string.Empty);
|
|
|
|
|
|
|
|
|
|
var items = normalized
|
|
|
|
|
.Split(new[] { ',', ';', '|', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
|
|
|
|
.Select(item => item.Trim())
|
|
|
|
|
.Where(item => !string.IsNullOrWhiteSpace(item))
|
|
|
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
|
|
|
.ToArray();
|
|
|
|
|
|
|
|
|
|
return items.Length > 0 ? items : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 17:16:40 +08:00
|
|
|
private static Dictionary<string, string> GetPrefixedMap(IDictionary<string, string> map, string prefix)
|
|
|
|
|
{
|
|
|
|
|
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
foreach (var pair in map)
|
|
|
|
|
{
|
|
|
|
|
if (!pair.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var nestedKey = pair.Key.Substring(prefix.Length).Trim();
|
|
|
|
|
if (string.IsNullOrWhiteSpace(nestedKey) || string.IsNullOrWhiteSpace(pair.Value))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result[nestedKey] = pair.Value.Trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result.Count > 0 ? result : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static Dictionary<string, string> MergeMaps(
|
|
|
|
|
Dictionary<string, string> current,
|
|
|
|
|
Dictionary<string, string> incoming)
|
|
|
|
|
{
|
|
|
|
|
if (incoming == null || incoming.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
return current;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result = current != null
|
|
|
|
|
? new Dictionary<string, string>(current, StringComparer.OrdinalIgnoreCase)
|
|
|
|
|
: new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
|
|
|
|
|
foreach (var pair in incoming)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(pair.Key) || string.IsNullOrWhiteSpace(pair.Value))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result[pair.Key] = pair.Value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result.Count > 0 ? result : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:48:32 +08:00
|
|
|
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<string>();
|
|
|
|
|
foreach (var item in enumerable)
|
|
|
|
|
{
|
|
|
|
|
if (item == null)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
items.Add(item.ToString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return string.Join(",", items);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return value.ToString();
|
|
|
|
|
}
|
2026-03-18 17:16:40 +08:00
|
|
|
|
|
|
|
|
private static Dictionary<string, string> ToStringMap(IDictionary dictionary)
|
|
|
|
|
{
|
|
|
|
|
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
foreach (DictionaryEntry entry in dictionary)
|
|
|
|
|
{
|
|
|
|
|
if (entry.Key == null || entry.Value == null)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var key = entry.Key.ToString()?.Trim();
|
|
|
|
|
var value = entry.Value.ToString()?.Trim();
|
|
|
|
|
if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(value))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result[key] = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result.Count > 0 ? result : null;
|
|
|
|
|
}
|
2026-03-18 16:48:32 +08:00
|
|
|
}
|