petsHero-AI/lib/core/referrer/referrer_service.dart
2026-05-08 19:21:17 +08:00

289 lines
10 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String?> _completer = Completer<String?>();
/// attribution 回调的 Completer与 getAttributionWithTimeout 竞速,谁先返回用谁
static final Completer<AdjustAttribution?> _attributionCallbackCompleter =
Completer<AdjustAttribution?>();
/// 过长会拖慢 [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_adjustPlay 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 = <String, dynamic>{
'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<String?> getReferrer() async {
if (_cachedReferrer != null) return _cachedReferrer;
if (_completer.isCompleted) return _completer.future;
var digest = '';
try {
// 竞速getAttributionWithTimeout 与 attribution 回调,谁先返回用谁
final attribution = await Future.any<AdjustAttribution?>([
(() 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<void> init() async {
await getReferrer();
}
/// 仅 Adjust 归因 digestBase64 JSON用于 /v1/user/referrer accolade=android_adjust
static Future<String> 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<String> 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<void> startOrganicPolling({
required Future<void> 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<void> 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<void> 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 ?? '<null>'}" 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<bool> _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<String?> _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<String> _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;
}
}