import 'dart:async'; import 'dart:convert'; import 'package:adjust_sdk/adjust_attribution.dart'; import 'package:adjust_sdk/adjust.dart'; import 'package:flutter/foundation.dart'; import 'package:play_install_referrer/play_install_referrer.dart'; import '../log/app_logger.dart'; /// 归因信息服务(优先从 Adjust 获取,fallback 使用 Play Install Referrer) class ReferrerService { ReferrerService._(); static final _log = AppLogger('ReferrerService'); static String? _cachedReferrer; static String _referrerSource = 'gg'; static final Completer _completer = Completer(); /// attribution 回调的 Completer,与 getAttributionWithTimeout 竞速,谁先返回用谁 static final Completer _attributionCallbackCompleter = Completer(); /// 过长会拖慢 [AuthService.init] 内首次 getReferrer;过短可能拿不到 Adjust,仍有 Play Install Referrer fallback static const int _adjustTimeoutMs = 4500; /// 与 [Future.any] 外层兜底,避免回调 Future 永不完成时永远挂起 static const Duration _adjustRaceTimeout = Duration(seconds: 6); /// Play Install Referrer 读取超时(部分机型/系统上可能极慢) static const Duration _playInstallReferrerTimeout = Duration(seconds: 10); /// 自然量轮询:每次检查间隔 static const Duration _organicPollInterval = Duration(seconds: 5); /// 自然量轮询:最长持续时间(达到后无论是否仍是自然量都停止) static const Duration _organicPollMaxDuration = Duration(minutes: 5); static bool _organicPollRunning = false; static Timer? _organicPollTimer; /// 由 Adjust attributionCallback 调用,首次安装时归因往往通过此回调返回(晚于 getAttributionWithTimeout) static void receiveAttributionFromCallback(AdjustAttribution attribution) { if (!_attributionCallbackCompleter.isCompleted) { _attributionCallbackCompleter.complete(attribution); } } /// 归因来源:Adjust 为 android_adjust,Play Install Referrer 为 gg static String get referrerSource => _referrerSource; /// JSON 可编码的 costAmount:保留字段,非有限数字用字符串避免 encode 抛错 static Object? _jsonEncodableCostAmount(num? v) { if (v == null) return null; if (v is double && !v.isFinite) return v.toString(); return v; } /// 将完整 AdjustAttribution 序列化为 JSON(供 digest / Base64),不省略任何键 static String _attributionToDigest(AdjustAttribution attr) { final map = { 'trackerToken': attr.trackerToken, 'trackerName': attr.trackerName, 'network': attr.network, 'campaign': attr.campaign, 'adgroup': attr.adgroup, 'creative': attr.creative, 'clickLabel': attr.clickLabel, 'costType': attr.costType, 'costAmount': _jsonEncodableCostAmount(attr.costAmount), 'costCurrency': attr.costCurrency, 'jsonResponse': attr.jsonResponse, 'fbInstallReferrer': attr.fbInstallReferrer, }; try { return jsonEncode(map); } catch (_) { return ''; } } /// 获取 referrer/digest,优先从 Adjust 归因获取,fallback 使用 Play Install Referrer /// 同时监听 attributionCallback,首次安装时归因往往通过回调返回(可能晚于 getAttributionWithTimeout) static Future getReferrer() async { if (_cachedReferrer != null) return _cachedReferrer; if (_completer.isCompleted) return _completer.future; var digest = ''; try { // 竞速:getAttributionWithTimeout 与 attribution 回调,谁先返回用谁 final attribution = await Future.any([ (() async { try { return await Adjust.getAttributionWithTimeout(_adjustTimeoutMs); } catch (_) { return null; } })(), _attributionCallbackCompleter.future, ]).timeout(_adjustRaceTimeout, onTimeout: () => null); if (attribution != null) { final raw = _attributionToDigest(attribution); if (raw.isNotEmpty) { _referrerSource = 'android_adjust'; digest = base64Encode(utf8.encode(raw)); } } } catch (_) { digest = ''; } if (digest.isEmpty) { _referrerSource = 'gg'; if (defaultTargetPlatform == TargetPlatform.android) { try { final details = await PlayInstallReferrer.installReferrer .timeout(_playInstallReferrerTimeout); digest = details.installReferrer ?? ''; } catch (_) { digest = ''; } } } _cachedReferrer = digest; if (!_completer.isCompleted) _completer.complete(_cachedReferrer); return _cachedReferrer; } /// 初始化并缓存 referrer(建议在 app 启动时调用) static Future init() async { await getReferrer(); } /// 仅 Adjust 归因 digest(Base64 JSON),用于 /v1/user/referrer accolade=android_adjust static Future getAdjustReferrerDigest() async { try { final attr = await Adjust.getAttribution(); final raw = _attributionToDigest(attr); if (raw.isEmpty) return ''; return base64Encode(utf8.encode(raw)); } catch (_) { return ''; } } /// 仅 Google Play Install Referrer 字符串,用于 /v1/user/referrer accolade=gg static Future getGgReferrerDigest() async { if (defaultTargetPlatform != TargetPlatform.android) return ''; try { final details = await PlayInstallReferrer.installReferrer .timeout(_playInstallReferrerTimeout); return details.installReferrer ?? ''; } catch (_) { return ''; } } /// 启动“自然量→渠道”轮询:仅当 Adjust 与 Play Install Referrer 当前都判定为自然量时开始; /// 之后每 [_organicPollInterval] 重新判定一次,直到任一渠道变为非自然量或超过 /// [_organicPollMaxDuration]。结束时调用 [onStopped],参数表示是否检测到非自然量。 /// /// 多次调用是幂等的:已在轮询时直接返回。 static Future startOrganicPolling({ required Future Function(bool detectedNonOrganic) onStopped, }) async { if (_organicPollRunning) { _log.d('startOrganicPolling: already running, skip'); return; } final initiallyOrganic = await _isCurrentlyOrganic(); if (!initiallyOrganic) { _log.d('startOrganicPolling: 初始归因即为渠道,无需轮询'); return; } _organicPollRunning = true; final startedAt = DateTime.now(); var tickCount = 0; _log.d( 'startOrganicPolling: 启动自然量轮询,每 ${_organicPollInterval.inSeconds}s 一次,最长 ${_organicPollMaxDuration.inMinutes}min'); Future finish(bool detectedNonOrganic, {required String reason}) async { _stopOrganicPoll(); _log.d('startOrganicPolling: 结束,原因=$reason changed=$detectedNonOrganic'); try { await onStopped(detectedNonOrganic); } catch (e, st) { _log.d('startOrganicPolling: onStopped 回调异常: $e'); _log.d('startOrganicPolling: stack $st'); } } Future tick() async { if (!_organicPollRunning) return; tickCount++; final elapsed = DateTime.now().difference(startedAt); if (elapsed >= _organicPollMaxDuration) { await finish(false, reason: 'timeout'); return; } final adjustNetwork = await _readAdjustNetwork(); final adjustOrganic = _attributionIsOrganicByNetwork(adjustNetwork); final pirRef = await _readPlayInstallReferrerRaw(); final pirOrganic = _installReferrerIsOrganic(pirRef); final stillOrganic = adjustOrganic && pirOrganic; _log.d( 'organic poll tick#$tickCount elapsed=${elapsed.inSeconds}s ' 'adjustNetwork="${adjustNetwork ?? ''}" adjustOrganic=$adjustOrganic ' 'pir="$pirRef" pirOrganic=$pirOrganic stillOrganic=$stillOrganic'); if (!_organicPollRunning) return; if (!stillOrganic) { await finish(true, reason: 'detectedNonOrganic'); return; } _organicPollTimer = Timer(_organicPollInterval, () { if (!_organicPollRunning) return; unawaited(tick()); }); } _organicPollTimer = Timer(_organicPollInterval, () { if (!_organicPollRunning) return; unawaited(tick()); }); } static void _stopOrganicPoll() { _organicPollTimer?.cancel(); _organicPollTimer = null; _organicPollRunning = false; } /// 当 Adjust 归因和 Play Install Referrer 都判定为自然量时返回 true static Future _isCurrentlyOrganic() async { final adjustNetwork = await _readAdjustNetwork(); final adjustOrganic = _attributionIsOrganicByNetwork(adjustNetwork); final pirRef = await _readPlayInstallReferrerRaw(); final pirOrganic = _installReferrerIsOrganic(pirRef); return adjustOrganic && pirOrganic; } /// 读取 Adjust 当前归因的 network 字段(异常返回 null,按未归因处理) static Future _readAdjustNetwork() async { try { final attr = await Adjust.getAttribution(); return attr.network; } catch (_) { return null; } } /// network 为空 / 'organic' 视为自然量(含未归因情况) static bool _attributionIsOrganicByNetwork(String? network) { final n = (network ?? '').trim().toLowerCase(); return n.isEmpty || n == 'organic'; } /// 读取 Play Install Referrer 原始字符串(非 Android / 异常返回空串) static Future _readPlayInstallReferrerRaw() async { if (defaultTargetPlatform != TargetPlatform.android) return ''; try { final details = await PlayInstallReferrer.installReferrer .timeout(_playInstallReferrerTimeout); return details.installReferrer ?? ''; } catch (_) { return ''; } } /// Google Play 自然量典型 referrer:`utm_source=google-play&utm_medium=organic`; /// 空串/缺少 `utm_medium=` 视为尚未归因,按自然量处理。 static bool _installReferrerIsOrganic(String ref) { final s = ref.trim(); if (s.isEmpty) return true; final lower = s.toLowerCase(); if (lower.contains('utm_medium=organic')) return true; if (!lower.contains('utm_medium=')) return true; return false; } }