591 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Dart
		
	
	
		
		
			
		
	
	
			591 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Dart
		
	
	
| 
								 | 
							
								import 'dart:async';
							 | 
						|||
| 
								 | 
							
								import 'dart:collection';
							 | 
						|||
| 
								 | 
							
								import 'dart:convert';
							 | 
						|||
| 
								 | 
							
								import 'dart:io';
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								import 'package:flutter/services.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_analytics_flutter/events_constants.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_analytics_flutter/events_constants.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_app/account/account_data_store.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_app/analytics/guru_analytics.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_app/firebase/remoteconfig/remote_config_manager.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_app/guru_app.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_app/property/app_property.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_app/property/settings/guru_settings.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_app/utils/guru_file_utils_extension.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_utils/core/ext.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_utils/file/file_utils.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_utils/log/log.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_utils/property/runtime_property.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_utils/quiver/cache.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_utils/quiver/collection.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_utils/settings/settings.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_utils/tuple/tuple.dart';
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								abstract class EventMatcher {
							 | 
						|||
| 
								 | 
							
								  bool match(String eventName);
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								class UniversalMatcher extends EventMatcher {
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  bool match(String eventName) => true;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  String toString() {
							 | 
						|||
| 
								 | 
							
								    return 'UniversalMatcher';
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								class RegexMatcher extends EventMatcher {
							 | 
						|||
| 
								 | 
							
								  final RegExp re;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  RegexMatcher(String pattern) : re = RegExp(pattern);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  bool match(String eventName) => re.hasMatch(eventName);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  String toString() {
							 | 
						|||
| 
								 | 
							
								    return 'RegexMatcher:${re.pattern}';
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								class WildcardMatcher extends RegexMatcher {
							 | 
						|||
| 
								 | 
							
								  final String wildcard;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  WildcardMatcher(this.wildcard) : super("^${wildcard.replaceAll("*", ".*")}\$");
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  bool match(String eventName) => super.match(eventName);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  String toString() {
							 | 
						|||
| 
								 | 
							
								    return 'WildcardMatcher:$wildcard => ${re.pattern}';
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								abstract class StrategyValidator {
							 | 
						|||
| 
								 | 
							
								  bool get alwaysVerify => false;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  const StrategyValidator();
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  bool validate();
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								class UnlimitedValidator extends StrategyValidator {
							 | 
						|||
| 
								 | 
							
								  const UnlimitedValidator();
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  bool validate() => true;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  String toString() {
							 | 
						|||
| 
								 | 
							
								    return 'UnlimitedValidator';
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								class DisabledValidator extends StrategyValidator {
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  bool validate() => false;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  String toString() {
							 | 
						|||
| 
								 | 
							
								    return 'DisabledValidator';
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								class PlatformValidator extends StrategyValidator {
							 | 
						|||
| 
								 | 
							
								  final String platform;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  PlatformValidator(this.platform);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  bool validate() => Platform.isAndroid ? platform == "android" : platform == "ios";
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  String toString() {
							 | 
						|||
| 
								 | 
							
								    return 'PlatformValidator($platform)';
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								class CountryCodeValidator extends StrategyValidator {
							 | 
						|||
| 
								 | 
							
								  final Set<String> included;
							 | 
						|||
| 
								 | 
							
								  final Set<String> excluded;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  CountryCodeValidator(this.included, this.excluded);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  bool validate() {
							 | 
						|||
| 
								 | 
							
								    final countryCode = AccountDataStore.instance.countryCode;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    // 如果excluded不为空,证明存在排除选项,该validate将只判断所有excluded中的逻辑,
							 | 
						|||
| 
								 | 
							
								    // 将不会在判断included中的逻辑
							 | 
						|||
| 
								 | 
							
								    if (excluded.isNotEmpty) {
							 | 
						|||
| 
								 | 
							
								      return !excluded.contains(countryCode);
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    if (included.contains(countryCode)) {
							 | 
						|||
| 
								 | 
							
								      return true;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								    return false;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  String toString() {
							 | 
						|||
| 
								 | 
							
								    return 'CountryCodeValidator{included: $included, excluded: $excluded}';
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								class UserPropertyValidator extends StrategyValidator {
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  bool get alwaysVerify => true;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  final List<Tuple2<String, String>> validProperties;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  UserPropertyValidator(this.validProperties);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  bool validate() {
							 | 
						|||
| 
								 | 
							
								    for (var tuple in validProperties) {
							 | 
						|||
| 
								 | 
							
								      if (GuruAnalytics.instance.getProperty(tuple.item1) != tuple.item2) {
							 | 
						|||
| 
								 | 
							
								        return false;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								    return true;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  String toString() {
							 | 
						|||
| 
								 | 
							
								    return 'UserPropertyValidator{validProperties: $validProperties}';
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								class RandomValidator extends StrategyValidator {
							 | 
						|||
| 
								 | 
							
								  final int percent;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  RandomValidator(int percent) : percent = percent.clamp(10, 90);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  bool validate() {
							 | 
						|||
| 
								 | 
							
								    final firstInstallTime =
							 | 
						|||
| 
								 | 
							
								        RuntimeProperty.instance.getInt(UtilsSettingsKeys.firstInstallTime, defValue: -1);
							 | 
						|||
| 
								 | 
							
								    return (firstInstallTime % 9) >= (percent ~/ 10 - 1);
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  String toString() {
							 | 
						|||
| 
								 | 
							
								    return 'RandomValidator{percent: $percent}';
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								class VersionValidator extends StrategyValidator {
							 | 
						|||
| 
								 | 
							
								  final String opt;
							 | 
						|||
| 
								 | 
							
								  final String buildId;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  VersionValidator(this.opt, this.buildId);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  bool validate() {
							 | 
						|||
| 
								 | 
							
								    final buildNumber = GuruSettings.instance.buildNumber.get();
							 | 
						|||
| 
								 | 
							
								    switch (opt) {
							 | 
						|||
| 
								 | 
							
								      case "ve":
							 | 
						|||
| 
								 | 
							
								        return buildNumber == buildId;
							 | 
						|||
| 
								 | 
							
								      case "vg":
							 | 
						|||
| 
								 | 
							
								        return buildNumber.compareTo(buildId) > 0;
							 | 
						|||
| 
								 | 
							
								      case "vge":
							 | 
						|||
| 
								 | 
							
								        return buildNumber.compareTo(buildId) >= 0;
							 | 
						|||
| 
								 | 
							
								      case "vl":
							 | 
						|||
| 
								 | 
							
								        return buildNumber.compareTo(buildId) < 0;
							 | 
						|||
| 
								 | 
							
								      case "vle":
							 | 
						|||
| 
								 | 
							
								        return buildNumber.compareTo(buildId) <= 0;
							 | 
						|||
| 
								 | 
							
								      default:
							 | 
						|||
| 
								 | 
							
								        return false;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  String toString() {
							 | 
						|||
| 
								 | 
							
								    return 'VersionValidator{opt: $opt, buildId: $buildId}';
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								class StrategyRuleTypeException implements Exception {
							 | 
						|||
| 
								 | 
							
								  final String message;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  StrategyRuleTypeException([this.message = "Type mismatch: Expected a StrategyRuleItem."]);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  String toString() => "StrategyRuleTypeException: $message";
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								class StrategyRule {
							 | 
						|||
| 
								 | 
							
								  final EventMatcher? matcher;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  final StrategyValidator validator;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  final AppEventCapabilities appEventCapabilities;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  final String? adjustToken;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  AppEventOptions? _options;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  StrategyRule(this.validator, this.appEventCapabilities, {this.matcher, this.adjustToken});
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  AppEventOptions? getAppEventOptions() {
							 | 
						|||
| 
								 | 
							
								    if ((_options != null && !validator.alwaysVerify) || validator.validate()) {
							 | 
						|||
| 
								 | 
							
								      return _options ??= AppEventOptions(capabilities: appEventCapabilities);
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								    return null;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  String toString() {
							 | 
						|||
| 
								 | 
							
								    return 'StrategyRule{matcher: $matcher, validator: $validator, appEventCapabilities: $appEventCapabilities, adjustToken: $adjustToken}';
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								class StrategyRuleParser {
							 | 
						|||
| 
								 | 
							
								  static final invalidWildcardReg = RegExp(r'[^a-zA-Z=0-9_*]');
							 | 
						|||
| 
								 | 
							
								  static final adjustTokenReg = RegExp(r'^[a-z0-9]{6}$');
							 | 
						|||
| 
								 | 
							
								  static final randomStrategyReg = RegExp(r'^r([1-9]0)$');
							 | 
						|||
| 
								 | 
							
								  static final userPropertyStrategyReg = RegExp(r'^up:(.+)=(.+)$');
							 | 
						|||
| 
								 | 
							
								  static final versionStrategyReg = RegExp(r'^(ve|vg|vl|vge|vle)(\d{8})$');
							 | 
						|||
| 
								 | 
							
								  static final countryStrategyReg = RegExp(r'^cc:(.+)$');
							 | 
						|||
| 
								 | 
							
								  static final countryCodeValidReg = RegExp(r'^[a-z]{2}|\![a-z]{2}$');
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  final List<String> fields;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  StrategyRuleParser(this.fields);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  EventMatcher? createEventMatcher(String event) {
							 | 
						|||
| 
								 | 
							
								    if (event == "_all_") {
							 | 
						|||
| 
								 | 
							
								      return UniversalMatcher();
							 | 
						|||
| 
								 | 
							
								    } else if (!invalidWildcardReg.hasMatch(event)) {
							 | 
						|||
| 
								 | 
							
								      if (event.contains("*")) {
							 | 
						|||
| 
								 | 
							
								        return WildcardMatcher(event);
							 | 
						|||
| 
								 | 
							
								      } else {
							 | 
						|||
| 
								 | 
							
								        // 返回空的话,表示精确匹配,无需提供matcher
							 | 
						|||
| 
								 | 
							
								        return null;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								    } else {
							 | 
						|||
| 
								 | 
							
								      return RegexMatcher(event);
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  StrategyValidator? createStrategyValidator(String strategy) {
							 | 
						|||
| 
								 | 
							
								    if (strategy == "unlimited") {
							 | 
						|||
| 
								 | 
							
								      return const UnlimitedValidator();
							 | 
						|||
| 
								 | 
							
								    } else if (strategy == "disabled") {
							 | 
						|||
| 
								 | 
							
								      return DisabledValidator();
							 | 
						|||
| 
								 | 
							
								    } else if (strategy == "android" || strategy == "ios") {
							 | 
						|||
| 
								 | 
							
								      return PlatformValidator(strategy);
							 | 
						|||
| 
								 | 
							
								    } else {
							 | 
						|||
| 
								 | 
							
								      final randomMatch = randomStrategyReg.firstMatch(strategy);
							 | 
						|||
| 
								 | 
							
								      final randomPercent = randomMatch?.group(1);
							 | 
						|||
| 
								 | 
							
								      if (!DartExt.isBlank(randomPercent)) {
							 | 
						|||
| 
								 | 
							
								        return RandomValidator(int.parse(randomPercent!));
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      final userPropertyMatch = userPropertyStrategyReg.firstMatch(strategy);
							 | 
						|||
| 
								 | 
							
								      final userPropertyKey = userPropertyMatch?.group(1);
							 | 
						|||
| 
								 | 
							
								      final userPropertyValue = userPropertyMatch?.group(2);
							 | 
						|||
| 
								 | 
							
								      if (!DartExt.isBlank(userPropertyKey) && !DartExt.isBlank(userPropertyValue)) {
							 | 
						|||
| 
								 | 
							
								        return UserPropertyValidator([Tuple2(userPropertyKey!, userPropertyValue!)]);
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      final versionMatch = versionStrategyReg.firstMatch(strategy);
							 | 
						|||
| 
								 | 
							
								      final versionOpt = versionMatch?.group(1);
							 | 
						|||
| 
								 | 
							
								      final versionBuildId = versionMatch?.group(2);
							 | 
						|||
| 
								 | 
							
								      if (!DartExt.isBlank(versionOpt) && !DartExt.isBlank(versionBuildId)) {
							 | 
						|||
| 
								 | 
							
								        return VersionValidator(versionOpt!, versionBuildId!);
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      final countryCodeMatch = countryStrategyReg.firstMatch(strategy);
							 | 
						|||
| 
								 | 
							
								      final countryCodeExpression = countryCodeMatch?.group(1);
							 | 
						|||
| 
								 | 
							
								      if (!DartExt.isBlank(countryCodeExpression)) {
							 | 
						|||
| 
								 | 
							
								        final included = <String>{};
							 | 
						|||
| 
								 | 
							
								        final excluded = <String>{};
							 | 
						|||
| 
								 | 
							
								        final countryCodes = countryCodeExpression!
							 | 
						|||
| 
								 | 
							
								            .split("|")
							 | 
						|||
| 
								 | 
							
								            .where((cc) => countryCodeValidReg.hasMatch(cc))
							 | 
						|||
| 
								 | 
							
								            .toSet();
							 | 
						|||
| 
								 | 
							
								        for (var cc in countryCodes) {
							 | 
						|||
| 
								 | 
							
								          if (cc.startsWith("!")) {
							 | 
						|||
| 
								 | 
							
								            excluded.add(cc.substring(1));
							 | 
						|||
| 
								 | 
							
								          } else {
							 | 
						|||
| 
								 | 
							
								            included.add(cc);
							 | 
						|||
| 
								 | 
							
								          }
							 | 
						|||
| 
								 | 
							
								        }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								        return CountryCodeValidator(included, excluded);
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								    return null;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  StrategyRuleItem? fromData(List<String> data) {
							 | 
						|||
| 
								 | 
							
								    if (data.length != fields.length) {
							 | 
						|||
| 
								 | 
							
								      return null;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								    String? event;
							 | 
						|||
| 
								 | 
							
								    EventMatcher? eventMatcher;
							 | 
						|||
| 
								 | 
							
								    StrategyValidator? validator;
							 | 
						|||
| 
								 | 
							
								    int appEventCapabilitiesFlag = 0;
							 | 
						|||
| 
								 | 
							
								    String? adjustToken;
							 | 
						|||
| 
								 | 
							
								    for (int i = 0; i < fields.length; ++i) {
							 | 
						|||
| 
								 | 
							
								      final field = fields[i];
							 | 
						|||
| 
								 | 
							
								      final value = data[i];
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      if (field == "event") {
							 | 
						|||
| 
								 | 
							
								        event = value;
							 | 
						|||
| 
								 | 
							
								        if (event.isEmpty) {
							 | 
						|||
| 
								 | 
							
								          return null;
							 | 
						|||
| 
								 | 
							
								        }
							 | 
						|||
| 
								 | 
							
								        try {
							 | 
						|||
| 
								 | 
							
								          eventMatcher = createEventMatcher(value);
							 | 
						|||
| 
								 | 
							
								          Log.d("eventMatcher:$eventMatcher");
							 | 
						|||
| 
								 | 
							
								        } catch (error, stacktrace) {
							 | 
						|||
| 
								 | 
							
								          Log.w("createEventMatcher error! $error", stackTrace: stacktrace);
							 | 
						|||
| 
								 | 
							
								          return null;
							 | 
						|||
| 
								 | 
							
								        }
							 | 
						|||
| 
								 | 
							
								      } else if (field == "guru") {
							 | 
						|||
| 
								 | 
							
								        if (value == '1') {
							 | 
						|||
| 
								 | 
							
								          appEventCapabilitiesFlag |= AppEventCapabilities.guru;
							 | 
						|||
| 
								 | 
							
								        }
							 | 
						|||
| 
								 | 
							
								      } else if (field == "firebase") {
							 | 
						|||
| 
								 | 
							
								        if (value == '1') {
							 | 
						|||
| 
								 | 
							
								          appEventCapabilitiesFlag |= AppEventCapabilities.firebase;
							 | 
						|||
| 
								 | 
							
								        }
							 | 
						|||
| 
								 | 
							
								      } else if (field == "facebook") {
							 | 
						|||
| 
								 | 
							
								        if (value == '1') {
							 | 
						|||
| 
								 | 
							
								          appEventCapabilitiesFlag |= AppEventCapabilities.facebook;
							 | 
						|||
| 
								 | 
							
								        }
							 | 
						|||
| 
								 | 
							
								      } else if (field == "adjust") {
							 | 
						|||
| 
								 | 
							
								        if (value == '1') {}
							 | 
						|||
| 
								 | 
							
								      } else if (field == "strategy") {
							 | 
						|||
| 
								 | 
							
								        validator = createStrategyValidator(value);
							 | 
						|||
| 
								 | 
							
								      } else if ((Platform.isAndroid && field == "ata") || (Platform.isIOS && field == "ati")) {
							 | 
						|||
| 
								 | 
							
								        if (value.isNotEmpty && adjustTokenReg.hasMatch(value)) {
							 | 
						|||
| 
								 | 
							
								          adjustToken = value;
							 | 
						|||
| 
								 | 
							
								        }
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								    if (event != null && validator != null) {
							 | 
						|||
| 
								 | 
							
								      return StrategyRuleItem(
							 | 
						|||
| 
								 | 
							
								          event,
							 | 
						|||
| 
								 | 
							
								          StrategyRule(validator, AppEventCapabilities(appEventCapabilitiesFlag),
							 | 
						|||
| 
								 | 
							
								              matcher: eventMatcher, adjustToken: adjustToken));
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								    return null;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								class StrategyRuleItem extends Comparable {
							 | 
						|||
| 
								 | 
							
								  final String eventName;
							 | 
						|||
| 
								 | 
							
								  final StrategyRule rule;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  StrategyRuleItem(this.eventName, this.rule);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  int compareTo(other) {
							 | 
						|||
| 
								 | 
							
								    if (other is StrategyRuleItem) {
							 | 
						|||
| 
								 | 
							
								      return eventName.compareTo(other.eventName);
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								    throw StrategyRuleTypeException();
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								class GuruAnalyticsStrategy {
							 | 
						|||
| 
								 | 
							
								  static const String tag = "GuruAnalyticsStrategy";
							 | 
						|||
| 
								 | 
							
								  final List<StrategyRule> priorityRules = [];
							 | 
						|||
| 
								 | 
							
								  final SplayTreeMap<String, StrategyRule> explicitRules = SplayTreeMap();
							 | 
						|||
| 
								 | 
							
								  final Map<String, AdjustEventConverter> iosAdjustEventConverters = {};
							 | 
						|||
| 
								 | 
							
								  final Map<String, AdjustEventConverter> androidAdjustEventConverts = {};
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  bool loaded = false;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  final LinkedLruHashMap<String, StrategyRule> eventRules = LinkedLruHashMap(maximumSize: 128);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  GuruAnalyticsStrategy._();
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  static final GuruAnalyticsStrategy instance = GuruAnalyticsStrategy._();
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  void reset() {
							 | 
						|||
| 
								 | 
							
								    priorityRules.clear();
							 | 
						|||
| 
								 | 
							
								    explicitRules.clear();
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  static const guruAnalyticsStrategyExtension = ".gas"; // Guru Analytics Strategy
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  Future<File?> checkAndCreateLocalStrategyFile() async {
							 | 
						|||
| 
								 | 
							
								    final currentLocalStrategy =
							 | 
						|||
| 
								 | 
							
								        "${GuruSettings.instance.buildNumber.get()}$guruAnalyticsStrategyExtension";
							 | 
						|||
| 
								 | 
							
								    final file = await FileUtils.instance.getGuruConfigFile("analytics", currentLocalStrategy);
							 | 
						|||
| 
								 | 
							
								    if (!file.existsSync()) {
							 | 
						|||
| 
								 | 
							
								      try {
							 | 
						|||
| 
								 | 
							
								        final data = await rootBundle.loadString("assets/guru/analytics_strategy.csv");
							 | 
						|||
| 
								 | 
							
								        file.writeAsStringSync(data);
							 | 
						|||
| 
								 | 
							
								        Log.i("load local strategy success! [$currentLocalStrategy]", tag: tag);
							 | 
						|||
| 
								 | 
							
								        return file;
							 | 
						|||
| 
								 | 
							
								      } catch (error, stacktrace) {
							 | 
						|||
| 
								 | 
							
								        Log.w("not config local strategy!", tag: tag);
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								    return null;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  Future load() async {
							 | 
						|||
| 
								 | 
							
								    try {
							 | 
						|||
| 
								 | 
							
								      final analyticsConfig = RemoteConfigManager.instance.getAnalyticsConfig();
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      if (!GuruApp.instance.appSpec.deployment.enabledGuruAnalyticsStrategy ||
							 | 
						|||
| 
								 | 
							
								          !analyticsConfig.enabledStrategy) {
							 | 
						|||
| 
								 | 
							
								        Log.w("analytics strategy disabled!", tag: tag);
							 | 
						|||
| 
								 | 
							
								        return;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      final String remoteAnalyticsStrategy = analyticsConfig.strategy;
							 | 
						|||
| 
								 | 
							
								      final latestAnalyticsStrategy = await AppProperty.getInstance().getLatestAnalyticsStrategy();
							 | 
						|||
| 
								 | 
							
								      if (remoteAnalyticsStrategy != latestAnalyticsStrategy) {
							 | 
						|||
| 
								 | 
							
								        loaded = false;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								      if (loaded) {
							 | 
						|||
| 
								 | 
							
								        Log.w("already loaded! ignore!", tag: tag);
							 | 
						|||
| 
								 | 
							
								        return;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								      File? file;
							 | 
						|||
| 
								 | 
							
								      // 如果remoteAnalyticsStrategy非空表示云控配置了strategy
							 | 
						|||
| 
								 | 
							
								      if (!DartExt.isBlank(remoteAnalyticsStrategy)) {
							 | 
						|||
| 
								 | 
							
								        file = await FileUtils.instance.getGuruConfigFile("analytics", remoteAnalyticsStrategy);
							 | 
						|||
| 
								 | 
							
								        if (!file.existsSync()) {
							 | 
						|||
| 
								 | 
							
								          try {
							 | 
						|||
| 
								 | 
							
								            await FileUtils.instance.downloadFile(
							 | 
						|||
| 
								 | 
							
								                "${GuruApp.instance.details.storagePrefix}/guru%2Fanalytics%2F$remoteAnalyticsStrategy?alt=media",
							 | 
						|||
| 
								 | 
							
								                file);
							 | 
						|||
| 
								 | 
							
								            Log.i("download analytics strategy[$remoteAnalyticsStrategy] success", tag: tag);
							 | 
						|||
| 
								 | 
							
								          } catch (error, stacktrace) {
							 | 
						|||
| 
								 | 
							
								            Log.w("downloadFile error! $error try to fallback", tag: tag);
							 | 
						|||
| 
								 | 
							
								            // 这里没有使用上一次的strategy做回滚的原因,
							 | 
						|||
| 
								 | 
							
								            // 主要是考虑到上一次的云端strategy可能没有本地的strategy可靠,
							 | 
						|||
| 
								 | 
							
								            // SDK假设本地的strategy比Firebase Storage中配置的strategy更可靠
							 | 
						|||
| 
								 | 
							
								            // 因此这里在出现下载异常的情况下,会回滚到本地strategy上
							 | 
						|||
| 
								 | 
							
								            // 如果不想使用这个机制,可以在自己的项目中不配置任何strategy
							 | 
						|||
| 
								 | 
							
								          }
							 | 
						|||
| 
								 | 
							
								        }
							 | 
						|||
| 
								 | 
							
								        if (remoteAnalyticsStrategy != latestAnalyticsStrategy) {
							 | 
						|||
| 
								 | 
							
								          AppProperty.getInstance().setLatestAnalyticsStrategy(remoteAnalyticsStrategy);
							 | 
						|||
| 
								 | 
							
								          final latestStrategyFile =
							 | 
						|||
| 
								 | 
							
								              await FileUtils.instance.getGuruConfigFile("analytics", latestAnalyticsStrategy);
							 | 
						|||
| 
								 | 
							
								          if (latestStrategyFile.existsSync()) {
							 | 
						|||
| 
								 | 
							
								            FileUtils.instance.deleteFile(latestStrategyFile);
							 | 
						|||
| 
								 | 
							
								          }
							 | 
						|||
| 
								 | 
							
								        }
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      // 如果当前文件为空或是不存在,证明有可能相应的strategy下载失败,或是没有设置
							 | 
						|||
| 
								 | 
							
								      // 因此这种情况下尝试使用本地的strategy进行加载
							 | 
						|||
| 
								 | 
							
								      if (file?.existsSync() != true) {
							 | 
						|||
| 
								 | 
							
								        file = await checkAndCreateLocalStrategyFile();
							 | 
						|||
| 
								 | 
							
								        if (file?.existsSync() != true) {
							 | 
						|||
| 
								 | 
							
								          return;
							 | 
						|||
| 
								 | 
							
								        }
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      final Stream<String> strategyTextStream = file!.openRead().transform(utf8.decoder);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      StrategyRule? newDefaultRule;
							 | 
						|||
| 
								 | 
							
								      final List<StrategyRule> newPriorityRules = [];
							 | 
						|||
| 
								 | 
							
								      final Map<String, StrategyRule> newExplicitRules = {};
							 | 
						|||
| 
								 | 
							
								      StrategyRuleParser? parser;
							 | 
						|||
| 
								 | 
							
								      int lineNum = 0;
							 | 
						|||
| 
								 | 
							
								      await for (var line in strategyTextStream.transform(const LineSplitter())) {
							 | 
						|||
| 
								 | 
							
								        final list = line.split(",");
							 | 
						|||
| 
								 | 
							
								        Log.d("[${lineNum++}] $list", tag: tag);
							 | 
						|||
| 
								 | 
							
								        if (parser == null) {
							 | 
						|||
| 
								 | 
							
								          parser = StrategyRuleParser(list);
							 | 
						|||
| 
								 | 
							
								        } else {
							 | 
						|||
| 
								 | 
							
								          final ruleItem = parser.fromData(list);
							 | 
						|||
| 
								 | 
							
								          if (ruleItem == null) {
							 | 
						|||
| 
								 | 
							
								            continue;
							 | 
						|||
| 
								 | 
							
								          }
							 | 
						|||
| 
								 | 
							
								          if (ruleItem.eventName == "_all_") {
							 | 
						|||
| 
								 | 
							
								            newDefaultRule = ruleItem.rule;
							 | 
						|||
| 
								 | 
							
								          } else if (ruleItem.rule.matcher != null) {
							 | 
						|||
| 
								 | 
							
								            newPriorityRules.add(ruleItem.rule);
							 | 
						|||
| 
								 | 
							
								          } else {
							 | 
						|||
| 
								 | 
							
								            newExplicitRules[ruleItem.eventName] = ruleItem.rule;
							 | 
						|||
| 
								 | 
							
								          }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								          if (ruleItem.rule.adjustToken != null) {
							 | 
						|||
| 
								 | 
							
								            if (Platform.isAndroid) {
							 | 
						|||
| 
								 | 
							
								              androidAdjustEventConverts[ruleItem.eventName] =
							 | 
						|||
| 
								 | 
							
								                  (_) => AdjustEvent(ruleItem.rule.adjustToken!);
							 | 
						|||
| 
								 | 
							
								            } else if (Platform.isIOS) {
							 | 
						|||
| 
								 | 
							
								              iosAdjustEventConverters[ruleItem.eventName] =
							 | 
						|||
| 
								 | 
							
								                  (_) => AdjustEvent(ruleItem.rule.adjustToken!);
							 | 
						|||
| 
								 | 
							
								            }
							 | 
						|||
| 
								 | 
							
								          }
							 | 
						|||
| 
								 | 
							
								        }
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      reset();
							 | 
						|||
| 
								 | 
							
								      priorityRules.addAll(newPriorityRules.reversed);
							 | 
						|||
| 
								 | 
							
								      if (newDefaultRule != null) {
							 | 
						|||
| 
								 | 
							
								        priorityRules.add(newDefaultRule);
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								      explicitRules.addAll(newExplicitRules);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      loaded = true;
							 | 
						|||
| 
								 | 
							
								      Log.d(
							 | 
						|||
| 
								 | 
							
								          "analytics strategy loaded! ${eventRules.length} ${explicitRules.length} ${priorityRules.length}",
							 | 
						|||
| 
								 | 
							
								          tag: tag);
							 | 
						|||
| 
								 | 
							
								    } catch (error, stacktrace) {}
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  StrategyRule? getStrategyRule(String eventName) {
							 | 
						|||
| 
								 | 
							
								    Log.d(
							 | 
						|||
| 
								 | 
							
								        "[$loaded]getStrategyRule:$eventName ${eventRules.length} ${explicitRules.length} ${priorityRules.length}");
							 | 
						|||
| 
								 | 
							
								    if (!loaded) {
							 | 
						|||
| 
								 | 
							
								      return null;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    final rule = eventRules[eventName];
							 | 
						|||
| 
								 | 
							
								    if (rule != null) {
							 | 
						|||
| 
								 | 
							
								      return rule;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    final explicitRule = explicitRules[eventName];
							 | 
						|||
| 
								 | 
							
								    if (explicitRule != null) {
							 | 
						|||
| 
								 | 
							
								      return explicitRule;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    for (var rule in priorityRules) {
							 | 
						|||
| 
								 | 
							
								      Log.d("matcher: ${rule.matcher} eventName: $eventName ${rule.matcher?.match(eventName)}",
							 | 
						|||
| 
								 | 
							
								          tag: tag);
							 | 
						|||
| 
								 | 
							
								      if (rule.matcher?.match(eventName) == true) {
							 | 
						|||
| 
								 | 
							
								        return rule;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								    // 如果没有启用strategy,默认按之前逻辑处理
							 | 
						|||
| 
								 | 
							
								    return null;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  AdjustEventConverter? getAdjustEventConverter(String eventName) {
							 | 
						|||
| 
								 | 
							
								    if (Platform.isAndroid) {
							 | 
						|||
| 
								 | 
							
								      return androidAdjustEventConverts[eventName];
							 | 
						|||
| 
								 | 
							
								    } else if (Platform.isIOS) {
							 | 
						|||
| 
								 | 
							
								      return iosAdjustEventConverters[eventName];
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								    return null;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  void testRule(String eventName) {
							 | 
						|||
| 
								 | 
							
								    final rule = getStrategyRule(eventName);
							 | 
						|||
| 
								 | 
							
								    if (rule?.matcher?.match(eventName) != false) {
							 | 
						|||
| 
								 | 
							
								      Log.d("testMatch: $eventName => $rule success!", tag: tag);
							 | 
						|||
| 
								 | 
							
								    } else {
							 | 
						|||
| 
								 | 
							
								      Log.d("testMatch: $eventName => $rule error!", tag: tag);
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 |