511 lines
17 KiB
Dart
511 lines
17 KiB
Dart
import 'dart:io';
|
||
|
||
import 'package:adjust_sdk/adjust_event.dart';
|
||
import 'package:firebase_analytics/firebase_analytics.dart';
|
||
import 'package:firebase_core/firebase_core.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:guru_app/account/account_data_store.dart';
|
||
import 'package:guru_app/account/account_manager.dart';
|
||
import 'package:guru_app/account/model/account.dart';
|
||
import 'package:guru_app/account/model/credential.dart';
|
||
import 'package:guru_app/account/model/user.dart';
|
||
import 'package:guru_app/ads/ads_manager.dart';
|
||
import 'package:guru_app/analytics/abtest/abtest_model.dart';
|
||
import 'package:guru_app/analytics/guru_analytics.dart';
|
||
import 'package:guru_app/app/app_models.dart';
|
||
import 'package:guru_app/database/guru_db.dart';
|
||
import 'package:guru_app/financial/financial_manager.dart';
|
||
import 'package:guru_app/financial/iap/iap_manager.dart';
|
||
import 'package:guru_app/financial/manifest/manifest.dart';
|
||
import 'package:guru_app/financial/manifest/manifest_manager.dart';
|
||
import 'package:guru_app/financial/product/product_model.dart';
|
||
import 'package:guru_app/financial/reward/reward_manager.dart';
|
||
import 'package:guru_app/firebase/dxlinks/dxlink_manager.dart';
|
||
import 'package:guru_app/inventory/inventory_manager.dart';
|
||
import 'package:guru_applovin_flutter/guru_applovin_flutter.dart';
|
||
import 'package:guru_app/property/app_property.dart';
|
||
import 'package:guru_app/property/property_keys.dart';
|
||
import 'package:guru_utils/auth/auth_credential_manager.dart';
|
||
import 'package:guru_utils/collection/collectionutils.dart';
|
||
import 'package:guru_utils/controller/aware/ads/overlay/ads_overlay.dart';
|
||
import 'package:guru_utils/datetime/datetime_utils.dart';
|
||
import 'package:guru_utils/device/device_utils.dart';
|
||
import 'package:guru_utils/lifecycle/lifecycle_manager.dart';
|
||
import 'package:guru_utils/network/network_utils.dart';
|
||
import 'package:guru_utils/property/app_property.dart';
|
||
import 'package:guru_app/property/settings/guru_settings.dart';
|
||
import 'package:guru_app/firebase/firebase.dart';
|
||
import 'package:guru_utils/http/http_ex.dart';
|
||
import 'package:guru_utils/log/log.dart';
|
||
import 'package:guru_utils/packages/guru_package.dart';
|
||
import 'package:guru_utils/ads/ads.dart';
|
||
import 'package:guru_utils/guru_utils.dart';
|
||
import 'package:guru_utils/property/property_model.dart';
|
||
import 'package:logger/logger.dart' as Logger;
|
||
import 'package:guru_utils/aigc/bi/ai_bi.dart';
|
||
import 'package:package_info_plus/package_info_plus.dart';
|
||
import 'package:guru_popup/guru_popup.dart';
|
||
import 'package:guru_app/analytics/abtest/abtest_model.dart';
|
||
|
||
export 'package:firebase_core/firebase_core.dart';
|
||
export 'package:guru_app/app/app_models.dart';
|
||
export 'package:guru_utils/log/log.dart';
|
||
export 'package:guru_spec/guru_spec.dart';
|
||
export 'package:guru_app/analytics/guru_analytics.dart';
|
||
export 'package:guru_app/financial/product/product_model.dart';
|
||
export 'package:adjust_sdk/adjust_event.dart';
|
||
export 'package:guru_utils/ads/ads.dart';
|
||
export 'package:guru_utils/guru_utils.dart';
|
||
export 'dart:io';
|
||
export 'dart:math';
|
||
export 'package:guru_app/financial/manifest/manifest.dart';
|
||
export 'package:guru_app/firebase/messaging/remote_messaging_manager.dart';
|
||
export 'package:guru_app/analytics/abtest/abtest_model.dart';
|
||
|
||
/// Created by Haoyi on 2022/8/25
|
||
|
||
abstract class AppSpec {
|
||
String get appName;
|
||
|
||
AppCategory get appCategory;
|
||
|
||
String get flavor;
|
||
|
||
AppDetails get details;
|
||
|
||
AdsProfile get adsProfile;
|
||
|
||
ProductProfile get productProfile;
|
||
|
||
AdjustProfile get adjustProfile;
|
||
|
||
Deployment get deployment;
|
||
|
||
Map<String, dynamic> get defaultRemoteConfig;
|
||
|
||
Map<String, ABTestExperiment> get localABTestExperiments;
|
||
|
||
String getRemoteConfigKey(String key);
|
||
}
|
||
|
||
class NotImplementationAppSpecCreatorException implements Exception {
|
||
NotImplementationAppSpecCreatorException();
|
||
|
||
@override
|
||
String toString() {
|
||
return 'NotImplementationAppSpecCreatorException';
|
||
}
|
||
}
|
||
|
||
class AppEnv {
|
||
final AppSpec spec;
|
||
final RootPackage package;
|
||
final IGuruSdkProtocol protocol;
|
||
|
||
AppEnv(
|
||
{required this.spec,
|
||
required this.package,
|
||
required this.protocol});
|
||
}
|
||
|
||
extension _GuruPackageExtension on GuruPackage {
|
||
Iterable<Locale> _mergeSupportedLocales() {
|
||
final Set<Locale> locales = supportedLocales.toSet();
|
||
for (var child in children) {
|
||
locales.addAll(child._mergeSupportedLocales());
|
||
}
|
||
return locales;
|
||
}
|
||
|
||
Iterable<LocalizationsDelegate<dynamic>> _mergeLocalizationsDelegates() {
|
||
final Set<LocalizationsDelegate<dynamic>> delegates = localizationsDelegates.toSet();
|
||
for (var child in children) {
|
||
delegates.addAll(child._mergeLocalizationsDelegates());
|
||
}
|
||
return delegates;
|
||
}
|
||
|
||
Future _dispatchInitialize() async {
|
||
await initialize();
|
||
children.sort((p1, p2) {
|
||
return p2.priority.compareTo(p1.priority);
|
||
});
|
||
for (var child in children) {
|
||
if (flattenChildrenAsyncInit) {
|
||
child._dispatchInitialize();
|
||
} else {
|
||
await child._dispatchInitialize();
|
||
}
|
||
}
|
||
}
|
||
|
||
Future _dispatchInitializeAsync() async {
|
||
initializeAsync();
|
||
for (var child in children) {
|
||
child._dispatchInitializeAsync();
|
||
}
|
||
}
|
||
|
||
Future _dispatchSwitchSession(String oldToken, String newToken) async {
|
||
await switchSession(oldToken, newToken);
|
||
children.sort((p1, p2) {
|
||
return p2.priority.compareTo(p1.priority);
|
||
});
|
||
for (var child in children) {
|
||
if (flattenChildrenAsyncInit) {
|
||
child._dispatchSwitchSession(oldToken, newToken);
|
||
} else {
|
||
await child._dispatchSwitchSession(oldToken, newToken);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
enum AppCategory { game, app }
|
||
|
||
abstract class IGuruSdkProtocol {
|
||
static void _unimplementedError(String name) {
|
||
Log.e(
|
||
"[$name] It is critically important that the GuruSDK protocol be implemented with precision. \n"
|
||
"Failure to adhere to its standards will result in inaccuracies within our analytics data,\n"
|
||
"thereby severely compromising our marketing strategies and the effectiveness of our user acquisition efforts.\n"
|
||
"It is essential to understand that non-compliance is not an option,\n"
|
||
"as it poses significant risks to our operational success and strategic objectives.");
|
||
throw UnimplementedError("Please fully implement the IGuruSdkProtocol!");
|
||
}
|
||
|
||
InventoryDelegate? get inventoryDelegate => null;
|
||
|
||
BackgroundMessageHandler? get backgroundMessageHandler => null;
|
||
|
||
ToastDelegate? get toastDelegate => null;
|
||
|
||
IAccountAuthDelegate? get accountAuthDelegate => null;
|
||
|
||
String getLevelName() {
|
||
if (GuruApp.instance.appSpec.appCategory == AppCategory.game) {
|
||
_unimplementedError("getLevelName");
|
||
}
|
||
return "app";
|
||
}
|
||
|
||
int getCurrentLevel() {
|
||
if (GuruApp.instance.appSpec.appCategory == AppCategory.game) {
|
||
_unimplementedError("getCurrentLevel");
|
||
}
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
final Set<PropertyKey> _deviceSharedProperties = {
|
||
PropertyKeys.deviceId,
|
||
PropertyKeys.version,
|
||
PropertyKeys.buildNumber,
|
||
PropertyKeys.firstInstallTime,
|
||
PropertyKeys.firstInstallVersion,
|
||
PropertyKeys.prevInstallVersion,
|
||
PropertyKeys.latestInstallVersion,
|
||
PropertyKeys.previousInstalledVersion,
|
||
PropertyKeys.latestLtDate,
|
||
PropertyKeys.ltDays,
|
||
PropertyKeys.appInstanceId,
|
||
PropertyKeys.debugMode,
|
||
PropertyKeys.keepOnScreenDuration,
|
||
PropertyKeys.analyticsAdId,
|
||
PropertyKeys.analyticsAdjustId,
|
||
PropertyKeys.analyticsDeviceId,
|
||
PropertyKeys.analyticsIdfa,
|
||
PropertyKeys.analyticsFirebaseId,
|
||
PropertyKeys.latestAnalyticsStrategy
|
||
};
|
||
|
||
class GuruApp {
|
||
static late GuruApp _instance;
|
||
|
||
static GuruApp get instance => _instance;
|
||
|
||
final RootPackage rootPackage;
|
||
|
||
final AppSpec appSpec;
|
||
|
||
final IGuruSdkProtocol protocol;
|
||
RemoteDeployment? _remoteDeployment;
|
||
|
||
RemoteDeployment get remoteDeployment =>
|
||
_remoteDeployment ??= RemoteConfigManager.instance.getRemoteDeployment();
|
||
|
||
String get appName => appSpec.appName;
|
||
|
||
String get flavor => appSpec.flavor;
|
||
|
||
AppDetails get details => appSpec.details;
|
||
|
||
AdsProfile get adsProfile => appSpec.adsProfile;
|
||
|
||
AdjustProfile get adjustProfile => appSpec.adjustProfile;
|
||
|
||
ProductProfile get productProfile => appSpec.productProfile;
|
||
|
||
Map<String, dynamic> get defaultRemoteConfig => appSpec.defaultRemoteConfig;
|
||
|
||
Set<String> get conversionEvents => appSpec.deployment.conversionEvents;
|
||
|
||
GuruApp._(
|
||
{required this.appSpec,
|
||
required this.rootPackage,
|
||
required this.protocol,
|
||
}) {
|
||
GuruUtils.toastDelegate = protocol.toastDelegate;
|
||
AdsOverlay.bind(showBanner: GuruPopup.instance.showAdsBanner);
|
||
}
|
||
|
||
Iterable<Locale> get supportedLocales => rootPackage._mergeSupportedLocales();
|
||
|
||
Iterable<LocalizationsDelegate<dynamic>> get localizationsDelegates =>
|
||
rootPackage._mergeLocalizationsDelegates();
|
||
|
||
String getRemoteConfigKey(String key) => appSpec.getRemoteConfigKey(key);
|
||
|
||
bool? _check;
|
||
|
||
@visibleForTesting
|
||
static void setMockInstance(GuruApp app) {
|
||
_instance = app;
|
||
}
|
||
|
||
Future _initialize() async {
|
||
try {
|
||
await GuruDB.instance.initDatabase();
|
||
AppProperty.initialize(GuruDB.instance, cacheSize: appSpec.deployment.propertyCacheSize);
|
||
await GuruSettings.instance.refresh();
|
||
Paint.enableDithering = appSpec.deployment.enableDithering; // 3.16 default enabled
|
||
await _dispatchInitializeSync();
|
||
_dispatchInitializeAsync();
|
||
} catch (error, stacktrace) {
|
||
Log.w("initialize error:$error, $stacktrace");
|
||
}
|
||
}
|
||
|
||
Future _migrateDeviceSharedData(PropertyBundle latestData) async {
|
||
final PropertyBundle bundle = PropertyBundle();
|
||
final keys = {
|
||
..._deviceSharedProperties,
|
||
...(protocol.accountAuthDelegate?.deviceSharedProperties ?? {})
|
||
};
|
||
for (var key in keys) {
|
||
final value = latestData.getString(key);
|
||
if (value != null) {
|
||
bundle.setString(key, value);
|
||
}
|
||
}
|
||
await AppProperty.getInstance().setProperties(bundle);
|
||
}
|
||
|
||
RemoteDeployment refreshRemoteDeployment() {
|
||
try {
|
||
return _remoteDeployment ??= RemoteConfigManager.instance.getRemoteDeployment();
|
||
} catch (error, stacktrace) {
|
||
Log.w("refreshRemoteDeployment error:$error, $stacktrace");
|
||
}
|
||
return RemoteDeployment.fromJson({});
|
||
}
|
||
|
||
Future<bool> switchAccount(GuruUser user, Credential credential, {GuruUser? oldUser}) async {
|
||
final String oldToken = oldUser?.uid ?? "";
|
||
final String newToken = user.uid;
|
||
try {
|
||
final previousUserProperties =
|
||
PropertyBundle(map: await AppProperty.getInstance().loadAllValues());
|
||
|
||
final result = await GuruDB.instance.switchSession(oldToken, newToken);
|
||
if (!result) {
|
||
Log.w("switchSession failed");
|
||
return false;
|
||
}
|
||
AppProperty.reload(cacheSize: appSpec.deployment.propertyCacheSize);
|
||
await _migrateDeviceSharedData(previousUserProperties);
|
||
await GuruSettings.instance.refresh();
|
||
await DeviceUtils.reload();
|
||
FinancialManager.instance.switchSession(oldToken, newToken);
|
||
GuruAnalytics.instance.switchSession(oldToken, newToken);
|
||
await AccountManager.instance.processLogin(user, credential);
|
||
await rootPackage._dispatchSwitchSession(oldToken, newToken);
|
||
return true;
|
||
} catch (error, stacktrace) {
|
||
Log.w("switchSession error:$error, $stacktrace");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
Future<bool> _checkApp() async {
|
||
try {
|
||
final pkgName = (await PackageInfo.fromPlatform()).appName;
|
||
final result = _check ??= (pkgName != GuruApp.instance.details.appId);
|
||
GuruAnalytics.instance.logGuruEvent(
|
||
"dev_audit",
|
||
CollectionUtils.filterOutNulls({
|
||
"item_category": "pkg",
|
||
"result": result == true ? 1 : 0,
|
||
"err_info": result != true ? pkgName : null,
|
||
}));
|
||
return result == true;
|
||
} catch (error, stacktrace) {
|
||
Log.w("checkApp error:$error, $stacktrace");
|
||
GuruAnalytics.instance.logException(error, stacktrace: stacktrace);
|
||
GuruAnalytics.instance.logGuruEvent(
|
||
"dev_audit",
|
||
CollectionUtils.filterOutNulls({
|
||
"item_category": "pkg",
|
||
"result": 0,
|
||
"err_info": error.runtimeType.toString(),
|
||
}));
|
||
return false;
|
||
}
|
||
}
|
||
|
||
Future _dispatchInitializeSync() async {
|
||
await RemoteConfigManager.instance.init(appSpec.defaultRemoteConfig);
|
||
refreshRemoteDeployment();
|
||
await DeviceUtils.initialize();
|
||
await GuruAnalytics.instance.prepare();
|
||
await rootPackage._dispatchInitialize();
|
||
try {
|
||
GuruUtils.isTablet = (await GuruApplovinFlutter.instance.isTablet()) ?? false;
|
||
Log.d("isTablet: ${GuruUtils.isTablet}");
|
||
} catch (error, stacktrace) {
|
||
Log.w("invoke isTablet error:$error, $stacktrace");
|
||
}
|
||
}
|
||
|
||
Future _dispatchInitializeAsync() async {
|
||
_initCommon();
|
||
_initRemoteConfig();
|
||
_initRemoteMessaging();
|
||
_initAnalytics();
|
||
if (appSpec.adsProfile != AdsProfile.invalid) {
|
||
_initAds();
|
||
}
|
||
_initFinancial();
|
||
_initAccount();
|
||
_initDxLink();
|
||
rootPackage._dispatchInitializeAsync();
|
||
Future.delayed(const Duration(seconds: 15), () async {
|
||
await _checkApp();
|
||
});
|
||
}
|
||
|
||
static Future initialize({required AppEnv appEnv}) async {
|
||
final backgroundMessageHandler = appEnv.protocol.backgroundMessageHandler;
|
||
if (backgroundMessageHandler != null) {
|
||
FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler);
|
||
}
|
||
WidgetsFlutterBinding.ensureInitialized();
|
||
try {
|
||
await Firebase.initializeApp();
|
||
} catch (error, stacktrace) {
|
||
Log.e("Firebase.initializeApp() error!", error: error, stackTrace: stacktrace);
|
||
}
|
||
GuruUtils.flavor = appEnv.spec.flavor;
|
||
|
||
/// 这里不用担心重复初始化,因为initialize会把对应的 AuthType 重新赋值
|
||
/// 如果传入的 AuthType 有重复,会覆盖之前的 AuthType
|
||
AuthCredentialManager.initialize([
|
||
...AccountManager.defaultSupportedAuthCredentialDelegates,
|
||
...appEnv.protocol.accountAuthDelegate?.supportedAuthCredentialDelegates ?? []
|
||
]);
|
||
try {
|
||
_instance = GuruApp._(
|
||
appSpec: appEnv.spec,
|
||
rootPackage: appEnv.package,
|
||
protocol: appEnv.protocol);
|
||
Log.init(_instance.appName,
|
||
persistentLogFileSize: appEnv.spec.deployment.logFileSizeLimit,
|
||
persistentLogCount: appEnv.spec.deployment.logFileCount,
|
||
persistentLevel: appEnv.spec.deployment.persistentLogLevel);
|
||
AiBi.instance.init();
|
||
AdsManager.instance.ensureInitialize();
|
||
await _instance._initialize();
|
||
LifecycleManager.instance.init();
|
||
} catch (error, stacktrace) {
|
||
Log.e("GuruApp initialize error!", error: error, stackTrace: stacktrace);
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
void showToast(String message, {Duration duration = const Duration(seconds: 3)}) {
|
||
GuruUtils.showToast(message, duration: duration);
|
||
}
|
||
}
|
||
|
||
extension GuruAppInitializerExt on GuruApp {
|
||
Future _initCommon() async {
|
||
await NetworkUtils.init();
|
||
}
|
||
|
||
Future _initRemoteConfig() async {
|
||
await RemoteConfigManager.instance.fetchAndActivate();
|
||
final cdnConfig = RemoteConfigManager.instance.getCdnConfig();
|
||
HttpEx.init(cdnConfig, GuruApp.instance.appSpec.details.storagePrefix);
|
||
refreshRemoteDeployment();
|
||
Settings.get()
|
||
.keepOnScreenDuration
|
||
.set(remoteDeployment.keepScreenOnDuration * DateTimeUtils.minuteInMillis);
|
||
}
|
||
|
||
void _initAnalytics() {
|
||
GuruAnalytics.instance.init();
|
||
}
|
||
|
||
void _initRemoteMessaging() async {
|
||
RemoteMessagingManager.instance.init();
|
||
}
|
||
|
||
void _initDxLink() {
|
||
Future.delayed(const Duration(seconds: 2), () {
|
||
DxLinkManager.instance.init();
|
||
});
|
||
}
|
||
|
||
void _initAds() async {
|
||
try {
|
||
await AccountDataStore.instance.observableSaasUser
|
||
.firstWhere((saasUser) => saasUser?.isValid == true)
|
||
.timeout(const Duration(seconds: 3));
|
||
} catch (error, stacktrace) {
|
||
Log.w("wait account error! $error", stackTrace: stacktrace);
|
||
} finally {
|
||
await AdsManager.instance.initialize(saasUser: AccountDataStore.instance.user);
|
||
}
|
||
}
|
||
|
||
Future _initFinancial() async {
|
||
ManifestManager.instance.addBuilders(GuruApp.instance.productProfile.manifestBuilders);
|
||
|
||
FinancialManager.instance.init();
|
||
}
|
||
|
||
Future _initAccount() async {
|
||
await AccountManager.instance.init();
|
||
}
|
||
}
|
||
|
||
extension GuruAppFinancialExt on GuruApp {
|
||
ProductId defineProductId(String sku, int attr, TransactionMethod method) {
|
||
return productProfile.define(sku, attr, method);
|
||
}
|
||
|
||
ProductId? findProductId({String? sku, int? attr}) {
|
||
return productProfile.find(sku: sku, attr: attr);
|
||
}
|
||
|
||
Set<ProductId> offerProductIds(ProductId productId) {
|
||
return productProfile.offerProductIds(productId);
|
||
}
|
||
}
|
||
|
||
extension GuruRemoteConfigExt on GuruApp {
|
||
String getDefaultRemoteConfig(String key, {String defaultValue = ""}) {
|
||
return defaultRemoteConfig[key] ?? defaultValue;
|
||
}
|
||
}
|