diff --git a/guru_app/.github/release.yaml b/guru_app/.github/release.yaml new file mode 100644 index 0000000..f7970b8 --- /dev/null +++ b/guru_app/.github/release.yaml @@ -0,0 +1,9 @@ +# .github/release.yml +changelog: + categories: + - title: 🟢 Features + labels: + - Feature + - title: 🟠 Optimize + labels: + - optimize diff --git a/guru_app/.github/workflows/github-project-issue-to-sheets.yaml b/guru_app/.github/workflows/github-project-issue-to-sheets.yaml new file mode 100644 index 0000000..b2cc324 --- /dev/null +++ b/guru_app/.github/workflows/github-project-issue-to-sheets.yaml @@ -0,0 +1,21 @@ +name: github-project-issue-to-sheets + +# Controls when the action will run. Triggers the workflow on push or pull request +# events but only for the master branch +on: + workflow_dispatch: + issues: + types: [opened, deleted, transferred, closed, reopened, assigned, unassigned, labeled, unlabeled] + +jobs: + github-project-issue-to-sheets: + runs-on: ubuntu-latest + name: github-project-issue-to-sheets + steps: + - name: Transfer GitHub Project Issues into Google Sheets + id: github-project-issue-to-sheets + uses: ViRGiL175/github-project-issue-to-sheets@v2.0.0 + with: + google-api-service-account-credentials: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_DATA }} + document-id: '1TMTFBE7xPmgIsJVfYbTMIoQ7D_s42KGqFHxqu3kAFEA' + sheet-name: 'GitHub Issues' \ No newline at end of file diff --git a/guru_app/.gitignore b/guru_app/.gitignore new file mode 100644 index 0000000..2ee8465 --- /dev/null +++ b/guru_app/.gitignore @@ -0,0 +1,120 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +.vscode +.gradle +.idea +/local.properties +.DS_Store +/build +.metadata +ios/Flutter/flutter_export_environment.sh + +**/.settings +**/.project +**/.classpath + + +# build id +build_id.properties + +# IntelliJ related + +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +/build/ +flutter_* +.flutter-plugins-dependencies + +# goCli related +go-cli/.packages +go-cli/pubspec.lock + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/gradlew +**/android/gradlew.bat +**/android/fastlane/report.xml +**/android/fastlane/README.md + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* +**/ios/build/* +**/ios/fastlane/README.md +**/ios/fastlane/report.xml + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages + +# Crashlytics +debugSymbols/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# i18n +**/android/res/values/strings_en.arb +lib/generated/i18n.dart + +# output +output/ios/adhok/* +output/ios/appstore/* +ios/fastlane/report.xml +android/fastlane/report.xml + +**/android/fastlane/metadata/android/**/images/** +**/ios/fastlane/screenshots/** \ No newline at end of file diff --git a/guru_app/CHANGELOG.md b/guru_app/CHANGELOG.md new file mode 100644 index 0000000..46dadf8 --- /dev/null +++ b/guru_app/CHANGELOG.md @@ -0,0 +1,74 @@ +## v1.2.0(beta) +- **`[guru_app]`** + - 在financial模块添加igc购买流程 + - 取消iapManager的restorePurchase机制,改用reloadOrders(本地读取机制) + - Deployment中添加`autoRestoreIap`、`enableAnalyticsStatistic`、`initIgc`和`igcBalanceSecret`的支持 +- **`[guru_spec]`** + - deployment解析器添加`auto_restore_iap`的支持,取值范围为true或false,true表示在IapManager将自动进行restore操作,false反之 + - deployment解析器添加`enable_analytics_statistic`的支持,取值范围为true或false,true表示在GuruAnalytics将自动添加统计的UserProperty + - deployment解析器添加`init_igc`的支持,取值为int型,在IgcManager初始化的时候第一次使用时会给予相应的igc数量做为初始值 + - deployment解析器添加`igcBalanceSecret`的支持,取值int型,在igc混淆用于防修改的安全secret key + +## v1.1.0 +- **`[guru_app]`** + - 在financial模块添加reward购买流程 + - 升级数据库,针对order表添加category字段,来记录商品分类,方便后续搜索 + - 移除guru_app中的通用广告model定义(移至**guru_utils**) + - 移除BaseController,LifecycleController和AdsController(移至**guru_utils**) + - 移除RewardsAware,InterstitialAware,BannerAware(移至**guru_utils**) +- **`[guru_spec]`** + - 添加`products`解析器 + - 支持`manifest`定义 + - 支持manifest category的汇总 + - 支持category的lint检查(在定义相似内容时会报错) + - 支持同category的manifest参数lint检查(在定义同category的manifest时,如果参数不匹配将报错) + - 移除`iap_profile`解析器 +- **`[guru_utils]`** + - 添加通用广告model定义 + - 添加controller定义 + - 添加RewardsAware,InterstitialAware,BannerAware +- **`[guru_navigator]`** + - 添加`guru_navigator`plugin,针对Android的deepLink和ios的universalLink的处理 + +## v1.0.1 +- **`[guru_app]`** + - iap相关逻辑优化 +- **`[guru_utils]`** + - 抽象RemoteUtils以便兼容老项目 + - 抽象AnalyticsUtils以便兼容老项目 + - Vibration库的抽象及优化 +- **`[guru_spec]`** + - 强化对兄弟包的支持 + +## v1.0.0 +- **`[guru_app]`** + 该库包含guru自身的相关业务逻辑,将公司的业务逻辑进行统一封装统一管理,相应三方库的版本进行统一调优主要包括如下主要模块 + - ***`Account`*** + 处理匿名登陆相关逻辑,并完成设备上报,错误重试,恢复等相关机制 + - ***`Ads`*** + 处理广告逻辑(MAX),支持插屏,激励视频,Banner + - ***`Analytics`*** + 处理打点相关逻辑,现集成Firebase、Facebook、Guru和Adjust,并封装了相应的标准点和Guru标准点 + - ***`DxLink`*** + 支持处理DynamicLink和Deeplink的回跳相关逻辑 + - ***`CloudMessaging`*** + 处理Push/In-app Messaging相关逻辑 + - ***`RemoteConfig`*** + 处理相关的RemoteConfig相关逻辑,这个依赖于GuruApp中**GuruSpec**的配置生成 + - ***`Financial`*** + 处理相关交易信息,当前版本支持IAP,后续将扩展虚拟货币及Rewards相关的购买逻辑 + - ***`Audio`*** + 音频处理逻辑,通过soundpool逻辑进行二次封装,支持更高效的音效输出 + - ***`Router`*** + 依赖于Get的路由机制 + - ***`Controller`*** + 依赖于Get的GetWidget来配合Controller的逻辑,现在实现了LifecycleController和AdsController,并封装了相应的业务逻辑,并实现了相应的Aware来支持辅助扩展 + +- **`[guru_utils]`** + 该packages是一个通用的工具类,实现了大部分常用操作大概模块(集合,网络,Math, ui),该库没有引用任何GP,ADS, Firebase相关库,因此老项目可以正常引入 +- **`[guru_spec]`** + 该packages是一个方便生成APP基础信息的一个生成器,这样在配置文件中生成后,将可以将信息生成到代码中,支持flavors,该库依赖GP,ADS, Firebase,因此需要引入GuruApp库 +- **`[guru_platform_data]`** + 该库封装了一些平台相关的原生操作,该库弥补pub.dev上未实现的原生特殊功能 +- **`[soundpool]`** + 该库移植自原有soundpool但由于该库长期不更新,并内部依赖有错误,因此单独抽出来进行适配。 diff --git a/guru_app/LICENSE b/guru_app/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/guru_app/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/guru_app/README.md b/guru_app/README.md new file mode 100644 index 0000000..8b55e73 --- /dev/null +++ b/guru_app/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/guru_app/analysis_options.yaml b/guru_app/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/guru_app/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/guru_app/flavors/spider/guru_spec.yaml b/guru_app/flavors/spider/guru_spec.yaml new file mode 100644 index 0000000..ce968b7 --- /dev/null +++ b/guru_app/flavors/spider/guru_spec.yaml @@ -0,0 +1,125 @@ +app_name: Spider + +# App接入GuruApp的基础信息(下面内容必填) +details: + # 中台接口上报时的app_id,影响中台push接入和后期的中台打点接入(必填) + saas_app_id: spider + + # 针对DynamicLink和Deeplink的判断(必填) + authority: solitaire.fungame.studio + + # 对应Firebase项目中的基础链接(必填) + storage_prefix: https://firebasestorage.googleapis.com/v0/b/solitaire-66fbf.appspot.com/o + + # 对应CDN云控中的默认cdn链接(必填) + default_cdn_prefix: https://cdn1.solitaire.fungame.studio + + # Android的商店链接,后期分享,用户反馈或PubMatic等广告源的使用(必填) + android_gp_url: https://play.google.com/store/apps/details?id=solitaire.patience.card.games.klondike.free + + # IOS的商店链接,后期分享,用户反馈或PubMatic等广告源的使用(必填) + ios_spp_store_url: + + # 隐私协议的链接(必填) + policy_url: https://solitaire.fungame.studio/policy.html + + # 隐私条款的链接(必填) + terms_url: https://solitaire.fungame.studio/termsofservice.html + + # 联系邮箱 (必填) + email_url: card@fungame.studio + +deployment: + # AppProperty Cache的大小,默认是256 + property_cache_size: 512 + + # Paint.enableDithering,默认是true + enable_dithering: false + + # 禁用激励视频,默认是false + disable_rewards_ads: true + + +# 广告配置 +ads_profile: + # Banner广告ID(变现提供) + banner_ad_unit_id: + android: a1dc70299fd5d487 + ios: 97da0e2028ba80b7 + + # Interstitial广告ID(变现提供) + interstitial_ad_unit_id: + android: 25b7c47878fcbf6a + ios: 4e7ba2c4921ecdfb + + # Rewards广告ID(变现提供) + rewards_ad_unit_id: + android: 3cd13a4e5c388e7b + ios: 2a65c75c3ed690b2 + + # Amazon广告的AppId(变现提供) + amz_app_id: + android: 22296b56-f6b3-4bee-9fd1-0cd6d5cc69bc + ios: 9fdfd4c0-3f34-4bd4-b9b4-1f649ff50a2a + + # Amazon广告的Banner Slot Id(变现提供) + banner_amz_slot_id: + android: 3c10ec33-a2bf-44be-ac9f-707853e63ff2 + ios: 7cb36f8a-2953-4f02-a1cb-ec3dfdf33878 + + # Amazon广告的Interstitial Slot Id(变现提供) + interstitial_amz_slot_id: + android: b7fac191-5986-4144-9fdb-691556b2e092 + ios: 82d23cfa-2b5d-4501-bfc3-1cd2b688ed41 + +# attr +# possessive: 购买后永久有效 +# consumable: 可消耗商品 +# subscriptions: 订阅类商品 +iap_profile: + # 去广告的 SKU(名称固定不可变,否则无法适配相关模块的去广告机制,内部sku值可改变) +# no_ads: +# android: so.a.iap.noads.699 +# ios: so.i.iap.noads.699 +# attr: possessive + + # 名字可自定义根据自身产品定义 + # coin200: + # android: so.a.iapc.coin.200 + # ios: so.i.iapc.coin.200 + # attr: consumable + +remote_config: + iads_config: '{"free_s":600,"win_count":4,"scene":"game_start|new_block|p2g|p2h|reset_keep|reset_scs|ads_break|double|nap","sp_scene":"new_block:120;reset_scs:120","retry_min_s":10,"retry_max_s":600,"amazon_enable":false,"imp_gap_s":120}' + + rads_config: '{"win_count":3}' + + bads_config: '{"free_s":180,"win_count":1}' + +# adjust 相关配置 +adjust_profile: + # 对应adjust的appToken,必填项 + app_token: + android: fwbn7l32vpc0 + ios: xxakw3rgxnnk + + # 如果有对应的事件映射在这里统一定义 + event_map: + level_start: + android: hq0xzz + ios: b8khry + + in_app_purchase: + android: yzy3uh + ios: z0gje7 + revenue: true + + level_end: + android: so63k4 + ios: 1p8z5t + + tutorial_complete: + android: 95fu7q + ios: 1p8z5t + + diff --git a/guru_app/guru/guru_spec.yaml b/guru_app/guru/guru_spec.yaml new file mode 100644 index 0000000..4f7092c --- /dev/null +++ b/guru_app/guru/guru_spec.yaml @@ -0,0 +1,431 @@ +app_name: GuruApp + +flavor: "guru_test" + +# App接入GuruApp的基础信息(下面内容必填) +details: + # 中台接口上报时的app_id,影响中台push接入和后期的中台打点接入(必填) + saas_app_id: guruapp + + # 针对DynamicLink和Deeplink的判断(必填) + authority: demo.gurugame.fun + + # 对应Firebase项目中的基础链接(必填) + storage_prefix: https://firebasestorage.googleapis.com/v0/b/example.appspot.com/o + + # 对应CDN云控中的默认cdn链接(必填) + default_cdn_prefix: https://cdn1.example.gurugame.fun + + # Android的商店链接,后期分享,用户反馈或PubMatic等广告源的使用(必填) + android_gp_url: https://play.google.com/store/apps/details?id=app_package_id + + # IOS的商店链接,后期分享,用户反馈或PubMatic等广告源的使用(必填) + ios_spp_store_url: + + # 隐私协议的链接(必填) + policy_url: https://solitaire.fungame.studio/policy.html + + # 隐私条款的链接(必填) + terms_url: https://solitaire.fungame.studio/termsofservice.html + + # 联系邮箱 (必填) + email_url: demo@gurugame.fun + + # Android Package Name (必填) + package_name: guru.app.demo + + # iOS Bundle Id (必填) + bundle_id: guru.app.demo + + # Facebook App Id + facebook_app_id: 123456789 + +deployment: + # AppProperty Cache的大小,默认是256 + property_cache_size: 512 + + # Paint.enableDithering,默认是true + enable_dithering: false + + # 禁用激励视频,默认是false + disable_rewards_ads: true + + # 是否启用 Analytics Statistic 统计 + enable_analytics_statistic: true + + # 是否自动恢复IAP购买数据 + auto_restore_iap: false + + # 初始的游戏币数量,默认是 0 + init_igc: 500 + + # igc(游戏内货币) 验证密钥混,int类型,防止igc被外部修改 + igc_balance_secret: 2654404609 + + # GuruApp Persistent Log 默认10M + log_file_size_limit: 10485760 + + # GuruApp Persistent Log 保存的个数,默认7个 + log_file_count: 7 + + # 使用persistent log的最小等级,最终 >= 该level的日志将会被存储到本地 + # verbose: 0 + # debug: 1 + # info: 2 + # warning: 3 + # error: 4 + # wtf: 5 + # nothing: 6 + persistent_log_level: 1 + + # ios 验证服务器的密码 + ios_validate_receipt_password: aa998877665544332211bb00cc + + + # 被标注的conversion点,在自打点库中将被以Emergency的优先级进行发送 + conversion_events: + - first_rads_rewarded + - level_end_success_1 + - level_end_success_6 + - level_end_success_10 + - level_end_success_12 + - level_end_success_15 + - level_up + - level_up_1 + - level_up_3 + - level_up_5 + - level_up_7 + - level_up_10 + - level_up_12 + - level_up_15 + - tch_ad_rev_roas_001 + - tutorial_complete + + api_connect_timeout: 15000 + + api_receive_timeout: 15000 + + # + # Sandbox lets you test subscription events, such as renewals, state changes, and interrupted purchases, + # without having to wait the length of the subscription duration. Once you added testers in sandbox, + # you can choose a subscription renewal speed for each tester to determine how quickly subscriptions renew. + # By default, accounts are set to a speed equalization of 1 month = 5 minutes, + # but you can slow down or speed up the renewal period, based on the options below. + # Subscriptions renew up to 12 times before auto-renewal turns off on the thirteenth renewal attempt. + # ┌───────────────┬────────────────┬────────────────┬────────────────┬────────────────┬────────────────┐ + # │ Subscription │ Renewal every │ Renewal every │ Renewal every │ Renewal every │ Renewal every │ + # │ Duration │ 3 Minutes │ 5 Minutes │ 15 Minutes │ 30 Minutes │ Hour │ + # ├───────────────┼────────────────┼────────────────┼────────────────┼────────────────┼────────────────┤ + # │ 1 Week │ 3 minutes │ 3 minutes │ 5 minutes │ 10 minutes │ 15 minutes │ + # ├───────────────┼────────────────┼────────────────┼────────────────┼────────────────┼────────────────┤ + # │ 1 Month │ 3 minutes │ 5 minutes │ 15 minutes │ 30 minutes │ 1 hour │ + # ├───────────────┼────────────────┼────────────────┼────────────────┼────────────────┼────────────────┤ + # │ 6 Months │ 18 minutes │ 30 minutes │ 90 minutes │ 3 hours │ 6 hours │ + # ├───────────────┼────────────────┼────────────────┼────────────────┼────────────────┼────────────────┤ + # │ 1 Year │ 36 minutes │ 1 hour │ 3 hours │ 6 hours │ 12 hours │ + # └───────────────┴────────────────┴────────────────┴────────────────┴────────────────┴────────────────┘ + # + # level 1: Renewal every 3 minutes per month + # level 2: Renewal every 5 minutes per month (default) + # level 3: Renewal every 15 minutes per month + # level 4: Renewal every 30 minutes per month + # level 5: Renewal every 1 hour per month + ios_sandbox_subs_renewal_speed: 2 + + # 是否使用广告合规初始化的逻辑,合规初始化是指先收集GDPR在初始化广告 + ads_compliant_initialization: false + + # 自动请求通知栏权限,默认是false + auto_request_notification_permission: false + + # 请求通知栏权限时的提示出发机制 + # rationale: 依赖Android原生的shouldShowRequestRationale返回值来展示对应的Rationale页面 + # request: 依赖请求的次数来展示对应的Rationale页面 + notification_permission_prompt_trigger: rationale + + # 是否追踪通知栏权限的通过率,默认是false。 + # 如果为true时将会上报对应noti_perm_req_`n`和noti_perm_pass_`n` + # n: 表示第几次请求 + # 注意!如果开启了追踪通知栏权限的通过率统计点位,那么firebase打点中将会出现`n`个noti_perm_req_`n`和noti_perm_pass_`n`这两个打点 + # 这里的n的限制需要配置tracking_notification_permission_pass_limit_times + tracking_notification_permission_pass: false + + # 如果追踪通知栏权限的通过率,这个值表示最大的追踪次数,超过最大次数后将不再追踪 + tracking_notification_permission_pass_limit_times: 10 + + # 是否打开GuruAnalytics的策略,默认是false + enabled_guru_analytics_strategy: false + + # 在RewardedAware中调用 showRewardedAd 方法时 + # 在激励视频不可用时,是否允许使用插屏做为替代奖励,默认是 false + # 注意:即使这里设置成 True,在你使用的页面 Controller中,要确保 with InterstitialAware + allow_interstitial_as_alternative_reward: false + + # 在 Banner 广告未成功加载的期间,填充一个内部的广告,可以是一个推广,一个内部的广告。 + # 它不会影响正常的广告展示逻辑,只要 Banner 广告正常加载,都会将其进行隐藏。 + # 因此它的优先级永远不会大于正常的 Banner 广告, 默认值是 false + show_internal_ads_when_banner_unavailable: true + +# 广告配置 +ads_profile: + # Banner广告ID(变现提供) + banner_ad_unit_id: + android: xxxxxxxxxxxxxxxx + ios: xxxxxxxxxxxxxxxx + + # Interstitial广告ID(变现提供) + interstitial_ad_unit_id: + android: xxxxxxxxxxxxxxxx + ios: xxxxxxxxxxxxxxxx + + # Rewards广告ID(变现提供) + rewards_ad_unit_id: + android: xxxxxxxxxxxxxxxx + ios: xxxxxxxxxxxxxxxx + + # Amazon广告的AppId(变现提供) + amz_app_id: + android: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + ios: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + # Amazon广告的Banner Slot Id(变现提供) + banner_amz_slot_id: + android: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + ios: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + # Amazon广告的Interstitial Slot Id(变现提供) + interstitial_amz_slot_id: + android: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + ios: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + rewarded_amz_slot_id: + android: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + ios: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + +remote_config: + # 保留配置,插屏广告相关配置 + iads_config: '{"free_s":600,"win_count":4,"scene":"game_start","sp_scene":"new_block:120;reset_scs:120","retry_min_s":10,"retry_max_s":600,"amazon_enable":false,"imp_gap_s":120}' + + # 保留配置,激励广告相关配置 + rads_config: '{"win_count":3}' + + # 保留配置,Banner广告相关配置 + bads_config: '{"free_s":180,"win_count":1}' + + # 保留配置,打点相关配置 + analytics_config: '{"cap":"firebase|facebook|guru", "init_delay_s": 10}' + +products: + # sku + # 商品的ID,如果同时指定了android和ios,优先级sku为主 + # 支持参数定义,见下面的样例商品theme的sku + # + # android,ios + # 各个平台的SKU,如果指定了sku将忽略这两个选择 + # + # attr + # possessive: 购买后永久有效 + # consumable: 可消耗商品 + # subscriptions: 订阅类商品 + # + # method: + # 购买的方式,支持iap,igc,reward + # iap: 通过IAP购买的商品 + # igc: 通过虚拟游戏币购买的商品 + # reward: 通过奖励的方式获取的商品 + # 如果这个商品支持多种方式购买,可以通过逗号的形式进行串联 + # + # capabilities: + # 表示该商品的能力,现阶段只支持noAds + # + # manifest: + # 指购买该商品后所获得的清单 + # category: + # 该manifest的种类,这个主要用于后面的打点 + # 如果在guru_spec.yaml中定义相同的category,该category的名字必须能保证名字是不相似的, + # 如果出现名字相似的情况下,将会视为冲突,如:noAds和no_ads这类相似的名字都将无法生成 + # details,details1,details2...detailsN + # 指该商品的获取明细,如igc多少个,道具多少个 + # details中必须定义type和amount否则将无法生成 + # details中的type,现阶段系统预定义了igc(表示游戏内货币) + # 如果需要处理模板库不支持的类型,需要在使用模板库的时候添加相应的distributor + # 自定义参数 + # 可以指定不同的参数,也可以引用sku中的参数,见下面的样例商品theme的manifest + # 如果在guru_spec.yaml中定义相同的category的商品,自定义参数必须一至, + # 否则将无法生成 + # + + no_ads: + android: so.a.iap.noads.699 + ios: so.i.iap.noads.699 + attr: asset + method: iap,reward,igc + manifest: + category: "no_ads" + + no_ads_coin_bundle: + android: so.a.iap.noads.coin.799 + ios: so.i.iap.noads.coin.799 + attr: asset + method: iap + capabilities: noAds + manifest: + category: "no_ads" + details: + type: "igc" # in-game currency + amount: 500 + details1: + type: "cup" + amount: 1 + details2: + type: "frag" + amount: 20 + + theme: + sku: "theme_{theme_id}" + method: igc,reward + attr: possessive + manifest: + category: "theme_{1}" + theme_id: "{1}" + + # theme2: + # sku: "theme2_{theme_id}" + # method: igc,reward + # attr: possessive + # manifest: + # category: "theme" + # theme_id: "{1}" + # details: + # type: "theme" + # amount: 1 + # details2: + # type: "theme3" + # amount: 1 + + prop: + sku: "theme_{prop_id}_{pc_id}" + method: igc,reward + attr: possessive + manifest: + category: "prop" + details: + type: "prop" + amount: 1 + theme_id: "{1}" + details2: + type: "pc" + amount: 1 + theme_id: "{2}" + + no_ads2: + android: so.a.iap.noads.699 + ios: so.i.iap.noads.699 + attr: possessive + method: iap + capabilities: noAds + manifest: + category: no_ads + details: + type: no_ads + amount: 1 + ignore_sales: true + + coin200: + android: so.a.iapc.coin.200 + ios: so.i.iapc.coin.200 + method: iap + attr: consumable + points: true + manifest: + category: coin + details: + type: coin + amount: 200 + + stage_pack: + android: so.a.iap.stage.1 + ios: so.i.iap.stage.1 + attr: consumable + method: iap + manifest: + category: "stage_1" + details: + type: "stage" # in-game currency + amount: 1 + stage: 1 + + premium_week: + android: "m2.a.sub.premium" + ios: "m2.i.sub.premium.p1w" + attr: subscriptions + method: iap + capabilities: noAds + base_plan: weekly + group: premium + offers: + - freetrial + - discount + manifest: + category: "sub" + details: + type: "igc" + amount: 8000 + + premium_year: + android: "m2.a.sub.premium" + ios: "m2.i.sub.premium.p1y" + attr: subscriptions + method: iap + capabilities: noAds + base_plan: yearly + group: premium + offers: + - freetrial + - discount + manifest: + category: "sub" + details: + type: "igc" + amount: 16000 + + theme_mul: + sku: "theme_{category}_{theme_id}" + attr: possessive + method: igc + manifest: + category: "{1}" + theme_id: "{2}" + cate: "{1}" + +# adjust 相关配置 +adjust_profile: + # 对应adjust的appToken,必填项 + app_token: + android: testapptoken + ios: testapptoken + + # 如果有对应的事件映射在这里统一定义 + event_map: + level_start: + android: hq0xzz + ios: b8khry + + iap_purchase: + android: yzy3uh + ios: z0gje7 + params: true + sub_purchase: + android: yzy3uh + ios: z0gje7 + params: true + + level_end: + android: so63k4 + ios: 1p8z5t + + tutorial_complete: + android: 95fu7q + ios: 1p8z5t + diff --git a/guru_app/guru/spider/guru_spec.yaml b/guru_app/guru/spider/guru_spec.yaml new file mode 100644 index 0000000..9794155 --- /dev/null +++ b/guru_app/guru/spider/guru_spec.yaml @@ -0,0 +1,151 @@ +app_name: Spider + +flavor: Spider + +# App接入GuruApp的基础信息(下面内容必填) +details: + # 中台接口上报时的app_id,影响中台push接入和后期的中台打点接入(必填) + saas_app_id: spider + + # 针对DynamicLink和Deeplink的判断(必填) + authority: solitaire.fungame.studio + + # 对应Firebase项目中的基础链接(必填) + storage_prefix: https://firebasestorage.googleapis.com/v0/b/solitaire-66fbf.appspot.com/o + + # 对应CDN云控中的默认cdn链接(必填) + default_cdn_prefix: https://cdn1.solitaire.fungame.studio + + # Android的商店链接,后期分享,用户反馈或PubMatic等广告源的使用(必填) + android_gp_url: https://play.google.com/store/apps/details?id=solitaire.patience.card.games.klondike.free + + # IOS的商店链接,后期分享,用户反馈或PubMatic等广告源的使用(必填) + ios_spp_store_url: + + # 隐私协议的链接(必填) + policy_url: https://solitaire.fungame.studio/policy.html + + # 隐私条款的链接(必填) + terms_url: https://solitaire.fungame.studio/termsofservice.html + + # 联系邮箱 (必填) + email_url: card@fungame.studio + + # Android Package Name + package_name: guru.app.demo + + # iOS Bundle Id + bundle_id: guru.app.demo + + # Facebook App Id + facebook_app_id: 987654321 + +deployment: + # AppProperty Cache的大小,默认是256 + property_cache_size: 512 + + # Paint.enableDithering,默认是true + enable_dithering: false + + # 禁用激励视频,默认是false + disable_rewards_ads: true + + +# 广告配置 +ads_profile: + # Banner广告ID(变现提供) + banner_ad_unit_id: + android: a1dc70299fd5d487 + ios: 97da0e2028ba80b7 + + # Interstitial广告ID(变现提供) + interstitial_ad_unit_id: + android: 25b7c47878fcbf6a + ios: 4e7ba2c4921ecdfb + + # Rewards广告ID(变现提供) + rewards_ad_unit_id: + android: 3cd13a4e5c388e7b + ios: 2a65c75c3ed690b2 + + # Amazon广告的AppId(变现提供) + amz_app_id: + android: 22296b56-f6b3-4bee-9fd1-0cd6d5cc69bc + ios: 9fdfd4c0-3f34-4bd4-b9b4-1f649ff50a2a + + # Amazon广告的Banner Slot Id(变现提供) + banner_amz_slot_id: + android: 3c10ec33-a2bf-44be-ac9f-707853e63ff2 + ios: 7cb36f8a-2953-4f02-a1cb-ec3dfdf33878 + + # Amazon广告的Interstitial Slot Id(变现提供) + interstitial_amz_slot_id: + android: b7fac191-5986-4144-9fdb-691556b2e092 + ios: 82d23cfa-2b5d-4501-bfc3-1cd2b688ed41 + +# attr +# asset(or possessive): 购买后永久有效 +# consumable: 可消耗商品 +# subscriptions: 订阅类商品 + +# capabilities +# noAds +products: + # 去广告的 SKU(名称固定不可变,否则无法适配相关模块的去广告机制,内部sku值可改变) + no_ads: + android: so.a.iap.noads.699 + ios: so.i.iap.noads.699 + attr: possessive + capabilities: noAds + + # 名字可自定义根据自身产品定义 + coin200: + android: so.a.iapc.coin.200 + ios: so.i.iapc.coin.200 + attr: consumable + + theme: + sku: "theme_{theme_id}" + method: igc,reward + attr: possessive + manifest: + category: "{1}" + theme_id: "{1}" + details: + type: "theme" + amount: 1 + +remote_config: + iads_config: '{"free_s":600,"win_count":4,"scene":"game_start|new_block|p2g|p2h|reset_keep|reset_scs|ads_break|double|nap","sp_scene":"new_block:120;reset_scs:120","retry_min_s":10,"retry_max_s":600,"amazon_enable":false,"imp_gap_s":120}' + + rads_config: '{"win_count":3}' + + bads_config: '{"free_s":180,"win_count":1}' + +# adjust 相关配置 +adjust_profile: + # 对应adjust的appToken,必填项 + app_token: + android: fwbn7l32vpc0 + ios: xxakw3rgxnnk + + # 如果有对应的事件映射在这里统一定义 + event_map: + level_start: + android: hq0xzz + ios: b8khry + + in_app_purchase: + android: yzy3uh + ios: z0gje7 + revenue: true + + level_end: + android: so63k4 + ios: 1p8z5t + + tutorial_complete: + android: 95fu7q + ios: 1p8z5t + + diff --git a/guru_app/lib/account/account_auth_extension.dart b/guru_app/lib/account/account_auth_extension.dart new file mode 100644 index 0000000..06e5155 --- /dev/null +++ b/guru_app/lib/account/account_auth_extension.dart @@ -0,0 +1,51 @@ +/// Created by Haoyi on 2021/7/26 + +part of "account_manager.dart"; + +extension AccountAuthExtension on AccountManager { + Future _authenticate(SaasUser saasUser, + {bool canRefreshFirebaseToken = true}) async { + User? firebaseUser; + SaasUser newSaasUser = saasUser; + firebaseUser = await _authenticateFirebase(saasUser).catchError((error) { + Log.e("_authenticateFirebase error! $error", tag: "Account"); + return null; + }); + if (firebaseUser == null && canRefreshFirebaseToken) { + try { + newSaasUser = await _refreshFirebaseToken(saasUser); + return _authenticate(newSaasUser, canRefreshFirebaseToken: false); + } catch (error, stacktrace) { + return AccountAuth(saasUser, null); + } + } + + return AccountAuth(newSaasUser, firebaseUser); + } + + Future _refreshFirebaseToken(SaasUser oldSaasUser) async { + return await GuruApi.instance + .renewFirebaseToken() + .then((tokenData) => oldSaasUser.copyWith(firebaseToken: tokenData.firebaseToken)); + } + + Future _authenticateFirebase(SaasUser saasUser) async { + int retry = 0; + dynamic lastError; + while (retry < 1) { + try { + Log.i("[$retry] _authenticateFirebase:${saasUser.firebaseToken}", tag: "Account"); + + return await FirebaseAuth.instance + .signInWithCustomToken(saasUser.firebaseToken) + .then((result) => result.user); + } catch (error, stacktrace) { + await Future.delayed(const Duration(milliseconds: 600)); + retry++; + Log.i("[$retry] _authenticateFirebase error :$error, $stacktrace", tag: "Account"); + lastError = error; + } + } + throw lastError ?? ("_authenticateFirebase error!"); + } +} diff --git a/guru_app/lib/account/account_data_store.dart b/guru_app/lib/account/account_data_store.dart new file mode 100644 index 0000000..53fc9bd --- /dev/null +++ b/guru_app/lib/account/account_data_store.dart @@ -0,0 +1,124 @@ +import 'dart:convert'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:guru_app/account/model/account_profile.dart'; +import 'package:guru_app/account/model/user.dart'; +import 'package:guru_app/analytics/guru_analytics.dart'; +import 'package:guru_app/api/guru_api.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:guru_app/property/app_property.dart'; +import 'package:guru_utils/device/device_info.dart'; +import 'package:guru_utils/extensions/extensions.dart'; + +import 'package:http/http.dart' as http; + +/// Created by Haoyi on 6/3/21 +/// +enum AccountDataStatus { idle, initializing, initialized, waiting, error } + +class AccountDataStore { + static final AccountDataStore instance = AccountDataStore._(); + + final BehaviorSubject _deviceInfoSubject = BehaviorSubject.seeded(null); + final BehaviorSubject _saasUserSubject = BehaviorSubject.seeded(null); + final BehaviorSubject _firebaseUser = BehaviorSubject.seeded(null); + final BehaviorSubject _accountProfile = BehaviorSubject.seeded(null); + final BehaviorSubject _accountDataStatus = + BehaviorSubject.seeded(AccountDataStatus.idle); + int initRetryCount = 0; + + Stream get observableAccountProfile => _accountProfile.stream; + + // final EnvConfig envConfig; + + AccountDataStore._(); + + String? get saasToken => _saasUserSubject.value?.token; + + String? get uid => _saasUserSubject.value?.uid; + + AccountProfile? get accountProfile => _accountProfile.value; + + String? get nickname => _accountProfile.value?.nickname; + + String? get countryCode => _accountProfile.value?.countryCode; + + SaasUser? get user => _saasUserSubject.value; + + String? get avatar => _accountProfile.value?.avatar; + + DeviceInfo? get currentDevice => _deviceInfoSubject.value; + + AccountDataStatus get accountDataStatus => _accountDataStatus.value; + + bool get initialized => _accountDataStatus.value == AccountDataStatus.initialized; + + Stream get observableInitialized => + _accountDataStatus.stream.map((status) => status == AccountDataStatus.initialized); + + Stream get observableSaasUser => _saasUserSubject.stream; + + void dispose() { + _deviceInfoSubject.close(); + _saasUserSubject.close(); + _firebaseUser.close(); + _accountProfile.close(); + } + + Future signInAnonymousInLocked() async { + // 这里需要使用原始的http post请求。否则这里将会死锁DIO所有请求 + final secret = await AppProperty.getInstance().getAnonymousSecretKey(); + final headers = { + "X-APP-ID": GuruApp.instance.details.saasAppId, + "content-type": "application/json" + }; + try { + final uri = Uri.parse("${GuruApi.saasApiHost}/auth/api/v1/tokens/provider/secret"); + final response = await http + .post(uri, + headers: headers, + body: jsonEncode(AnonymousLoginReqBody(secret: secret)), + encoding: utf8) + .timeout(const Duration(seconds: 30)); + final data = const Utf8Decoder().convert(response.bodyBytes); + if (data.isNotEmpty) { + final result = json.decode(data); + return SaasUser.fromJson(result["data"]); + } + } catch (error, stacktrace) { + Log.v("signInAnonymousInLocked error:$error", tag: "Account"); + } + return null; + } + + Future refreshAuth() async { + final saasUser = await signInAnonymousInLocked(); + if (saasUser != null) { + updateSaasUser(saasUser); + } + } + + void updateDeviceInfo(DeviceInfo deviceInfo) { + _deviceInfoSubject.addEx(deviceInfo); + } + + void updateSaasUser(SaasUser saasUser) { + _saasUserSubject.addEx(saasUser); + + if (saasUser.createAtTimestamp > 0) { + GuruAnalytics.instance + .setUserProperty("user_created_timestamp", saasUser.createAtTimestamp.toString()); + } + } + + void updateFirebaseUser(User user) { + _firebaseUser.addEx(user); + } + + void updateAccountProfile(AccountProfile profile) { + _accountProfile.addEx(profile); + } + + bool transitionTo(AccountDataStatus status, {AccountDataStatus? expectStatus}) { + return _accountDataStatus.addIfChanged(status); + } +} diff --git a/guru_app/lib/account/account_manager.dart b/guru_app/lib/account/account_manager.dart new file mode 100644 index 0000000..1527813 --- /dev/null +++ b/guru_app/lib/account/account_manager.dart @@ -0,0 +1,161 @@ +import 'dart:async'; + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:guru_app/account/account_data_store.dart'; +import 'package:guru_app/account/model/account.dart'; +import 'package:guru_app/account/model/account_profile.dart'; +import 'package:guru_app/account/model/user.dart'; +import 'package:guru_app/analytics/guru_analytics.dart'; +import 'package:guru_app/api/guru_api.dart'; +import 'package:guru_app/firebase/firebase.dart'; +import 'package:guru_app/firebase/firestore/firestore_manager.dart'; +import 'package:guru_app/property/app_property.dart'; +import 'package:guru_app/property/settings/guru_settings.dart'; +import 'package:guru_utils/collection/collectionutils.dart'; +import 'package:guru_utils/core/ext.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_utils/device/device_info.dart'; +import 'package:guru_utils/device/device_utils.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/network/network_utils.dart'; + +/// Created by Haoyi on 6/3/21 +/// +/// +part "account_service_extension.dart"; + +part "account_auth_extension.dart"; + +class ModifyNicknameException implements Exception { + final String? message; + final dynamic cause; + + ModifyNicknameException(this.message, {this.cause}); + + @override + String toString() { + return "ModifyNicknameException: $message cause:$cause"; + } +} + +class ModifyLevelException implements Exception { + final String? message; + final dynamic cause; + + ModifyLevelException(this.message, {this.cause}); + + @override + String toString() { + return "ModifyLevelException: $message cause:$cause"; + } +} + +class AccountManager { + final AccountDataStore accountDataStore; + + // final FirestoreService firestoreService; + + Timer? retryTimer; + + static AccountManager instance = AccountManager(); + + AccountManager() : accountDataStore = AccountDataStore.instance; + + Future init({Completer? completer}) async { + try { + final result = accountDataStore.transitionTo(AccountDataStatus.initializing); + if (!result) { + Log.w( + "init account error, current initializing! please wait result! retry[${accountDataStore.initRetryCount}]", + tag: "Account"); + return; + } + retryTimer?.cancel(); + final account = await AppProperty.getInstance().loadAccount(); + final restoreResult = await _restoreAccount(account); + if (!restoreResult) { + Log.v("init account error: restoreAccount error! retry[${accountDataStore.initRetryCount}]", + tag: "Account"); + _retry(); + } else { + accountDataStore.initRetryCount = 0; + accountDataStore.transitionTo(AccountDataStatus.initialized); + Log.v("init account success!", tag: "Account"); + } + } catch (error, stacktrace) { + completer?.complete(error); + Log.v("init account error retry[${accountDataStore.initRetryCount}]:$error $stacktrace", + tag: "Account"); + _retry(); + } + completer?.complete(true); + } + + void _retry() { + final intervalSeconds = (accountDataStore.initRetryCount * 2 + 8).clamp(8, 30); + retryTimer?.cancel(); + accountDataStore.transitionTo(AccountDataStatus.waiting); + retryTimer = Timer(Duration(seconds: intervalSeconds), () { + init(); + accountDataStore.initRetryCount++; + }); + } + + Future updateLocalProfile(Map modifiedJson) async { + modifiedJson[AccountProfile.dirtyField] = true; + final dirtyAccountProfile = accountDataStore.accountProfile?.merge(modifiedJson) ?? + AccountProfile.fromJson(modifiedJson); + AppProperty.getInstance().setAccountProfile(dirtyAccountProfile); + accountDataStore.updateAccountProfile(dirtyAccountProfile); + } + + Future modifyProfile( + {String? nickname, + String? avatar, + String? countryCode, + Map userData = const {}}) async { + int retryCount = 2; + Log.i("modifyProfile $nickname $avatar $countryCode", syncFirebase: true, tag: "Account"); + if (nickname == null && avatar == null && countryCode == null && userData.isEmpty) { + return false; + } + final now = DateTimeUtils.currentTimeInMillis(); + final modifiedJson = CollectionUtils.filterOutNulls({ + AccountProfile.uidField: accountDataStore.uid, + AccountProfile.nicknameField: nickname, + AccountProfile.countryField: countryCode?.toLowerCase(), + AccountProfile.avatarField: avatar, + AccountProfile.updateAtField: now, + AccountProfile.versionField: GuruSettings.instance.version.get(), + AccountProfile.roleField: + GuruSettings.instance.debugMode.get() == true ? UserAttr.tester : UserAttr.real, + AccountProfile.dirtyField: true, + ...userData + }); + await updateLocalProfile(modifiedJson); + + while ((await NetworkUtils.isNetworkConnected()) && retryCount-- > 0) { + final accountProfile = + await FirestoreManager.instance.modifyProfile(modifiedJson).onError((error, stackTrace) { + Log.i("modifyProfile error!:$error"); + GuruAnalytics.instance.logException(ModifyLevelException("modifyProfile error!:$error"), + stacktrace: stackTrace); + return null; + }); + if (accountProfile != null) { + Log.i("modifyProfile success! $accountProfile", tag: "Account"); + AppProperty.getInstance().setAccountProfile(accountProfile); + accountDataStore.updateAccountProfile(accountProfile); + return true; + } else { + Log.i("[$retryCount] modify profile error!", tag: "Account"); + await authenticate().timeout(const Duration(seconds: 15)).catchError((error, stackTrace) { + Log.i("re-authenticate error:$error", stackTrace: stackTrace, tag: "Account"); + }); + await Future.delayed(const Duration(seconds: 1)); + } + } + + return false; + } +} diff --git a/guru_app/lib/account/account_service_extension.dart b/guru_app/lib/account/account_service_extension.dart new file mode 100644 index 0000000..4fd7bfa --- /dev/null +++ b/guru_app/lib/account/account_service_extension.dart @@ -0,0 +1,151 @@ +/// Created by Haoyi on 6/3/21 + +part of "account_manager.dart"; + +extension AccountServiceExtension on AccountManager { + Future _restoreAccount(Account account) async { + SaasUser? saasUser = account.saasUser; + Log.d("restoreAccount $saasUser", tag: "Account"); + saasUser ??= await signInWithAnonymous().catchError((error, stacktrace) { + Log.v("signInWithAnonymous error:$error, $stacktrace"); + return null; + }); + + Log.v("_restoreAccount saasUser:$saasUser", tag: "Account"); + final device = account.device; + if (device != null) { + _updateDevice(device); + } + + final accountProfile = account.accountProfile; + if (accountProfile != null) { + _updateAccountProfile(accountProfile); + } + + if (saasUser != null) { + _updateSaasUser(saasUser); + await _verifyOrReportAuthDevice(saasUser); + final auth = await authenticate(); + if (auth == null) { + return false; + } + if (accountProfile != null) { + await _checkOrUploadAccountProfile(accountProfile); + } + return true; + } else { + return false; + } + } + + Future authenticate() async { + final saasUser = accountDataStore.user; + if (saasUser == null) { + return null; + } + try { + final auth = await _authenticate(saasUser); + final newSaasUser = auth.user; + if (newSaasUser != null && !saasUser.isSame(newSaasUser)) { + _updateSaasUser(newSaasUser); + } + if (auth.firebaseUser != null) { + _updateFirebaseUser(auth.firebaseUser!); + Log.i("_updateFirebaseUser success!", tag: "Account"); + } + return auth; + } catch (error, stacktrace) { + GuruAnalytics.instance.logException(error, stacktrace: stacktrace); + } + return null; + } + + Future _buildDevice(SaasUser saasUser) async { + final DeviceInfo? deviceInfo = await AppProperty.getInstance().getAccountDevice(); + final firebasePushToken = await RemoteMessagingManager.instance.getToken(); + + if (firebasePushToken != null) { + final deviceId = await AppProperty.getInstance().getDeviceId(); + final newDeviceInfo = await DeviceUtils.buildDeviceInfo( + deviceId: deviceId, firebasePushToken: firebasePushToken, uid: saasUser.uid); + return DeviceTrack(newDeviceInfo, deviceInfo); + } + return DeviceTrack(null, deviceInfo); + } + + Future signInWithAnonymous() async { + final anonymousSecretKey = await AppProperty.getInstance().getAnonymousSecretKey(); + return GuruApi.instance.signInWithAnonymous(secret: anonymousSecretKey); + } + + Future _verifyOrReportAuthDevice(SaasUser saasUser) async { + final deviceTrack = await _buildDevice(saasUser); + final latestReportDeviceTimestamp = + await AppProperty.getInstance().getLatestReportDeviceTimestamp(); + final elapsedInterval = DateTimeUtils.currentTimeInMillis() - latestReportDeviceTimestamp; + final isChanged = (elapsedInterval > DateTimeUtils.sixHourInMillis) || deviceTrack.isChanged; + final reportDevice = deviceTrack.device; + final deviceId = deviceTrack.device?.deviceId ?? ""; + if (deviceId.isNotEmpty) { + GuruAnalytics.instance.setDeviceId(deviceId); + } + if (isChanged && reportDevice?.isValid == true && saasUser.isValid == true) { + final result = await GuruApi.instance.reportDevice(reportDevice!).then((_) { + return true; + }).catchError((error) { + Log.i("reportDevice error:$error", tag: "Account"); + return false; + }); + if (result) { + reportDevice.dumpDevice(msg: "REPORT DEVICE SUCCESS"); + _updateDevice(reportDevice); + AppProperty.getInstance() + .setLatestReportDeviceTimestamp(DateTimeUtils.currentTimeInMillis()); + AppProperty.getInstance().setAccountDevice(reportDevice); + } + } + } + + Future _checkOrUploadAccountProfile(AccountProfile accountProfile) async { + bool upload = accountProfile.dirty; + + String? changedCountryCode = DeviceUtils.buildLocaleInfo().countryCode.toLowerCase(); + if (DartExt.isBlank(changedCountryCode) || accountProfile.countryCode == changedCountryCode) { + changedCountryCode = null; + } else { + upload = true; + } + + Log.d( + "_checkOrUploadAccountProfile dirty:${accountProfile.dirty} upload:$upload $changedCountryCode", + tag: "Account"); + if (upload) { + await modifyProfile(countryCode: changedCountryCode); + } + } + + void refreshFcmToken() { + final saasUser = accountDataStore.user; + if (saasUser != null) { + _verifyOrReportAuthDevice(saasUser); + } + } + + void _updateDevice(DeviceInfo device) { + accountDataStore.updateDeviceInfo(device); + } + + void _updateSaasUser(SaasUser saasUser) { + accountDataStore.updateSaasUser(saasUser); + AppProperty.getInstance().setAccountSaasUser(saasUser); + GuruAnalytics.instance.setUserId(saasUser.uid); + } + + void _updateFirebaseUser(User user) { + accountDataStore.updateFirebaseUser(user); + } + + void _updateAccountProfile(AccountProfile accountProfile) { + accountDataStore.updateAccountProfile(accountProfile); + } +} diff --git a/guru_app/lib/account/model/account.dart b/guru_app/lib/account/model/account.dart new file mode 100644 index 0000000..594be12 --- /dev/null +++ b/guru_app/lib/account/model/account.dart @@ -0,0 +1,40 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:guru_app/account/model/account_profile.dart'; +import 'package:guru_app/account/model/user.dart'; +import 'package:guru_utils/device/device_info.dart'; + + +/// Created by Haoyi on 6/3/21 + +class Account { + final SaasUser? saasUser; + final DeviceInfo? device; + final AccountProfile? accountProfile; + final User? firebaseUser; + + String? get uid => saasUser?.uid; + + String? get nickname => accountProfile?.nickname; + + Account.restore({this.saasUser, this.device, this.accountProfile, this.firebaseUser}); +} + +class AccountAuth { + final SaasUser? user; + final User? firebaseUser; + + AccountAuth(this.user, this.firebaseUser); + + bool get isValid => uid != null && uid != ""; + + String? get saasToken => user?.token; + + String? get uid => user?.uid; + + bool get existsFirebaseUser => firebaseUser != null; + + @override + String toString() { + return 'AccountAuth{user: $user, firebaseUser: $firebaseUser}'; + } +} diff --git a/guru_app/lib/account/model/account_profile.dart b/guru_app/lib/account/model/account_profile.dart new file mode 100644 index 0000000..5d5d887 --- /dev/null +++ b/guru_app/lib/account/model/account_profile.dart @@ -0,0 +1,135 @@ +import 'package:json_annotation/json_annotation.dart'; + +/// Created by Haoyi on 2021/7/28 +/// + +part "account_profile.g.dart"; + +class AccountRole { + static const normal = 0; + static const tester = 10; + static const machine = 100; +} + +// 这里没有使用genericArgumentFactories是外部构造方法都需要单独提供解析器。 +// 这样做的好处是可以flatten到同级数据 +@JsonSerializable() +class AccountProfile { + static const String uidField = "uid"; + static const String nicknameField = "nickname"; + static const String countryField = "country"; + static const String bestScoreField = "score"; + static const String avatarField = "avatar"; + static const String versionField = "ver"; + static const String updateAtField = "upt"; + static const String dirtyField = "dirty"; + static const String roleField = "role"; + + static final _generalFieldSet = { + uidField, + nicknameField, + countryField, + bestScoreField, + avatarField, + versionField, + updateAtField, + dirtyField, + roleField + }; + + @JsonKey(name: uidField, defaultValue: "") + final String uid; + + @JsonKey(name: nicknameField, defaultValue: "") + final String nickname; + + @JsonKey(name: countryField, defaultValue: "") + final String countryCode; + + @JsonKey(name: avatarField, defaultValue: "") + final String avatar; + + @JsonKey(name: versionField, defaultValue: "") + final String version; + + @JsonKey(name: dirtyField, defaultValue: false) + final bool dirty; + + @JsonKey(name: updateAtField, defaultValue: 0) + final int updateAt; + + @JsonKey(name: roleField, defaultValue: 0) + final int role; + + @JsonKey(ignore: true) + final Map userData; + + const AccountProfile._({this.uid = "", + this.nickname = "", + this.countryCode = "", + this.avatar = "avatar_1", + this.version = "", + this.dirty = false, + this.role = AccountRole.normal, + this.userData = const {}, + this.updateAt = 0}); + + static const AccountProfile empty = AccountProfile._(); + + AccountProfile({this.uid = "", + this.nickname = "", + this.countryCode = "", + this.avatar = "avatar_1", + this.version = "", + this.dirty = false, + this.role = AccountRole.normal, + Map userData = const {}, + this.updateAt = 0}) : userData = Map.from(userData); + + + factory AccountProfile.fromJson(Map json) => + _$AccountProfileFromJson(json) + ..userData.addAll(_validateUserData(json, direct: false)); + + Map toJson() => + _$AccountProfileToJson(this) + ..addAll(_validateUserData(userData)); + + static Map _validateUserData(Map data, {bool direct = true}) { + return (direct ? data : Map.from(data)) + ..removeWhere((key, value) => _generalFieldSet.contains(key)); + } + + AccountProfile copyWith({String? nickname, + String? countryCode, + String? avatar, + String? version, + bool? dirty, + int? role, + Map? userData, + bool mergeUserData = true}) { + final changedUserData = {if (mergeUserData) ...this.userData}; + if (userData != null) { + changedUserData.addAll(userData); + } + return AccountProfile( + uid: uid, + nickname: nickname ?? this.nickname, + countryCode: countryCode ?? this.countryCode, + avatar: avatar ?? this.avatar, + version: version ?? this.version, + role: role ?? this.role, + userData: changedUserData, + dirty: dirty ?? this.dirty); + } + + AccountProfile merge(Map replaceJson) { + return AccountProfile.fromJson(toJson() + ..addAll(replaceJson)); + } + + @override + String toString() { + return 'AccountProfile{nickname: $nickname, countryCode: $countryCode}'; + } +} diff --git a/guru_app/lib/account/model/account_profile.g.dart b/guru_app/lib/account/model/account_profile.g.dart new file mode 100644 index 0000000..f31ad18 --- /dev/null +++ b/guru_app/lib/account/model/account_profile.g.dart @@ -0,0 +1,31 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'account_profile.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AccountProfile _$AccountProfileFromJson(Map json) => + AccountProfile( + uid: json['uid'] as String? ?? '', + nickname: json['nickname'] as String? ?? '', + countryCode: json['country'] as String? ?? '', + avatar: json['avatar'] as String? ?? '', + version: json['ver'] as String? ?? '', + dirty: json['dirty'] as bool? ?? false, + role: json['role'] as int? ?? 0, + updateAt: json['upt'] as int? ?? 0, + ); + +Map _$AccountProfileToJson(AccountProfile instance) => + { + 'uid': instance.uid, + 'nickname': instance.nickname, + 'country': instance.countryCode, + 'avatar': instance.avatar, + 'ver': instance.version, + 'dirty': instance.dirty, + 'upt': instance.updateAt, + 'role': instance.role, + }; diff --git a/guru_app/lib/account/model/user.dart b/guru_app/lib/account/model/user.dart new file mode 100644 index 0000000..af8844b --- /dev/null +++ b/guru_app/lib/account/model/user.dart @@ -0,0 +1,99 @@ +import 'package:json_annotation/json_annotation.dart'; + +/// Created by Haoyi on 6/3/21 + +part 'user.g.dart'; + +@JsonSerializable() +class SaasUser { + @JsonKey(name: 'uid', defaultValue: "") + final String uid; + + @JsonKey(name: 'token', defaultValue: "") + final String token; + + @JsonKey(name: 'firebaseToken', defaultValue: "") + final String firebaseToken; + + @JsonKey(name: 'createdAtTimestamp', defaultValue: 0) + final int createAtTimestamp; + + // bool get isAnonymous => (type == null) || (type == LOGIN_WITH_ANONYMOUS); + + bool get isValid => + (uid != "") && (token.isNotEmpty == true) && (firebaseToken.isNotEmpty == true); + + SaasUser( + {required this.uid, + required this.token, + required this.firebaseToken, + this.createAtTimestamp = 0}); + + factory SaasUser.fromJson(Map json) => _$SaasUserFromJson(json); + + Map toJson() => _$SaasUserToJson(this); + + SaasUser copyWith({String? firebaseToken, String? token}) { + return SaasUser( + uid: uid, token: token ?? this.token, firebaseToken: firebaseToken ?? this.firebaseToken); + } + + bool isSame(SaasUser? user) { + return uid == user?.uid && + token == user?.token && + firebaseToken == user?.firebaseToken && + createAtTimestamp == user?.createAtTimestamp; + } + + @override + String toString() { + return 'SaasUser{uid: $uid, token: $token, firebaseToken: $firebaseToken, createAtTimestamp: $createAtTimestamp}'; + } +} + +// 匿名登录请求数据 +@JsonSerializable() +class AnonymousLoginReqBody { + @JsonKey(name: 'secret', defaultValue: "") + final String secret; + + AnonymousLoginReqBody({required this.secret}); + + // @override + // String toString() { + // return "{secret: '$secret'}"; + // } + + factory AnonymousLoginReqBody.fromJson(Map json) => + _$AnonymousLoginReqBodyFromJson(json); + + Map toJson() => _$AnonymousLoginReqBodyToJson(this); +} + +@JsonSerializable() +class FirebaseTokenData { + @JsonKey(name: 'uid', defaultValue: "") + final String uid; + + @JsonKey(name: 'firebaseToken', defaultValue: "") + final String firebaseToken; + + FirebaseTokenData({required this.uid, required this.firebaseToken}); + + factory FirebaseTokenData.fromJson(Map json) => + _$FirebaseTokenDataFromJson(json); + + Map toJson() => _$FirebaseTokenDataToJson(this); + + @override + String toString() { + return 'FirebaseTokenData{uid: $uid, firebaseToken: $firebaseToken}'; + } +} + + +class UserAttr { + static const real = 0; + static const tester = 10; + static const machine = 100; +} \ No newline at end of file diff --git a/guru_app/lib/account/model/user.g.dart b/guru_app/lib/account/model/user.g.dart new file mode 100644 index 0000000..ff09843 --- /dev/null +++ b/guru_app/lib/account/model/user.g.dart @@ -0,0 +1,45 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SaasUser _$SaasUserFromJson(Map json) => SaasUser( + uid: json['uid'] as String? ?? '', + token: json['token'] as String? ?? '', + firebaseToken: json['firebaseToken'] as String? ?? '', + createAtTimestamp: json['createdAtTimestamp'] as int? ?? 0, + ); + +Map _$SaasUserToJson(SaasUser instance) => { + 'uid': instance.uid, + 'token': instance.token, + 'firebaseToken': instance.firebaseToken, + 'createdAtTimestamp': instance.createAtTimestamp, + }; + +AnonymousLoginReqBody _$AnonymousLoginReqBodyFromJson( + Map json) => + AnonymousLoginReqBody( + secret: json['secret'] as String? ?? '', + ); + +Map _$AnonymousLoginReqBodyToJson( + AnonymousLoginReqBody instance) => + { + 'secret': instance.secret, + }; + +FirebaseTokenData _$FirebaseTokenDataFromJson(Map json) => + FirebaseTokenData( + uid: json['uid'] as String? ?? '', + firebaseToken: json['firebaseToken'] as String? ?? '', + ); + +Map _$FirebaseTokenDataToJson(FirebaseTokenData instance) => + { + 'uid': instance.uid, + 'firebaseToken': instance.firebaseToken, + }; diff --git a/guru_app/lib/ads/ads_global_property.dart b/guru_app/lib/ads/ads_global_property.dart new file mode 100644 index 0000000..f38e2fb --- /dev/null +++ b/guru_app/lib/ads/ads_global_property.dart @@ -0,0 +1,12 @@ +part of 'ads_manager.dart'; + +/// Created by Haoyi on 2022/4/28 + +extension AdsGlobalProperty on AdsManager { + int get latestFullscreenAdsHiddenTimestamps => + adsGlobalProperties["latestFullscreenAdsHiddenTimestamps"] as int? ?? 0; + + set latestFullscreenAdsHiddenTimestamps(int ts) { + adsGlobalProperties["latestFullscreenAdsHiddenTimestamps"] = ts; + } +} diff --git a/guru_app/lib/ads/ads_manager.dart b/guru_app/lib/ads/ads_manager.dart new file mode 100644 index 0000000..e02c4a6 --- /dev/null +++ b/guru_app/lib/ads/ads_manager.dart @@ -0,0 +1,636 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:ui'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:guru_app/account/model/user.dart'; +import 'package:guru_app/ads/applovin/banner/applovin_banner_ads.dart'; +import 'package:guru_app/ads/core/ads.dart'; +import 'package:guru_app/ads/core/ads_config.dart'; +import 'package:guru_app/ads/core/ads_impression.dart'; +import 'package:guru_app/ads/core/strategy/interstitial/max_strategy_interstitial_ads.dart'; +import 'package:guru_app/financial/asset/assets_model.dart'; +import 'package:guru_app/financial/asset/assets_store.dart'; +import 'package:guru_app/financial/iap/iap_manager.dart'; +import 'package:guru_app/financial/iap/iap_model.dart'; +import 'package:guru_app/firebase/remoteconfig/remote_config_manager.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:guru_utils/lifecycle/lifecycle_manager.dart'; +import 'package:guru_app/property/app_property.dart'; +import 'package:guru_app/property/property_keys.dart'; +import 'package:guru_app/property/settings/guru_settings.dart'; +import 'package:guru_applovin_flutter/guru_applovin_flutter.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/tuple/tuple.dart'; +import 'package:guru_utils/ads/ads.dart'; + +import 'applovin/interstitial/applovin_interstitial_ads.dart'; +import 'applovin/rewarded/applovin_rewarded_ads.dart'; +import 'utils/ads_exception.dart'; +import 'package:device_info_plus/device_info_plus.dart'; + +part 'ads_global_property.dart'; + +/// Created by Haoyi on 2022/3/2 + +class AdsManager extends AdsManagerDelegate { + static final AdsManager _instance = AdsManager._(); + + static AdsManager get instance => _instance; + + AdsManager._(); + + final Map interstitialAds = {}; + + final Map rewardsAds = {}; + + final AdImpressionController adImpressionController = AdImpressionController(); + + final BehaviorSubject _adsConfigSubject = + BehaviorSubject.seeded(AdsConfig.defaultAdsConfig); + + final BehaviorSubject _adsProfileSubject = + BehaviorSubject.seeded(GuruApp.instance.adsProfile); + + final BehaviorSubject _initializedSubject = BehaviorSubject.seeded(false); + + final BehaviorSubject connectivityStatusSubject = + BehaviorSubject.seeded(ConnectivityResult.none); + + final BehaviorSubject noBannerAndInterstitialAdsSubject = BehaviorSubject.seeded(false); + + final Map adsGlobalProperties = {}; + + static const Set _reservedKeywords = { + "app_version", + "lt", + "paid", + "blv", + "os_version", + "connection" + }; + + static const List ltSamples = [0, 1, 2, 3, 4, 5, 6, 14, 30, 60, 90, 120, 180]; + + @override + Stream get observableInitialized => _initializedSubject.stream; + + Stream get observableConnectivityStatus => connectivityStatusSubject.stream; + + @override + Stream get observableNoAds => noBannerAndInterstitialAdsSubject.stream; + + final CompositeSubscription subscriptions = CompositeSubscription(); + + AdsProfile get adsProfile => _adsProfileSubject.value; + + AdsConfig get adsConfig => _adsConfigSubject.value; + + bool get hasAmazonBannerAds => adsConfig.bannerConfig.amazonEnable; + + bool get hasAmazonInterstitialAds => adsConfig.interstitialConfig.amazonEnable; + + bool get hasAmazonAds => hasAmazonBannerAds || hasAmazonInterstitialAds; + + ConnectivityResult get connectivityStatus => connectivityStatusSubject.value; + + final BehaviorSubject> keywordsSubject = BehaviorSubject.seeded({}); + + Stream> get observableKeywords => keywordsSubject.stream; + + Map get adsKeywords => keywordsSubject.value; + + String? consentTestDeviceId; + + int? consentDebugGeography; + + static final RegExp _nonAlphaNumeric = RegExp('[^a-zA-Z0-9_]'); + static final RegExp _alpha = RegExp('[a-zA-Z]'); + + @override + bool get isPurchasedNoAd => noBannerAndInterstitialAdsSubject.value; + + void setProperty(String key, String value) { + adsGlobalProperties[key] = value; + } + + void setNoAds(bool noAds) { + noBannerAndInterstitialAdsSubject.addIfChanged(noAds); + GuruSettings.instance.isNoAds.set(noAds); + GuruAnalytics.instance.setUserProperty("user_type", noAds ? "noads" : "default"); + setProperty("user_type", noAds ? "noads" : "default"); + } + + void ensureInitialize() {} + + void listenIap() { + final obs = Rx.combineLatest2, Tuple2>>( + IapManager.instance.observableAvailable, + IapManager.instance.observableAssetStore, + (a, b) => Tuple2(a, b)); + subscriptions.add(obs.listen((tuple) { + final available = tuple.item1; + final purchasedStore = tuple.item2; + if (available && purchasedStore.isActive) { + final tempIsNoAds = + purchasedStore.existsAssets(GuruApp.instance.productProfile.noAdsCapIds); + final isNoAds = isPurchasedNoAd; + Log.i("purchased store changed active! tempIsNoAds:$tempIsNoAds isNoAds:$isNoAds", + syncFirebase: true); + if (isNoAds != tempIsNoAds) { + if (!tempIsNoAds) { + GuruAnalytics.instance.logException(NoAdsException( + "The payment system is abnormal, it shouldn't appear that the purchased item become unpurchased")); + } + + setNoAds(tempIsNoAds); + } + } + })); + } + + Future initialize({SaasUser? saasUser}) async { + _adsProfileSubject.addEx(GuruApp.instance.adsProfile); + await initEnv(); + await initSdk( + saasUser: saasUser, + onInitialized: () { + // loadAds(); + adImpressionController.init(); + initLifecycleConnectivity(); + checkAndPreload(); + // GuruSettings.instance.totalLevelUp + // .observe() + // .throttleTime(const Duration(seconds: 1)) + // .listen((count) { + // checkAndPreload(); + // }); + + Log.i("ADS Initialized", tag: "Ads", syncFirebase: true); + }); + + subscriptions.add(RemoteConfigManager.instance.observeConfig().listen((_) { + refreshAdsConfig(); + }, onError: (error, stacktrace) { + Log.i("init config error!", tag: "Ads", error: error, stackTrace: stacktrace); + })); + listenIap(); + } + + void initLifecycleConnectivity() { + StreamSubscription? streamSubscription; + LifecycleManager.instance.observableAppLifecycle.listen((foreground) { + if (foreground) { + streamSubscription = + Connectivity().onConnectivityChanged.listen((ConnectivityResult result) { + Log.i("Connectivity: $result", tag: "Connectivity"); + if (connectivityStatus == ConnectivityResult.none && result != ConnectivityResult.none) { + Log.i("connectivity changed! retry ads!", tag: "Ads"); + retry(); + } + final changed = connectivityStatusSubject.addIfChanged(result); + if (changed) { + setKeyword("connection", result.toString()); + } + }); + } else { + streamSubscription?.cancel(); + streamSubscription = null; + } + }); + } + + void initAdsProfile() { + final _hasAmazonBannerAds = hasAmazonBannerAds; + final _hasAmazonInterstitialAds = hasAmazonInterstitialAds; + final _hasAmazonAds = _hasAmazonBannerAds || _hasAmazonInterstitialAds; + final defaultAdsProfile = GuruApp.instance.adsProfile; + final strategyInterstitialIds = adsConfig.strategyAdsConfig.interstitialIds; + final newAdsProfile = adsProfile.copyWith( + amazonAppId: _hasAmazonAds ? defaultAdsProfile.amazonAppId : null, + amazonBannerSlotId: _hasAmazonBannerAds ? defaultAdsProfile.amazonBannerSlotId : null, + amazonInterstitialSlotId: + _hasAmazonInterstitialAds ? defaultAdsProfile.amazonInterstitialSlotId : null, + strategyInterstitialIds: strategyInterstitialIds); + _adsProfileSubject.addEx(newAdsProfile); + } + + Future initEnv() async { + final adsPropertyBundle = await AppProperty.getInstance().loadValuesByTag(PropertyTags.ads); + final isNoAds = adsPropertyBundle.getBool(PropertyKeys.isNoAds) ?? false; + + consentTestDeviceId = adsPropertyBundle.getString(PropertyKeys.admobConsentTestDeviceId); + consentDebugGeography = adsPropertyBundle.getInt(PropertyKeys.admobConsentDebugGeography); + noBannerAndInterstitialAdsSubject.addIfChanged(isNoAds); + GuruAnalytics.instance.setUserProperty("user_type", isNoAds ? "noads" : "default"); + setProperty("user_type", isNoAds ? "noads" : "default"); + + final result = await Connectivity().checkConnectivity().catchError((error) { + Log.w("checkConnectivity error! $error"); + }); + connectivityStatusSubject.addEx(result); + setProperty("connectivityStatus", result.toString()); + + refreshAdsConfig(); + initAdsProfile(); + } + + Future initSdk( + {SaasUser? saasUser, + required VoidCallback onInitialized, + Duration retryPeriod = const Duration(seconds: 15)}) async { + final _adsProfile = adsProfile; + bool initializeResult = false; + if (GuruApp.instance.appSpec.deployment.adsCompliantInitialization && + adsConfig.commonAdsConfig.compliantInitialization && + Platform.isAndroid) { + initializeResult = await GuruApplovinFlutter.instance + .gatherConsentAndInitialize( + userId: saasUser?.uid, + amazonAppId: _adsProfile.amazonAppId?.id, + pubmaticStoreUrl: adsProfile.pubmaticAppStoreUrl, + testDeviceId: consentTestDeviceId, + debugGeography: consentDebugGeography) + .catchError((err) => false) ?? + false; + } else { + initializeResult = await GuruApplovinFlutter.instance + .initialize( + userId: saasUser?.uid, + amazonAppId: _adsProfile.amazonAppId?.id, + pubmaticStoreUrl: adsProfile.pubmaticAppStoreUrl) + .catchError((err) => false) ?? + false; + } + + _initializedSubject.addEx(initializeResult); + Log.d("MAX sdk initialize result: $initializeResult"); + if (initializeResult) { + try { + await initKeywords(); + } catch (error, stacktrace) { + Log.e("initKeywords error! $error $stacktrace", tag: "Ads"); + } + onInitialized.call(); + } else { + Future.delayed(retryPeriod, () { + initSdk(onInitialized: onInitialized, retryPeriod: retryPeriod); + }); + Log.w("Ads Initialize error! retry", tag: "Ads", syncFirebase: true); + } + return initializeResult; + } + + void checkAndPreload( + {AdsValidator? rewardedValidator, AdsValidator? interstitialValidator}) async { + final canPreloadReward = + await adsConfig.rewardedConfig.canPreload(validator: rewardedValidator); + if (canPreloadReward) { + Log.d("preload reward canPreload!"); + final reward = await getRewardsAds(); + if (reward.loadCount <= 0) { + reward.preload(); + } + } + + final canPreloadInterstitial = + await adsConfig.interstitialConfig.canPreload(validator: interstitialValidator); + if (!isPurchasedNoAd && canPreloadInterstitial) { + Log.d("preload interstitial canPreload!"); + final interstitial = await getInterstitialAds(); + if (interstitial is AdsAudit && interstitial.loadCount <= 0) { + interstitial.preload(); + } + } + } + + void retry() async { + final canPreloadReward = await adsConfig.rewardedConfig.canPreload(); + if (canPreloadReward) { + final reward = await getRewardsAds(); + reward.retry(); + } + + final canPreload = await adsConfig.interstitialConfig.canPreload(); + if (canPreload) { + Log.d("preload interstitial canPreload!"); + final interstitial = await getInterstitialAds(); + interstitial.retry(); + } + } + + static int _nearestLt(int low, int high, int lt) { + if (low > high) { + return -low; + } + while (low <= high) { + final int mid = (low + high) >> 1; + if (lt == ltSamples[mid]) { + return mid; + } else if (lt < ltSamples[mid]) { + return _nearestLt(low, mid - 1, lt); + } else { + return _nearestLt(mid + 1, high, lt); + } + } + return -low; + } + + Future getKeywordLt() async { + final latestLtDate = await AppProperty.getInstance().getLatestLtDate(); + final dateNum = DateTimeUtils.yyyyMMddUtcNum; + + int lt = await AppProperty.getInstance().getLtDays(); + if (dateNum != latestLtDate) { + if (dateNum > latestLtDate) { + lt = lt + 1; + await AppProperty.getInstance().setLtDays(lt); + } + await AppProperty.getInstance().setLatestLtDate(dateNum); + } + final idx = _nearestLt(0, ltSamples.lastIndex, lt).abs().clamp(0, ltSamples.lastIndex); + Log.d( + "getKeywordLt: installTime:$latestLtDate now:$dateNum lt:$lt keywordLt:${ltSamples[idx]}"); + + return ltSamples[idx]; + } + + Future getOSVersion() async { + try { + final deviceInfo = DeviceInfoPlugin(); + if (Platform.isAndroid) { + final info = await deviceInfo.androidInfo; + return info.version.release; + } else if (Platform.isIOS) { + final info = await deviceInfo.iosInfo; + return info.systemVersion; + } + } catch (error, stacktrace) { + Log.w("getOSVersion error! $error"); + } + return "unknown"; + } + + Future getConnection() async { + try { + final connectivity = await Connectivity().checkConnectivity(); + return connectivity.toString(); + } catch (error, stacktrace) { + Log.w("getConnection error! $error"); + } + return "unknown"; + } + + Future initKeywords() async { + final paidUser = await AppProperty.getInstance().isPaidUser(); + final version = Settings.get().version.get(); + final lt = await getKeywordLt(); + final osVersion = await getOSVersion(); + final connection = await getConnection(); + final keywords = { + "app_version": version, + "paid": paidUser ? "true" : "false", + "lt": lt.toString(), + "os_version": osVersion, + "connection": connection + }; + + keywordsSubject.stream.listen((keywords) { + if (keywords.isNotEmpty) { + Log.i("invoke setKeywords: $keywords", tag: "Ads"); + GuruApplovinFlutter.instance.setKeywords(keywords); + } + }); + keywordsSubject.addEx(keywords); + } + + Future restoreKeywords(Map keywords) async { + if (GuruSettings.instance.debugMode.get()) { + final newKeywords = Map.of(keywords); + keywordsSubject.addEx(newKeywords); + } + } + + void setKeyword(String key, String value, {bool debugForce = false}) { + if (!GuruSettings.instance.debugMode.get() || !debugForce) { + if (_reservedKeywords.contains(key)) { + Log.w("setKeyword error! the key($key) is reserved and cannot be used!", tag: "Ads"); + return; + } + + if (key.isEmpty || + key.length > 36 || + key.indexOf(_alpha) != 0 || + key.contains(_nonAlphaNumeric)) { + Log.w("setKeyword error! the key($key) must contain 1 to 36 alphanumeric characters.", + tag: "Ads"); + return; + } + } + final newKeywords = Map.of(keywordsSubject.value); + newKeywords[key] = value; + keywordsSubject.addEx(newKeywords); + } + + void removeKeyword(String key, {bool debugForce = false}) { + if (!GuruSettings.instance.debugMode.get() || !debugForce) { + if (_reservedKeywords.contains(key)) { + Log.w("removeKeyword error! the key($key) is reserved and cannot be used!", tag: "Ads"); + return; + } + + if (key.isEmpty || + key.length > 36 || + key.indexOf(_alpha) != 0 || + key.contains(_nonAlphaNumeric)) { + Log.w("removeKeyword error! the key($key) must contain 1 to 36 alphanumeric characters.", + tag: "Ads"); + return; + } + } + final newKeywords = Map.of(keywordsSubject.value); + newKeywords.remove(key); + keywordsSubject.addEx(newKeywords); + } + + Future checkConsentDialogStatus() async { + return await GuruApplovinFlutter.instance.checkConsentDialogStatus(); + } + + Future afterAcceptPrivacy(bool consentResult) async { + return await GuruApplovinFlutter.instance.afterAcceptPrivacy(consentResult); + } + + bool testParseAdsDefaultConfig() { + final iadsConfigString = RemoteConfigReservedConstants.getDefaultConfigString( + RemoteConfigReservedConstants.iadsConfig) ?? + ""; + final radsConfigString = RemoteConfigReservedConstants.getDefaultConfigString( + RemoteConfigReservedConstants.radsConfig) ?? + ""; + final badsConfigString = RemoteConfigReservedConstants.getDefaultConfigString( + RemoteConfigReservedConstants.badsConfig) ?? + ""; + final iosAttConfigString = RemoteConfigReservedConstants.getDefaultConfigString( + RemoteConfigReservedConstants.iosAttConfig) ?? + ""; + try { + final adInterstitial = AdInterstitialConfig.fromJson(json.decode(iadsConfigString)); + final adBanner = AdBannerConfig.fromJson(json.decode(badsConfigString)); + final iosAttConfig = IOSAttConfig.fromJson(json.decode(iosAttConfigString)); + Log.d("==== ADS AdsConfig ===="); + Log.d(" ---> [INTERSTITIAL]: $iadsConfigString"); + Log.d(" ---> [BANNER]: $badsConfigString"); + Log.d(" ---> [IOSATT]: $iosAttConfigString"); + Log.d("======================="); + + _adsConfigSubject.addEx(AdsConfig.build( + interstitialConfig: adInterstitial, bannerConfig: adBanner, iosAttConfig: iosAttConfig)); + return true; + } catch (error, stacktrace) { + Log.e("refreshAdsConfig error $error $stacktrace"); + rethrow; + } + } + + bool refreshAdsConfig() { + try { + final commonAdsConfig = RemoteConfigManager.instance.getCommonAdsConfig(); + final adInterstitial = RemoteConfigManager.instance.getIadsConfig(); + final adReward = RemoteConfigManager.instance.getRadsConfig(); + final adBanner = RemoteConfigManager.instance.getBadsConfig(); + final strategyAdsConfig = RemoteConfigManager.instance.getStrategyAdsConfig(); + final iosAttConfig = RemoteConfigManager.instance.getIOSAttConfig(); + Log.d("==== ADS AdsConfig ====", tag: PropertyTags.ads); + Log.d(" ---> [COMMON]: ${commonAdsConfig.toJson()}", tag: PropertyTags.ads); + Log.d(" ---> [INTERSTITIAL]: ${adInterstitial.toJson()}", tag: PropertyTags.ads); + Log.d(" ---> [REWARD]: ${adReward.toJson()}", tag: PropertyTags.ads); + Log.d(" ---> [BANNER]: ${adBanner.toJson()}", tag: PropertyTags.ads); + Log.d(" ---> [STRATEGY]: ${strategyAdsConfig.toJson()}", tag: PropertyTags.ads); + Log.d(" ---> [IOSATT]: ${iosAttConfig.toJson()}", tag: PropertyTags.ads); + Log.d("=======================", tag: PropertyTags.ads); + _adsConfigSubject.addEx(AdsConfig.build( + commonAdsConfig: commonAdsConfig, + interstitialConfig: adInterstitial, + rewardedConfig: adReward, + bannerConfig: adBanner, + strategyAdsConfig: strategyAdsConfig, + iosAttConfig: iosAttConfig)); + return true; + } catch (error, stacktrace) { + Log.e("refreshAdsConfig error $error $stacktrace"); + rethrow; + } + } + + @override + Future getInterstitialAds() async { + final _adsProfile = adsProfile; + final strategyInterstitialIds = adsProfile.strategyInterstitialIds ?? []; + Ads? ad; + if (strategyInterstitialIds.isNotEmpty) { + if (strategyInterstitialIds.length > 1) { + ad = interstitialAds[strategyInterstitialIds.first.adUnitId] ??= + MaxStrategyInterstitialAds.create(strategyInterstitialIds)..init(); + } else { + ad = interstitialAds[strategyInterstitialIds.first.adUnitId] ??= + ApplovinInterstitialAds.create(strategyInterstitialIds.first.adUnitId, + strategyInterstitialIds.first.amazonAdSlotId) + ..init(); + } + } else { + ad = interstitialAds[_adsProfile.interstitialId] ??= ApplovinInterstitialAds.create( + _adsProfile.interstitialId, _adsProfile.amazonInterstitialSlotId) + ..init(); + } + return ad; + } + + @override + Future getRewardsAds() async { + final _adsProfile = adsProfile; + ApplovinRewardedAds? ad = rewardsAds[_adsProfile.rewardsId]; + if (ad == null) { + ad = ApplovinRewardedAds.create(_adsProfile.rewardsId, + adAmazonSlotId: _adsProfile.amazonRewardedSlotId) + ..init(); + rewardsAds[_adsProfile.rewardsId] = ad; + } + return ad; + } + + Future requestGdpr({int? debugGeography, String? testDeviceId}) { + Log.d("requestGdpr! debugGeography:$debugGeography testDeviceId:$testDeviceId", tag: "Ads"); + // adb logcat -s UserMessagingPlatform + // Use new ConsentDebugSettings.Builder().addTestDeviceHashedId("xxxx") to set this as a debug device. + return GuruApplovinFlutter.instance + .requestGdpr(debugGeography: debugGeography, testDeviceId: testDeviceId); + } + + Future resetGdpr() { + return GuruApplovinFlutter.instance.resetGdpr(); + } + + @override + Future createBannerAds({String? scene, AdsLifecycleObserver? observer}) async { + final _adsProfile = adsProfile; + return ApplovinBannerAds.create(_adsProfile.bannerId, _adsProfile.amazonBannerSlotId, + scene: scene, observer: observer); + } + + AdCause canShowInterstitial(String scene) { + if (isPurchasedNoAd) { + return AdCause.noAds; + } + final _adsProfile = adsProfile; + Ads? ad = interstitialAds[_adsProfile.interstitialId]; + int hiddenAt = 0; + if (ad is AdsAudit) { + hiddenAt = ad.latestHiddenAt; + } + + final now = DateTimeUtils.currentTimeInMillis(); + final impGapInMillis = + AdsManager.instance.adsConfig.interstitialConfig.getSceneImpGapInSeconds(scene) * 1000; + Log.d( + "canShowInterstitial($scene): now:$now latestFullscreenAdsHiddenTimestamps:$latestFullscreenAdsHiddenTimestamps hiddenAt:$hiddenAt impGapInMillis:$impGapInMillis", + tag: "Ads"); + if ((now - AdsManager.instance.latestFullscreenAdsHiddenTimestamps) < 60000 || + ((now - hiddenAt) < impGapInMillis)) { + Log.d("show ads too frequency", syncFirebase: true); + return AdCause.tooFrequent; + } + return AdCause.success; + } + + @override + Future validateInterstitial(String? scene, {AdsValidator? validator}) { + final interstitialConfig = adsConfig.interstitialConfig; + return interstitialConfig.check(scene ?? "", validator: validator); + } + + @override + Future validateRewards(String? scene, {AdsValidator? validator}) { + final rewardedConfig = adsConfig.rewardedConfig; + return rewardedConfig.check(scene ?? "", validator: validator); + } + + @override + Future validateBanner(String? scene, {AdsValidator? validator}) { + final rewardedConfig = adsConfig.bannerConfig; + return rewardedConfig.check(scene ?? "", validator: validator); + } + + @override + dynamic getConfig(String type) { + switch (type) { + case "bannerAutoDisposeInterval": + return adsConfig.bannerConfig.autoDisposeIntervalInMinutes; + case "allowInterstitialAsAlternativeReward": + return GuruApp.instance.appSpec.deployment.allowInterstitialAsAlternativeReward; + case "showInternalAdsWhenBannerUnavailable": + return GuruApp.instance.appSpec.deployment.showInternalAdsWhenBannerUnavailable; + } + } +} diff --git a/guru_app/lib/ads/applovin/banner/applovin_banner_ads.dart b/guru_app/lib/ads/applovin/banner/applovin_banner_ads.dart new file mode 100644 index 0000000..9bdc9f7 --- /dev/null +++ b/guru_app/lib/ads/applovin/banner/applovin_banner_ads.dart @@ -0,0 +1,107 @@ +import 'package:guru_app/ads/core/ads.dart'; +import 'package:guru_app/ads/core/ads_config.dart'; + +// import 'package:guru_utils/ads/data/ads.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_applovin_flutter/banner_ad.dart'; +import 'package:guru_applovin_flutter/guru_applovin_flutter.dart'; +import 'package:guru_utils/ads/ads.dart'; + +/// Created by Haoyi on 5/10/21 + +class ApplovinBannerAds extends BannerAds { + late BannerAd bannerAd; + @override + final AdUnitId adUnitId; + final AdSlotId? adAmazonSlotId; + + ApplovinBannerAds.create(this.adUnitId, this.adAmazonSlotId, + {String? scene, AdsLifecycleObserver? observer}) { + bannerAd = BannerAd( + adUnitId: adUnitId.id, adAmazonSlotId: adAmazonSlotId?.id, listener: dispatchEvent); + init(); + this.scene = scene ?? ""; + if (observer != null) { + addObserver(observer); + } + } + + @override + Map get eventsMapping => { + BannerAdEvent.onAdLoaded: AdsEvent.adLoaded, + BannerAdEvent.onAdLoadFailed: AdsEvent.adLoadFailed, + BannerAdEvent.onAdDisplayFailed: AdsEvent.adDisplayFailed, + BannerAdEvent.onAdDisplayed: AdsEvent.adDisplayed, + BannerAdEvent.onAdClicked: AdsEvent.adClick, + BannerAdEvent.onAdHidden: AdsEvent.adHidden, + }; + + void hideOtherBanner() { + // MoPubBannerAd.allBannerAds.forEach((key, value) { + // if (bannerAd.id != key) { + // value.hide(); + // } + // }); + } + + @override + Future requestDispose() async { + try { + return await bannerAd.dispose() ?? false; + } catch (error, stacktrace) { + Log.d("dispose error:$error $stacktrace"); + return false; + } + } + + @override + Future requestHide() async { + try { + return await bannerAd.hide() ?? false; + } catch (error, stacktrace) { + Log.d("requestHide error:$error $stacktrace"); + return false; + } + } + + @override + Future requestLoad() async { + try { + Log.w("[$runtimeType] requestLoad", tag: "Ads"); + final result = await bannerAd.load(placement: scene) ?? false; + if (result) { + return AdCause.success; + } else { + return AdCause.requestFailed; + } + } catch (error, stacktrace) { + Log.w("requestLoad error! $error $stacktrace"); + return AdCause.internalError; + } + } + + @override + Future requestShow({required String scene, bool ignoreCheck = false}) async { + hideOtherBanner(); + try { + final result = await bannerAd.show(anchorOffset: 3.0) ?? false; + if (result) { + return AdCause.success; + } else { + return AdCause.requestFailed; + } + } catch (error, stacktrace) { + Log.i("Banner show error $error $stacktrace"); + } + return AdCause.internalError; + } + + Future checkLoaded() async { + return true; + } + + @override + Future getStatus() async { + return AdStatus.LOADING; + } +} diff --git a/guru_app/lib/ads/applovin/interstitial/applovin_interstitial_ads.dart b/guru_app/lib/ads/applovin/interstitial/applovin_interstitial_ads.dart new file mode 100644 index 0000000..7e93af6 --- /dev/null +++ b/guru_app/lib/ads/applovin/interstitial/applovin_interstitial_ads.dart @@ -0,0 +1,124 @@ +import 'package:guru_app/ads/ads_manager.dart'; +import 'package:guru_app/ads/core/ads.dart'; +import 'package:guru_app/ads/core/ads_config.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_applovin_flutter/guru_applovin_flutter.dart'; +import 'package:guru_applovin_flutter/interstitial_ad.dart'; +import 'package:guru_utils/ads/ads.dart'; + + + +/// Created by Haoyi on 5/6/21 + +class ApplovinInterstitialAds extends InterstitialAds { + late InterstitialAd interstitialAd; + + @override + final AdUnitId adUnitId; + final AdSlotId? adAmazonSlotId; + + // @override + // RetryConfig get retryConfig { + // final adsService = Injector.provide(); + // return adsService.adsConfig.interstitialConfig.retryConfig; + // } + + ApplovinInterstitialAds.create(this.adUnitId, this.adAmazonSlotId); + + @override + void init() { + super.init(); + interstitialAd = InterstitialAd( + adUnitId: adUnitId.id, adAmazonSlotId: adAmazonSlotId?.id, listener: dispatchEvent); + } + + @override + Map get eventsMapping => { + InterstitialAdEvent.onAdLoaded: AdsEvent.adLoaded, + InterstitialAdEvent.onAdLoadFailed: AdsEvent.adLoadFailed, + InterstitialAdEvent.onAdDisplayFailed: AdsEvent.adDisplayFailed, + InterstitialAdEvent.onAdDisplayed: AdsEvent.adDisplayed, + InterstitialAdEvent.onAdClicked: AdsEvent.adClick, + InterstitialAdEvent.onAdHidden: AdsEvent.adHidden, + }; + + @override + Future requestDispose() async { + try { + return await interstitialAd.dispose() ?? false; + } catch (error, stacktrace) { + Log.w("requestDispose error", error: error, stackTrace: stacktrace); + return false; + } + } + + @override + Future requestHide() async { + return false; + } + + @override + Future requestLoad() async { + try { + Log.w("[$runtimeType] requestLoad", tag: "Ads"); + final result = await interstitialAd.load() ?? false; + if (result) { + return AdCause.success; + } else { + return AdCause.requestFailed; + } + } catch (error, stacktrace) { + Log.w("requestLoad error! $error $stacktrace"); + return AdCause.internalError; + } + } + + @override + Future requestShow({required String scene, bool ignoreCheck = false}) async { + // final now = DateTimeUtils.currentTimeInMillis(); + // final impGapInMillis = AdsManager.instance.adsConfig.interstitialConfig.impGapInSeconds * 1000; + // if ((now - AdsManager.instance.latestFullscreenAdsHiddenTimestamps) < 60000 || + // ((now - latestHiddenAt) < impGapInMillis)) { + // Log.d("show ads too frequency", syncFirebase: true); + // return AdCause.tooFrequent; + // } + if (!ignoreCheck) { + final result = AdsManager.instance.canShowInterstitial(scene); + if (result != AdCause.success) { + return result; + } + } + Log.d("[$runtimeType] requestShow", tag: "Ads", syncFirebase: true); + try { + final result = await interstitialAd.show(placement: scene) ?? false; + if (result) { + return AdCause.success; + } else { + return AdCause.requestFailed; + } + } catch (error, stacktrace) { + Log.w("requestShow error", error: error, stackTrace: stacktrace, syncFirebase: true); + return AdCause.internalError; + } + } + + @override + Future checkLoaded() async { + try { + return await interstitialAd.isLoaded() ?? false; + } catch (error, stacktrace) { + return false; + } + } + + @override + Future getStatus() async { + try { + return await interstitialAd.getAdState(); + } catch (error, stacktrace) { + Log.w("getInterstitialAdStatus error", + error: error, stackTrace: stacktrace, syncFirebase: true); + return AdStatus.FAILED; + } + } +} diff --git a/guru_app/lib/ads/applovin/rewarded/applovin_rewarded_ads.dart b/guru_app/lib/ads/applovin/rewarded/applovin_rewarded_ads.dart new file mode 100644 index 0000000..1822f5e --- /dev/null +++ b/guru_app/lib/ads/applovin/rewarded/applovin_rewarded_ads.dart @@ -0,0 +1,129 @@ +import 'package:guru_app/ads/core/ads.dart'; +import 'package:guru_app/ads/core/ads_config.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_applovin_flutter/guru_applovin_flutter.dart'; +import 'package:guru_applovin_flutter/rewarded_video_ad.dart'; +import 'package:guru_utils/ads/ads.dart'; + +/// Created by Haoyi on 5/26/21 + +class ApplovinRewardedAds extends RewardedAds { + late RewardedVideoAd rewardedVideoAd; + + @override + final AdUnitId adUnitId; + final AdSlotId? adAmazonSlotId; + + ApplovinRewardedAds.create(this.adUnitId, {this.adAmazonSlotId}); + + // @override + // RetryConfig get retryConfig { + // final adsService = Injector.provide(); + // return adsService.adsConfig.rewardedConfig.retryConfig; + // } + + @override + void init() { + super.init(); + rewardedVideoAd = RewardedVideoAd( + adUnitId: adUnitId.id, adAmazonSlotId: adAmazonSlotId?.id, listener: dispatchEvent); + } + + @override + Map get eventsMapping => { + RewardedVideoAdEvent.onAdLoaded: AdsEvent.adLoaded, + RewardedVideoAdEvent.onAdLoadFailed: AdsEvent.adLoadFailed, + RewardedVideoAdEvent.onAdDisplayFailed: AdsEvent.adDisplayFailed, + RewardedVideoAdEvent.onAdDisplayed: AdsEvent.adDisplayed, + RewardedVideoAdEvent.onAdClicked: AdsEvent.adClick, + RewardedVideoAdEvent.onAdHidden: AdsEvent.adHidden, + RewardedVideoAdEvent.onUserRewarded: AdsEvent.adRewarded + }; + + @override + Future requestDispose() async { + try { + return await rewardedVideoAd.dispose() ?? false; + } catch (error, stacktrace) { + Log.w("requestDispose error! $error $stacktrace"); + return false; + } + } + + @override + Future requestHide() async { + return false; + } + + @override + Future requestLoad() async { + try { + Log.w("[$runtimeType] requestLoad", tag: "Ads"); + final result = await rewardedVideoAd.load() ?? false; + if (result) { + return AdCause.success; + } else { + return AdCause.requestFailed; + } + } catch (error, stacktrace) { + Log.w("requestLoad error! $error $stacktrace"); + return AdCause.internalError; + } + } + + @override + Future requestReset() async { + try { + Log.w("[$runtimeType] requestLoad", tag: "Ads"); + final result = await rewardedVideoAd.dispose() ?? false; + if (result) { + rewardedVideoAd = RewardedVideoAd(adUnitId: adUnitId.id, listener: dispatchEvent); + return AdCause.success; + } else { + return AdCause.requestFailed; + } + } catch (error, stacktrace) { + Log.w("requestLoad error! $error $stacktrace"); + return AdCause.internalError; + } + } + + @override + Future requestShow({required String scene, bool ignoreCheck = false}) async { + // final now = DateTimeUtils.currentTimeInMillis(); + // if (now - latestHiddenAt < 60000) { + // Log.i("show ads too frequency"); + // return false; + // } + // Log.i("show interstitial"); + try { + Log.d("[$hashCode]requestShow rewardedAds", syncFirebase: true); + final result = await rewardedVideoAd.show(placement: scene) ?? false; + if (result) { + return AdCause.success; + } else { + return AdCause.requestFailed; + } + } catch (error, stacktrace) { + Log.w("requestShow rewarded error", error: error, stackTrace: stacktrace, syncFirebase: true); + return AdCause.internalError; + } + } + + @override + Future getStatus() async { + try { + return await rewardedVideoAd.getAdState(); + } catch (error, stacktrace) { + Log.w("getRewardedAdStatus error", error: error, stackTrace: stacktrace, syncFirebase: true); + return AdStatus.FAILED; + } + } + + @override + bool needReset() { + Log.d("reset check elapsedTimeInMillisSinceStartLoadAds $elapsedTimeInMillisSinceStartLoadAds", + tag: "Ads"); + return elapsedTimeInMillisSinceStartLoadAds > 30 * 1000; + } +} diff --git a/guru_app/lib/ads/core/ads.dart b/guru_app/lib/ads/core/ads.dart new file mode 100644 index 0000000..3542eac --- /dev/null +++ b/guru_app/lib/ads/core/ads.dart @@ -0,0 +1,201 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/foundation.dart'; +import 'package:guru_app/ads/ads_manager.dart'; +import 'package:guru_app/ads/core/ads_config.dart'; +import 'package:guru_app/aigc/bi/ai_bi.dart'; +import 'package:guru_app/analytics/guru_analytics.dart'; +import 'package:guru_app/hook/hook_manager.dart'; +import 'package:guru_app/property/app_property.dart'; +import 'package:guru_app/property/property_keys.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_applovin_flutter/guru_applovin_flutter.dart'; + +import 'package:guru_analytics_flutter/events_constants.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:guru_utils/ads/ads.dart'; + +part 'banner/banner_ads.dart'; + +part 'banner/banner_ads_handler.dart'; + +part 'handler/ads_audit.dart'; + +part 'interstitial/interstitial_ads.dart'; + +part 'interstitial/interstitial_ads_handler.dart'; + +part 'rewards/rewarded_ads.dart'; + +part 'rewards/rewarded_ads_handler.dart'; + +part 'handler/ads_cache.dart'; + +/// Created by Haoyi on 5/6/21 + +abstract class Ads extends AdsLifecycleOwner with AdsDelegate { + set loaded(bool loaded); + + final Map properties = {}; + + void setProperty(String name, String data) { + properties[name] = data; + } + + void retry() {} + + void preload() {} +} + +abstract class SingleAds extends Ads { + Map get eventsMapping; + + AdUnitId get adUnitId; + + @override + set loaded(bool loaded) { + loadedSubject.addEx(loaded); + setProperty("isLoaded", loaded ? "true" : "false"); + } + + @override + bool get loaded => loadedSubject.value == true; + + final BehaviorSubject loadedSubject = BehaviorSubject.seeded(false); + + @override + Stream get observableLoaded => loadedSubject.stream; + + void dispatchEvent(T event, {Map arguments = const {}}) { + final adsEvent = eventsMapping[event]; + if (adsEvent != null) { + final adsBundle = AdsBundle.create(this, arguments: arguments); + switch (adsEvent) { + case AdsEvent.adLoaded: + onAdLoaded(adsBundle); + break; + case AdsEvent.adLoadFailed: + onAdLoadFailed(adsBundle); + break; + case AdsEvent.adDisplayed: + onAdDisplayed(adsBundle); + break; + case AdsEvent.adDisplayFailed: + onAdDisplayFailed(adsBundle); + break; + case AdsEvent.adClick: + onAdClicked(adsBundle); + break; + case AdsEvent.adHidden: + onAdHidden(adsBundle); + break; + case AdsEvent.adRewarded: + onAdRewarded(adsBundle); + break; + } + } + } + + @override + @mustCallSuper + Future dispose() async { + bool result = false; + try { + result = await requestDispose(); + } catch (error, stacktrace) { + Log.d("requestDispose error:$error $stacktrace"); + } + onRequestDispose(AdsBundle.create(this)); + super.dispose(); + return result; + } + + @override + @mustCallSuper + Future reset() async { + AdCause result = AdCause.internalError; + try { + result = await requestReset(); + } catch (error, stacktrace) { + Log.d("requestReset error:$error $stacktrace"); + } + onRequestReset(AdsBundle.create(this)); + return result == AdCause.success; + } + + @mustCallSuper + void init() { + if (this is AdsAudit) { + Log.d("[$runtimeType]AdsAudit === add AdsAuditObserver", tag: "Ads"); + addObserver(AdsAuditObserver(runtimeType.toString())); + } + if (this is AdsCache) { + Log.d("[$runtimeType]AdsReload === add AdsReloadObserver", tag: "Ads"); + addObserver(AdsCacheObserver(runtimeType.toString())); + } + } + + @override + Future load() async { + final adCause = await requestLoad().catchError((error, stacktrace) { + Log.e("load error! ", tag: "Ads", error: error, stackTrace: stacktrace, syncFirebase: true); + return AdCause.internalError; + }); + Log.d("[$runtimeType]load complete!! $adCause", syncFirebase: true); + onRequestLoad(AdsBundle.create(this, arguments: {"cause": adCause})); + return adCause; + } + + @override + Future hide() async { + onRequestHide(AdsBundle.create(this)); + return await requestHide(); + } + + @override + Future show({required String scene, bool ignoreCheck = false}) async { + final adCause = + await requestShow(scene: scene, ignoreCheck: ignoreCheck).catchError((error, stacktrace) { + Log.e("show error! $error", stackTrace: stacktrace, syncFirebase: true); + return AdCause.internalError; + }); + Log.d("[$runtimeType]show $scene complete!! $adCause", syncFirebase: true); + onRequestShow(AdsBundle.create(this, arguments: {"scene": scene, "cause": adCause})); + return adCause; + } + + Future requestLoad(); + + Future requestShow({required String scene, bool ignoreCheck = false}); + + Future requestHide(); + + Future requestDispose(); + + Future requestReset() async { + return AdCause.internalError; + } + + Future getStatus(); + + @override + Future getState() async { + final status = await getStatus(); + return convertAdStatusToAdState(status); + } +} + +enum AdsEvent { + adLoaded, + adLoadFailed, + adDisplayed, + adDisplayFailed, + adClick, + adHidden, + adRewarded +} diff --git a/guru_app/lib/ads/core/ads_config.dart b/guru_app/lib/ads/core/ads_config.dart new file mode 100644 index 0000000..9709d56 --- /dev/null +++ b/guru_app/lib/ads/core/ads_config.dart @@ -0,0 +1,481 @@ +import 'dart:convert'; + +import 'package:guru_app/guru_app.dart'; +import 'package:guru_app/property/app_property.dart'; +import 'package:guru_app/property/property_keys.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:guru_utils/converts/converts.dart'; +import 'package:guru_utils/ads/ads.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_applovin_flutter/guru_applovin_flutter.dart'; + +part 'ads_config.g.dart'; + +AdState convertAdStatusToAdState(int status) { + switch (status) { + case AdStatus.LOADED: + return AdState.loaded; + case AdStatus.CREATED: + return AdState.created; + case AdStatus.LOADING: + return AdState.loading; + default: + return AdState.failed; + } +} + +@JsonSerializable() +class TaichiConfig { + @JsonKey(name: "enable", defaultValue: false) + final bool enable; + + @JsonKey(name: "threshold", defaultValue: "") + final String threshold; + + @JsonKey(name: "abnormal_threshold", defaultValue: 1.0) + final double abnormalThreshold; + + TaichiConfig({this.enable = false, this.threshold = "", this.abnormalThreshold = 1.0}); + + factory TaichiConfig.fromJson(Map json) => _$TaichiConfigFromJson(json); + + @override + String toString() { + return 'TaichiConfig{enable: $enable, threshold: $threshold}'; + } + + Map toJson() => _$TaichiConfigToJson(this); +} + +@JsonSerializable() +class ImpressionData { + @JsonKey(name: "ad_platform", defaultValue: "MAX") + final String platform; + + @JsonKey(name: "id", defaultValue: "") + final String id; + + @JsonKey(name: "adunit_id", defaultValue: "") + final String unitId; + + @JsonKey(name: "adunit_name", defaultValue: "") + final String unitName; + + @JsonKey(name: "adunit_format", defaultValue: "") + final String unitFormat; + + @JsonKey(name: "adgroup_id", defaultValue: "") + final String groupId; + + @JsonKey(name: "adgroup_name", defaultValue: "") + final String groupName; + + @JsonKey(name: "adgroup_type", defaultValue: "") + final String groupType; + + @JsonKey(name: "currency", defaultValue: "") + final String currency; + + @JsonKey(name: "country", defaultValue: "") + final String country; + + @JsonKey(name: "app_version", defaultValue: "") + final String appVersion; + + @JsonKey(name: "adgroup_priority", defaultValue: 0) + final int groupPriority; + + @JsonKey(name: "publisher_revenue", defaultValue: -1) + final double publisherRevenue; + + @JsonKey(name: "network_name", defaultValue: "") + final String networkName; + + @JsonKey(name: "network_placement_id", defaultValue: "") + final String networkPlacementId; + + @JsonKey(name: "precision", defaultValue: "") + final String precision; + + @JsonKey(ignore: true) + late Map payload; + + ImpressionData derive({double? newPublisherRevenue}) { + final newPayload = Map.from(payload); + newPayload["publisher_revenue"] = newPublisherRevenue ?? publisherRevenue; + return ImpressionData.fromJson(newPayload); + } + + @override + String toString() { + return 'ImpressionData{platform: $platform, id: $id, unitId: $unitId, unitName: $unitName, unitFormat: $unitFormat, groupId: $groupId, groupName: $groupName, groupType: $groupType, currency: $currency, country: $country, appVersion: $appVersion, groupPriority: $groupPriority, publisherRevenue: $publisherRevenue, networkName: $networkName, networkPlacementId: $networkPlacementId, precision: $precision}'; + } + + ImpressionData( + {required this.platform, + required this.id, + required this.unitId, + required this.unitName, + required this.unitFormat, + required this.groupId, + required this.groupName, + required this.groupType, + required this.currency, + required this.country, + required this.appVersion, + required this.groupPriority, + required this.publisherRevenue, + required this.networkName, + required this.networkPlacementId, + required this.precision}); + + factory ImpressionData.fromJson(Map json) => + _$ImpressionDataFromJson(json)..payload = json; + + Map toJson() => _$ImpressionDataToJson(this); +} + +@JsonSerializable() +class CpmCalibrationData { + @JsonKey(name: "list", defaultValue: []) + final List items; + + final Map _interstitialCpmCalibrationCountryMapping = {}; + + final Map _rewardedCpmCalibrationCountryMapping = {}; + + CpmCalibrationData(this.items); + + double getCpm(String format, String country) { + _ensureInitializedData(); + final upperFormat = format.toUpperCase(); + if (upperFormat.contains("FULLSCREEN") || upperFormat.contains("INTERSTITIAL")) { + return _interstitialCpmCalibrationCountryMapping[country] ?? -1; + } + + if (upperFormat.contains("REWARDED")) { + return _rewardedCpmCalibrationCountryMapping[country] ?? -1; + } + return -1; + } + + void _ensureInitializedData() { + if (_interstitialCpmCalibrationCountryMapping.isEmpty || + _rewardedCpmCalibrationCountryMapping.isEmpty) { + for (CpmCalibrationItem item in items) { + if (item.format == "reward") { + _rewardedCpmCalibrationCountryMapping[item.country.toUpperCase()] = item.cpm; + } else if (item.format == "inter") { + _interstitialCpmCalibrationCountryMapping[item.country.toUpperCase()] = item.cpm; + } + } + } + } + + factory CpmCalibrationData.fromJson(Map json) => + _$CpmCalibrationDataFromJson(json); + + Map toJson() => _$CpmCalibrationDataToJson(this); +} + +@JsonSerializable() +class CpmCalibrationItem { + @JsonKey(name: "format") + final String format; + + @JsonKey(name: "cpm") + final double cpm; + + @JsonKey(name: "country") + final String country; + + CpmCalibrationItem(this.format, this.cpm, this.country); + + factory CpmCalibrationItem.fromJson(Map json) => + _$CpmCalibrationItemFromJson(json); + + Map toJson() => _$CpmCalibrationItemToJson(this); +} + +@JsonSerializable() +class IRLDConfig { + @JsonKey(name: "fb_ecpm_cache_h", defaultValue: 12) + final int fbCpmCacheInHour; + + @JsonKey(name: "fb_irld_report", defaultValue: false) + final bool fbIrldReport; + + @JsonKey(name: "abnormal_threshold", defaultValue: 0.1) + final double abnormalThreshold; + + IRLDConfig({this.fbCpmCacheInHour = 12, this.fbIrldReport = false, this.abnormalThreshold = 0.1}); + + factory IRLDConfig.fromJson(Map json) => _$IRLDConfigFromJson(json); + + Map toJson() => _$IRLDConfigToJson(this); + + @override + String toString() { + return 'IRLDConfig{fbCpmCacheInHour: $fbCpmCacheInHour, fbIrldReport: $fbIrldReport, abnormalThreshold: $abnormalThreshold}'; + } +} + +class AdsConfig { + final CommonAdsConfig commonAdsConfig; + final AdInterstitialConfig interstitialConfig; + final AdRewardedConfig rewardedConfig; + final AdBannerConfig bannerConfig; + final StrategyAdsConfig strategyAdsConfig; + final IOSAttConfig iosAttConfig; + + AdsConfig.build( + {CommonAdsConfig? commonAdsConfig, + AdInterstitialConfig? interstitialConfig, + AdRewardedConfig? rewardedConfig, + AdBannerConfig? bannerConfig, + StrategyAdsConfig? strategyAdsConfig, + IOSAttConfig? iosAttConfig}) + : commonAdsConfig = commonAdsConfig ?? CommonAdsConfig.fromJson({}), + interstitialConfig = + interstitialConfig ?? AdInterstitialConfig.fromJson({}), + rewardedConfig = rewardedConfig ?? AdRewardedConfig.fromJson({}), + bannerConfig = bannerConfig ?? AdBannerConfig.fromJson({}), + strategyAdsConfig = strategyAdsConfig ?? StrategyAdsConfig.fromJson({}), + iosAttConfig = iosAttConfig ?? IOSAttConfig.fromJson({}); + + static AdsConfig defaultAdsConfig = AdsConfig.build(); + + Map dump() { + return { + "common_config": commonAdsConfig.toJson().toString(), + "iads_config": interstitialConfig.toJson().toString(), + "rads_config": rewardedConfig.toJson().toString(), + "bads_config": bannerConfig.toJson().toString(), + "sads_config": strategyAdsConfig.toJson().toString(), + "ios_att_config": iosAttConfig.toJson().toString() + }; + } +} + +@JsonSerializable() +class AdBannerConfig { + @JsonKey(name: "free_s", defaultValue: 600) + final int freeInSecond; + + @JsonKey(name: "validation", defaultValue: '') + final String validation; + + @JsonKey(name: "amazon_enable", defaultValue: false) + final bool amazonEnable; + + @JsonKey(name: "pubmatic_enable", defaultValue: false) + final bool pubmaticEnable; + + @JsonKey(name: "auto_dispose_interval_m", defaultValue: 5) + final int autoDisposeIntervalInMinutes; + + @override + String toString() { + return 'AdBannerConfig{freeInSecond: $freeInSecond, validation: $validation, amazonEnable: $amazonEnable, pubmaticEnable: $pubmaticEnable, autoDisposeIntervalInMinutes: $autoDisposeIntervalInMinutes}'; + } + + AdBannerConfig(this.freeInSecond, this.validation, this.amazonEnable, this.pubmaticEnable, + this.autoDisposeIntervalInMinutes); + + factory AdBannerConfig.fromJson(Map json) => _$AdBannerConfigFromJson(json); + + Map toJson() => _$AdBannerConfigToJson(this); + + Future check(String? scene, {AdsValidator? validator}) async { + if (!(await checkFreeTime())) { + return AdCause.invalidRequest; + } + + if (await validator?.call() == false) { + return AdCause.invalidRequest; + } + return AdCause.success; + } + + Future checkFreeTime() async { + final firstInstallTime = + await AppProperty.getInstance().getInt(PropertyKeys.firstInstallTime, defValue: 0); + return ((DateTimeUtils.currentTimeInMillis() - firstInstallTime) / 1000) >= freeInSecond; + } +} + +@JsonSerializable() +class AdInterstitialConfig { + @JsonKey(name: "free_s", defaultValue: 600) + final int freeInSecond; + + @JsonKey(name: "validation", defaultValue: '') + final String validation; + + @JsonKey(name: "scene", defaultValue: []) + @joinedStringConvert + final List scenes; + + @JsonKey(name: "sp_scene", defaultValue: {"new_block": 120, "reset_scs": 120}) + @configStringIntMapStringConvert + final Map specialScenes; + + @JsonKey(name: "retry_min_s", defaultValue: 4) + final int retryMinTimeInSecond; + + @JsonKey(name: "retry_max_s", defaultValue: 600) + final int retryMaxTimeInSecond; + + @JsonKey(name: "amazon_enable", defaultValue: false) + final bool amazonEnable; + + @JsonKey(name: "imp_gap_s", defaultValue: 120) + final int impGapInSeconds; + + AdInterstitialConfig(this.freeInSecond, this.validation, this.scenes, this.retryMinTimeInSecond, + this.retryMaxTimeInSecond, + {this.amazonEnable = true, required this.specialScenes, required this.impGapInSeconds}); + + factory AdInterstitialConfig.fromJson(Map json) => + _$AdInterstitialConfigFromJson(json); + + Map toJson() => _$AdInterstitialConfigToJson(this); + + bool checkSceneEnabled(String scene) { + return scenes.contains(scene); + } + + int getSceneImpGapInSeconds(String scene) { + return specialScenes[scene] ?? impGapInSeconds; + } + + Future checkFreeTime() async { + final firstInstallTime = + await AppProperty.getInstance().getInt(PropertyKeys.firstInstallTime, defValue: 0); + return ((DateTimeUtils.currentTimeInMillis() - firstInstallTime) / 1000) >= freeInSecond; + } + + Future canPreload({AdsValidator? validator}) async { + if (!(await checkFreeTime())) { + return false; + } + if (await validator?.call() == false) { + return false; + } + return true; + } + + Future check(String scene, {AdsValidator? validator}) async { + Log.d("check: $this", tag: "Ads"); + if (!checkSceneEnabled(scene)) { + return AdCause.disabledScene; + } + if (!(await checkFreeTime())) { + return AdCause.invalidRequest; + } + if (await validator?.call() == false) { + return AdCause.invalidRequest; + } + return AdCause.success; + } + + @override + String toString() { + return 'AdInterstitialConfig{freeInSecond: $freeInSecond, validation: $validation, scenes: $scenes, retryMinTimeInSecond: $retryMinTimeInSecond, retryMaxTimeInSecond: $retryMaxTimeInSecond, amazonEnable: $amazonEnable}'; + } + + RetryConfig get retryConfig => RetryConfig(retryMinTimeInSecond, retryMaxTimeInSecond); +} + +@JsonSerializable() +class StrategyAdsConfig { + @JsonKey(name: "iads") + final List? interstitialIds; + + StrategyAdsConfig({this.interstitialIds}); + + factory StrategyAdsConfig.fromJson(Map json) => + _$StrategyAdsConfigFromJson(json); + + Map toJson() => _$StrategyAdsConfigToJson(this); +} + +class RetryConfig { + final int minInSecond; + final int maxInSecond; + + RetryConfig(this.minInSecond, this.maxInSecond); +} + +@JsonSerializable() +class AdRewardedConfig { + @JsonKey(name: "retry_min_s", defaultValue: 4) + final int retryMinTimeInSecond; + + @JsonKey(name: "retry_max_s", defaultValue: 600) + final int retryMaxTimeInSecond; + + @JsonKey(name: "reset_ads", defaultValue: true) + final bool resetAds; + + @JsonKey(name: "validation", defaultValue: '') + final String validation; + + AdRewardedConfig(this.retryMinTimeInSecond, this.retryMaxTimeInSecond, + {this.resetAds = true, this.validation = ''}); + + factory AdRewardedConfig.fromJson(Map json) => _$AdRewardedConfigFromJson(json); + + Map toJson() => _$AdRewardedConfigToJson(this); + + RetryConfig get retryConfig => RetryConfig(retryMinTimeInSecond, retryMaxTimeInSecond); + + Future check(String scene, {AdsValidator? validator}) async { + if (GuruApp.instance.appSpec.deployment.disableRewardsAds) { + return AdCause.adsDisabled; + } + if (await validator?.call() == false) { + return AdCause.invalidRequest; + } + return AdCause.success; + } + + Future canPreload({AdsValidator? validator}) async { + if (GuruApp.instance.appSpec.deployment.disableRewardsAds) { + return false; + } + if (await validator?.call() == false) { + return false; + } + return true; + } + + @override + String toString() { + return 'AdRewardedConfig{retryMinTimeInSecond: $retryMinTimeInSecond, retryMaxTimeInSecond: $retryMaxTimeInSecond, resetAds: $resetAds}'; + } +} + +@JsonSerializable() +class IOSAttConfig { + @JsonKey(name: "enable", defaultValue: false) + bool enable; + + IOSAttConfig({this.enable = false}); + + factory IOSAttConfig.fromJson(Map json) => _$IOSAttConfigFromJson(json); + + Map toJson() => _$IOSAttConfigToJson(this); +} + +@JsonSerializable() +class CommonAdsConfig { + @JsonKey(name: "compliant_init", defaultValue: false) + final bool compliantInitialization; + + CommonAdsConfig({this.compliantInitialization = false}); + + factory CommonAdsConfig.fromJson(Map json) => _$CommonAdsConfigFromJson(json); + + Map toJson() => _$CommonAdsConfigToJson(this); +} diff --git a/guru_app/lib/ads/core/ads_config.g.dart b/guru_app/lib/ads/core/ads_config.g.dart new file mode 100644 index 0000000..b8be8a1 --- /dev/null +++ b/guru_app/lib/ads/core/ads_config.g.dart @@ -0,0 +1,200 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'ads_config.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +TaichiConfig _$TaichiConfigFromJson(Map json) => TaichiConfig( + enable: json['enable'] as bool? ?? false, + threshold: json['threshold'] as String? ?? '', + abnormalThreshold: + (json['abnormal_threshold'] as num?)?.toDouble() ?? 1.0, + ); + +Map _$TaichiConfigToJson(TaichiConfig instance) => + { + 'enable': instance.enable, + 'threshold': instance.threshold, + 'abnormal_threshold': instance.abnormalThreshold, + }; + +ImpressionData _$ImpressionDataFromJson(Map json) => + ImpressionData( + platform: json['ad_platform'] as String? ?? 'MAX', + id: json['id'] as String? ?? '', + unitId: json['adunit_id'] as String? ?? '', + unitName: json['adunit_name'] as String? ?? '', + unitFormat: json['adunit_format'] as String? ?? '', + groupId: json['adgroup_id'] as String? ?? '', + groupName: json['adgroup_name'] as String? ?? '', + groupType: json['adgroup_type'] as String? ?? '', + currency: json['currency'] as String? ?? '', + country: json['country'] as String? ?? '', + appVersion: json['app_version'] as String? ?? '', + groupPriority: json['adgroup_priority'] as int? ?? 0, + publisherRevenue: (json['publisher_revenue'] as num?)?.toDouble() ?? -1, + networkName: json['network_name'] as String? ?? '', + networkPlacementId: json['network_placement_id'] as String? ?? '', + precision: json['precision'] as String? ?? '', + ); + +Map _$ImpressionDataToJson(ImpressionData instance) => + { + 'ad_platform': instance.platform, + 'id': instance.id, + 'adunit_id': instance.unitId, + 'adunit_name': instance.unitName, + 'adunit_format': instance.unitFormat, + 'adgroup_id': instance.groupId, + 'adgroup_name': instance.groupName, + 'adgroup_type': instance.groupType, + 'currency': instance.currency, + 'country': instance.country, + 'app_version': instance.appVersion, + 'adgroup_priority': instance.groupPriority, + 'publisher_revenue': instance.publisherRevenue, + 'network_name': instance.networkName, + 'network_placement_id': instance.networkPlacementId, + 'precision': instance.precision, + }; + +CpmCalibrationData _$CpmCalibrationDataFromJson(Map json) => + CpmCalibrationData( + (json['list'] as List?) + ?.map( + (e) => CpmCalibrationItem.fromJson(e as Map)) + .toList() ?? + [], + ); + +Map _$CpmCalibrationDataToJson(CpmCalibrationData instance) => + { + 'list': instance.items, + }; + +CpmCalibrationItem _$CpmCalibrationItemFromJson(Map json) => + CpmCalibrationItem( + json['format'] as String, + (json['cpm'] as num).toDouble(), + json['country'] as String, + ); + +Map _$CpmCalibrationItemToJson(CpmCalibrationItem instance) => + { + 'format': instance.format, + 'cpm': instance.cpm, + 'country': instance.country, + }; + +IRLDConfig _$IRLDConfigFromJson(Map json) => IRLDConfig( + fbCpmCacheInHour: json['fb_ecpm_cache_h'] as int? ?? 12, + fbIrldReport: json['fb_irld_report'] as bool? ?? false, + abnormalThreshold: + (json['abnormal_threshold'] as num?)?.toDouble() ?? 0.1, + ); + +Map _$IRLDConfigToJson(IRLDConfig instance) => + { + 'fb_ecpm_cache_h': instance.fbCpmCacheInHour, + 'fb_irld_report': instance.fbIrldReport, + 'abnormal_threshold': instance.abnormalThreshold, + }; + +AdBannerConfig _$AdBannerConfigFromJson(Map json) => + AdBannerConfig( + json['free_s'] as int? ?? 600, + json['validation'] as String? ?? '', + json['amazon_enable'] as bool? ?? false, + json['pubmatic_enable'] as bool? ?? false, + json['auto_dispose_interval_m'] as int? ?? 5, + ); + +Map _$AdBannerConfigToJson(AdBannerConfig instance) => + { + 'free_s': instance.freeInSecond, + 'validation': instance.validation, + 'amazon_enable': instance.amazonEnable, + 'pubmatic_enable': instance.pubmaticEnable, + 'auto_dispose_interval_m': instance.autoDisposeIntervalInMinutes, + }; + +AdInterstitialConfig _$AdInterstitialConfigFromJson( + Map json) => + AdInterstitialConfig( + json['free_s'] as int? ?? 600, + json['validation'] as String? ?? '', + json['scene'] == null + ? [] + : joinedStringConvert.fromJson(json['scene'] as String), + json['retry_min_s'] as int? ?? 4, + json['retry_max_s'] as int? ?? 600, + amazonEnable: json['amazon_enable'] as bool? ?? false, + specialScenes: json['sp_scene'] == null + ? {'new_block': 120, 'reset_scs': 120} + : configStringIntMapStringConvert + .fromJson(json['sp_scene'] as String), + impGapInSeconds: json['imp_gap_s'] as int? ?? 120, + ); + +Map _$AdInterstitialConfigToJson( + AdInterstitialConfig instance) => + { + 'free_s': instance.freeInSecond, + 'validation': instance.validation, + 'scene': joinedStringConvert.toJson(instance.scenes), + 'sp_scene': + configStringIntMapStringConvert.toJson(instance.specialScenes), + 'retry_min_s': instance.retryMinTimeInSecond, + 'retry_max_s': instance.retryMaxTimeInSecond, + 'amazon_enable': instance.amazonEnable, + 'imp_gap_s': instance.impGapInSeconds, + }; + +StrategyAdsConfig _$StrategyAdsConfigFromJson(Map json) => + StrategyAdsConfig( + interstitialIds: (json['iads'] as List?) + ?.map((e) => AdId.fromJson(e as Map)) + .toList(), + ); + +Map _$StrategyAdsConfigToJson(StrategyAdsConfig instance) => + { + 'iads': instance.interstitialIds, + }; + +AdRewardedConfig _$AdRewardedConfigFromJson(Map json) => + AdRewardedConfig( + json['retry_min_s'] as int? ?? 4, + json['retry_max_s'] as int? ?? 600, + resetAds: json['reset_ads'] as bool? ?? true, + validation: json['validation'] as String? ?? '', + ); + +Map _$AdRewardedConfigToJson(AdRewardedConfig instance) => + { + 'retry_min_s': instance.retryMinTimeInSecond, + 'retry_max_s': instance.retryMaxTimeInSecond, + 'reset_ads': instance.resetAds, + 'validation': instance.validation, + }; + +IOSAttConfig _$IOSAttConfigFromJson(Map json) => IOSAttConfig( + enable: json['enable'] as bool? ?? false, + ); + +Map _$IOSAttConfigToJson(IOSAttConfig instance) => + { + 'enable': instance.enable, + }; + +CommonAdsConfig _$CommonAdsConfigFromJson(Map json) => + CommonAdsConfig( + compliantInitialization: json['compliant_init'] as bool? ?? false, + ); + +Map _$CommonAdsConfigToJson(CommonAdsConfig instance) => + { + 'compliant_init': instance.compliantInitialization, + }; diff --git a/guru_app/lib/ads/core/ads_impression.dart b/guru_app/lib/ads/core/ads_impression.dart new file mode 100644 index 0000000..f3abef7 --- /dev/null +++ b/guru_app/lib/ads/core/ads_impression.dart @@ -0,0 +1,189 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:guru_app/analytics/guru_analytics.dart'; +import 'package:guru_app/firebase/remoteconfig/remote_config_manager.dart'; +import 'package:guru_app/property/app_property.dart'; +import 'package:guru_app/property/property_keys.dart'; +import 'package:guru_utils/collection/collectionutils.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_applovin_flutter/ad_impression.dart'; + +import 'ads_config.dart'; +import 'exceptions/ads_exceptions.dart'; + +const ltvPhaseNames = [ + "tch_ad_rev_top40", + "tch_ad_rev_top30", + "tch_ad_rev_top20", + "tch_ad_rev_top10", + "tch_ad_rev_top5" +]; + +class AdImpressionController { + final List ltvThresholds = []; + final Map adsParams = {}; + static String latestImpressionPayload = ""; + TaichiConfig? taichiConfig; + + AdImpressionController() {} + + Future _init() async { + try { + taichiConfig = RemoteConfigManager.instance.getTaichiConfig(); + final config = taichiConfig; + if (config != null && config.enable && config.threshold.isNotEmpty) { + Log.d("set thresholds prepare! ${config.threshold}"); + final thresholdStrings = config.threshold.split(","); + if (thresholdStrings.isNotEmpty) { + try { + final thresholds = thresholdStrings.map((e) => double.parse(e)).toList(); + ltvThresholds.clear(); + ltvThresholds.addAll(thresholds); + Log.d("set thresholds success! $thresholds"); + } catch (error, stacktrace) { + Log.d("set thresholds error!", error: error, stackTrace: stacktrace); + } + } + } + } catch (error, stacktrace) { + Log.d('AdImpressionController params TaichiConfig err', error: error, stackTrace: stacktrace); + } + } + + void init() async { + await _init(); + + AdImpressionListener.addCallback((event, Map arguments) async { + Log.d("------------addListener arguments$arguments"); + switch (event) { + case AdImpressionEvent.onAdImpression: + if (arguments != null) { + final payload = arguments["payload"]; + if (payload == null || payload.isEmpty) { + break; + } + final payloadMap = json.decode(payload); + ImpressionData impressionData = ImpressionData.fromJson(payloadMap); + // 判断是不是facebook的network + // if (impressionData.networkName == "undisclosed") { + // final calibrationCpm = await facebookCalibrator.getCpm(impressionData.unitFormat, impressionData.country); + // if (calibrationCpm > 0) { + // final newImpressionData = impressionData.derive(newPublisherRevenue: calibrationCpm); + // if (facebookCalibrator.config?.fbIrldReport == true) { + // AnalyticsUtils.logEventEx("tch_fb_ad_rev", value: calibrationCpm, parameters: { + // FirebaseEventsParams.AD_FORMAT: impressionData.unitFormat, + // FirebaseEventsParams.AD_UNIT_NAME: impressionData.unitName, + // FirebaseEventsParams.CURRENCY: impressionData.currency, + // "country": impressionData.country, + // "mopub_rev": impressionData.publisherRevenue + // }); + // } + // + // impressionData = newImpressionData; + // } + // } + await refreshLtv(impressionData); + // _reportAdImpression(arguments); + + final jsonPayload = jsonEncode(impressionData.payload); + latestImpressionPayload = jsonPayload; + if (impressionData.publisherRevenue > 0) { + // AdjustAdRevenue adRevenue = AdjustAdRevenue(AdjustConfig.AdRevenueSourceAppLovinMAX); + // adRevenue.setRevenue(impressionData.publisherRevenue, "USD"); + // adRevenue.adRevenueNetwork = impressionData.networkName; + // adRevenue.adRevenueUnit = impressionData.unitId; + // adRevenue.adRevenuePlacement = impressionData.networkPlacementId; + // Adjust.trackAdRevenueNew(adRevenue); + + GuruAnalytics.instance.loadAdjustAdRevenue(impressionData); + } + } + break; + } + }); + } + + _reportAdImpression(Map arguments) { + // adFormat 为:BANNER,REWARDED,INTER + final adFormat = arguments["ad_format"] ?? ""; + GuruAnalytics.instance.logAdImpression("max_imp", adFormat, + adName: Platform.isIOS ? "isd_$adFormat" : "sd_$adFormat"); + } + + void onImpression(Map arguments) {} + + Future refreshLtv(ImpressionData impressionData) async { + final revenue = impressionData.publisherRevenue; + final adPlatform = impressionData.platform; + final currency = impressionData.currency; + if (revenue != -1) { + _logAdRevenue(impressionData); + // _logAdLtv(revenue: revenue, adPlatform: adPlatform, currency: currency); + } + Log.d("refreshLtv payload:${impressionData.payload}"); + } + + // _logAdLtv({double revenue = 0.0, String adPlatform = "MAX", String currency = ""}) async { + // final nowDate = DateTimeUtils.yyyyMMddUtcNum; + // final appProperty = AppProperty.getInstance(); + // final totalLtv = await appProperty.getDouble(PropertyKeys.totalLtv, defValue: 0.0); + // final previousDate = await appProperty.getInt(PropertyKeys.previousLtvDate, defValue: 0); + // double previousLtv = await appProperty.getDouble(PropertyKeys.previousLtv, defValue: 0.0); + // if (previousDate != nowDate) { + // previousLtv = 0.0; + // appProperty.setInt(PropertyKeys.previousLtvDate, nowDate); + // } + // final currentLtv = previousLtv + revenue; + // for (int i = 0; i < ltvThresholds.length; ++i) { + // if (previousLtv < ltvThresholds[i] && currentLtv >= ltvThresholds[i]) { + // if (i < ltvPhaseNames.length) { + // Analytics.instance.logAdLtv(ltvPhaseNames[i], currentLtv); + // } + // } + // } + // AppProperty.getInstance().setDouble(PropertyKeys.previousLtv, currentLtv); + // AppProperty.getInstance().setDouble(PropertyKeys.totalLtv, totalLtv + revenue); + // } + + _logAdRevenue(ImpressionData data) async { + final appProperty = AppProperty.getInstance(); + + GuruAnalytics.instance.logAdImp(data); + + final abnormalThreshold = taichiConfig?.abnormalThreshold ?? 1; + if (data.publisherRevenue >= abnormalThreshold) { + try { + final parameters = CollectionUtils.filterOutNulls({ + "ad_platform": data.platform, + "value": data.publisherRevenue, + "currency": data.currency, + "ad_format": data.unitFormat, + "ad_source": data.networkName, + "ad_unit_name": data.unitName, + "country": data.country, + "precision": data.precision + }); + GuruAnalytics.instance.logEvent("tch_ad_rev_value_abnormal", parameters); + final payload = data.payload; + GuruAnalytics.instance.logException(AbnormalRevenueException(payload.toString()), + stacktrace: StackTrace.current); + Log.d(payload.toString()); + return; + } catch (error, stacktrace) { + Log.d("convert impression data error! $error", stackTrace: stacktrace); + } + } + + double totalRevenue = await appProperty.getDouble(PropertyKeys.totalRevenue, defValue: 0.0); + totalRevenue += data.publisherRevenue; + if (totalRevenue >= 0.01) { + GuruAnalytics.instance.logAdRevenue(totalRevenue, data.platform, data.currency); + GuruAnalytics.instance.logPurchase(totalRevenue, + currency: data.currency, contentId: "MAX", adPlatform: "MAX"); + + totalRevenue = .0; + } + appProperty.setDouble(PropertyKeys.totalRevenue, totalRevenue); + } +} diff --git a/guru_app/lib/ads/core/banner/banner_ads.dart b/guru_app/lib/ads/core/banner/banner_ads.dart new file mode 100644 index 0000000..c0b8dff --- /dev/null +++ b/guru_app/lib/ads/core/banner/banner_ads.dart @@ -0,0 +1,11 @@ +/// Created by Haoyi on 5/10/21 + +part of '../ads.dart'; + +abstract class BannerAds extends SingleAds with AdsAudit { + @override + void init() { + super.init(); + addObserver(BannerAdsReportEventsObserver()); + } +} diff --git a/guru_app/lib/ads/core/banner/banner_ads_handler.dart b/guru_app/lib/ads/core/banner/banner_ads_handler.dart new file mode 100644 index 0000000..fdf56d6 --- /dev/null +++ b/guru_app/lib/ads/core/banner/banner_ads_handler.dart @@ -0,0 +1,79 @@ +/// Created by Haoyi on 5/11/21 +part of '../ads.dart'; + +class BannerAdsReportEventsObserver extends AdsLifecycleObserver { + void _apply(AdsBundle adsBundle, void Function(BannerAds ads) callback) { + if (adsBundle.ads is BannerAds) { + try { + callback(adsBundle.ads as BannerAds); + } catch (error, stacktrace) { + Log.i('BannerAdsReportEventsHandler apply error:$error, $stacktrace'); + } + } + } + + @override + void onRequestLoad(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + GuruAnalytics.instance.logEventEx("bads_load"); + }); + } + + @override + void onAdLoaded(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + // AnalyticsUtils.logEventEx("bads_loaded", + // parameters: {"duration": ads.elapsedTimeInMillisSinceStartLoadAds}); + }); + } + + @override + void onAdLoadFailed(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + String errorCode = adsBundle.arguments["errorName"] ?? "Unknown"; + // TODO + // AnalyticsUtils.logEventEx("bads_failed", itemCategory: "load", parameters: { + // "duration": ads.elapsedTimeInMillisSinceStartLoadAds, + // "error_code": errorCode + // }); + }); + } + + @override + void onAdDisplayFailed(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + String errorCode = adsBundle.arguments["errorName"] ?? "Unknown"; + // TODO + // AnalyticsUtils.logEventEx("bads_failed", + // itemCategory: "imp", + // parameters: {"duration": ads.elapsedTimeInMillisSinceLoadedAds, "error_code": errorCode}); + }); + } + + @override + void onAdDisplayed(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + // AnalyticsUtils.logAdImpression("bads_imp", AdTypeName.AD_TYPE_BANNER, + // scene: ads.scene, + // adName: ads.scene, + // parameters: {"duration": "${ads.elapsedTimeInMillisSinceLoadedAds}"}); + }); + } + + @override + void onAdClicked(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + GuruAnalytics.instance.logAdClick("bads_clk", AdTypeName.AD_TYPE_BANNER, + scene: ads.scene, adName: ads.scene); + AiBi.instance.adsClk(AdsType.banner, adScene: ads.scene); + }); + } + + @override + void onAdHidden(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + GuruAnalytics.instance.logEventEx("bads_close", itemCategory: ads.scene); + AiBi.instance.adsHide(AdsType.banner, adScene: ads.scene); + }); + } +} diff --git a/guru_app/lib/ads/core/exceptions/ads_exceptions.dart b/guru_app/lib/ads/core/exceptions/ads_exceptions.dart new file mode 100644 index 0000000..995c5e7 --- /dev/null +++ b/guru_app/lib/ads/core/exceptions/ads_exceptions.dart @@ -0,0 +1,23 @@ +/// Created by Haoyi on 2021/9/3 + +class AbnormalRevenueException implements Exception { + final String payload; + + AbnormalRevenueException(this.payload); + + @override + String toString() { + return 'AbnormalRevenueException{payload: $payload}'; + } +} + +class ShowRewardedVideoAdsException implements Exception { + final String message; + + ShowRewardedVideoAdsException(this.message); + + @override + String toString() { + return 'ShowRewardedVideoAdsException{payload: $message}'; + } +} diff --git a/guru_app/lib/ads/core/handler/ads_audit.dart b/guru_app/lib/ads/core/handler/ads_audit.dart new file mode 100644 index 0000000..f799da2 --- /dev/null +++ b/guru_app/lib/ads/core/handler/ads_audit.dart @@ -0,0 +1,142 @@ +/// Created by Haoyi on 5/7/21 + +part of '../ads.dart'; + +mixin AdsAudit on Ads { + int latestShownAt = 0; + int latestStartLoadAt = 0; + int latestLoadedAt = 0; + int latestHiddenAt = 0; + String scene = ""; + int loadCount = 0; + + int get elapsedTimeInMillisSinceLoadedAds => DateTimeUtils.currentTimeInMillis() - latestLoadedAt; + + int get elapsedTimeInMillisSinceStartLoadAds => + DateTimeUtils.currentTimeInMillis() - latestStartLoadAt; + + void resetLatestLoadedAt() { + latestLoadedAt = 0; + } + + void resetAudit() { + latestShownAt = 0; + latestHiddenAt = 0; + latestLoadedAt = 0; + latestStartLoadAt = 0; + scene = ""; + } +} + +class AdsAuditObserver extends AdsLifecycleObserver { + String adsName; + + final String tag; + + AdsAuditObserver(this.adsName, {this.tag = PropertyTags.ads}); + + @override + String get name => "$adsName-AuditObs"; + + void _apply(AdsBundle adsBundle, void Function(AdsAudit) callback) { + if (adsBundle.ads is AdsAudit) { + callback(adsBundle.ads as AdsAudit); + } + } + + @override + void onRequestShow(AdsBundle adsBundle) { + Log.i("[$name] onRequestShow! ${adsBundle.arguments}", tag: tag); + _apply(adsBundle, (adsAudit) { + final adCause = adsBundle.getValue("cause", defValue: AdCause.internalError); + if (adCause == AdCause.success) { + adsAudit.scene = adsBundle.getString("scene"); + adsAudit.setProperty("latestScene", adsAudit.scene); + } + }); + } + + @override + void onRequestLoad(AdsBundle adsBundle) { + Log.i("[$name] onRequestLoad! ", tag: tag); + _apply(adsBundle, (adsAudit) { + final adCause = adsBundle.getValue("cause", defValue: AdCause.internalError); + final now = DateTimeUtils.currentTimeInMillis(); + final humanDate = DateTime.fromMillisecondsSinceEpoch(now).toString(); + if (adCause == AdCause.success) { + adsAudit.latestStartLoadAt = now; + adsAudit.setProperty("latestStartLoadTime", humanDate); + adsAudit.loadCount++; + adsAudit.setProperty("winCount", adsAudit.loadCount.toString()); + } else { + adsAudit.setProperty("latestLoadCause", "[$humanDate]:${adCause.toString()}"); + } + }); + } + + @override + void onRequestReset(AdsBundle adsBundle) { + Log.i("[$name] onRequestReset! ", tag: tag); + _apply(adsBundle, (adsAudit) async { + adsAudit.resetAudit(); + }); + } + + @override + void onAdLoaded(AdsBundle adsBundle) { + Log.i("[$name] onAdLoaded! ", tag: tag); + _apply(adsBundle, (adsAudit) { + adsAudit.latestLoadedAt = DateTimeUtils.currentTimeInMillis(); + adsAudit.setProperty("latestLoadedTime", + DateTime.fromMillisecondsSinceEpoch(adsAudit.latestLoadedAt).toString()); + }); + } + + @override + void onAdLoadFailed(AdsBundle adsBundle) { + Log.i("[$name] onAdLoadFailed! ", tag: tag); + _apply(adsBundle, (adsAudit) { + adsAudit.setProperty("latestLoadFailedTime", DateTime.now().toString()); + }); + } + + @override + Future onAdDisplayFailed(AdsBundle adsBundle) async { + Log.i("[$name] onHidden! ", tag: tag); + _apply(adsBundle, (adsAudit) { + adsAudit.setProperty("latestDisplayFailedTime", DateTime.now().toString()); + adsAudit.setProperty("latestCreativeId", adsBundle.getString("ad_creative_id")); + adsAudit.setProperty("latestNetworkName", adsBundle.getString("ad_network_name")); + }); + } + + @override + void onAdDisplayed(AdsBundle adsBundle) { + Log.i("[$name] onAdDisplayed! ", tag: tag); + _apply(adsBundle, (adsAudit) { + adsAudit.setProperty("latestDisplayTime", DateTime.now().toString()); + adsAudit.setProperty("latestCreativeId", adsBundle.getString("ad_creative_id")); + adsAudit.setProperty("latestNetworkName", adsBundle.getString("ad_network_name")); + }); + } + + @override + void onAdClicked(AdsBundle adsBundle) {} + + @override + void onAdHidden(AdsBundle adsBundle) { + Log.i("[$name] onHidden! ", tag: tag); + + _apply(adsBundle, (adsAudit) { + final now = DateTime.now(); + adsAudit.latestHiddenAt = now.millisecondsSinceEpoch; + adsAudit.setProperty("latestHiddenTime", now.toString()); + AdsManager.instance.latestFullscreenAdsHiddenTimestamps = now.millisecondsSinceEpoch; + }); + } + + @override + void onAdRewarded(AdsBundle adsBundle) { + HookManager.instance.watchRewardAds(); + } +} diff --git a/guru_app/lib/ads/core/handler/ads_cache.dart b/guru_app/lib/ads/core/handler/ads_cache.dart new file mode 100644 index 0000000..d865158 --- /dev/null +++ b/guru_app/lib/ads/core/handler/ads_cache.dart @@ -0,0 +1,170 @@ +/// Created by Haoyi on 5/7/21 +part of '../ads.dart'; + +class RetryConfig { + final int minInSecond; + final int maxInSecond; + + RetryConfig(this.minInSecond, this.maxInSecond); +} + +mixin AdsCache on Ads { + int retryAttempt = 0; + + DateTime latestPreloadAt = DateTime.now(); + Timer? _retryTimer; + + RetryConfig get retryConfig => RetryConfig(4, 30); + + bool get isLoadingRewardAdsDelayed => _retryTimer != null; + + void _resetRetryTimer() { + try { + _retryTimer?.cancel(); + _retryTimer = null; + } catch (error, stacktrace) { + _retryTimer = null; + } + } + + void _resetPreload() { + retryAttempt = 0; + _resetRetryTimer(); + setProperty("Retry Attempt", "0"); + } + + @override + void preload() { + latestPreloadAt = DateTime.now(); + setProperty("Latest Preload Time", latestPreloadAt.toString()); + _resetRetryTimer(); + if (AdsManager.instance.connectivityStatus != ConnectivityResult.none) { + load(); + } + } + + @override + void retry() { + if (_retryTimer?.isActive == true) { + return; + } + retryAttempt++; + final delaySecond = pow(2, retryAttempt).toInt(); + final config = retryConfig; + final duration = Duration(seconds: delaySecond.clamp(config.minInSecond, config.maxInSecond)); + setProperty("Retry Attempt", retryAttempt.toString()); + setProperty("Retry Interval", duration.toString()); + _retryTimer = Timer(duration, () { + preload(); + }); + } +} + +class AdsCacheObserver extends AdsLifecycleObserver { + final String adsName; + + AdsCacheObserver(this.adsName); + + @override + String get name => "$adsName-AdsCache"; + + void _apply(AdsBundle adsBundle, void Function(AdsCache) callback) { + if (adsBundle.ads is AdsCache) { + callback(adsBundle.ads as AdsCache); + } + } + + @override + void onRequestShow(AdsBundle adsBundle) { + Log.i("[$name] onRequestShow! ${adsBundle.arguments}", tag: "Ads"); + _apply(adsBundle, (adsCache) async { + final adCause = adsBundle.getValue("cause", defValue: AdCause.internalError); + if (adCause != AdCause.success && adCause != AdCause.tooFrequent) { + final state = await adsCache.getState(); + Log.d("[$name] onRequestShow state:$state", tag: "Ads"); + if (state != AdState.loaded) { + adsCache.preload(); + } else { + Log.d("[$name] onRequestShow ignore preload! AdsState: $state", tag: "Ads"); + } + } + }); + } + + @override + void onRequestLoad(AdsBundle adsBundle) { + // Log.i("[$name] onRequestLoad! ${adsBundle.arguments}", tag: "Ads"); + _apply(adsBundle, (adsCache) async { + final state = await adsCache.getState(); + final adCause = adsBundle.getValue("cause", defValue: AdCause.internalError); + if (adCause != AdCause.success && state != AdState.loaded && state != AdState.loading) { + Log.d("[$name] onRequestLoad adCause:$adCause state:$state", tag: "Ads"); + adsCache.retry(); + } else { + if (state == AdState.loaded) { + adsCache._resetPreload(); + adsCache.loaded = true; + } + Log.d("[$name] onRequestLoad ignore preload! AdsState: $state", tag: "Ads"); + } + }); + } + + @override + void onRequestReset(AdsBundle adsBundle) { + Log.i("[$name] onRequestReset! ", tag: "Ads"); + _apply(adsBundle, (adsCache) async { + adsCache._resetPreload(); + }); + } + + @override + void onAdDisplayed(AdsBundle adsBundle) { + Log.i("[$name] onAdDisplayed! ", tag: "Ads"); + _apply(adsBundle, (adsCache) { + adsCache.loaded = false; + }); + } + + @override + void onAdLoaded(AdsBundle adsBundle) { + Log.i("[$name] onAdLoaded! ", tag: "Ads"); + _apply(adsBundle, (adsCache) { + adsCache._resetPreload(); + adsCache.loaded = true; + }); + } + + @override + void onAdLoadFailed(AdsBundle adsBundle) { + Log.i("[$name] onAdLoadFailed! ", tag: "Ads"); + _apply(adsBundle, (adsCache) async { + adsCache.retry(); + adsCache.loaded = false; + + }); + } + + @override + void onAdDisplayFailed(AdsBundle adsBundle) { + Log.i("[$name] onAdDisplayFailed! ", tag: "Ads"); + _apply(adsBundle, (adsCache) { + adsCache.preload(); + adsCache.loaded = false; + }); + } + + @override + void onAdHidden(AdsBundle adsBundle) { + Log.i("[$name] onHidden! ", tag: "Ads"); + _apply(adsBundle, (adsCache) async { + final state = await adsCache.getState(); + if (state != AdState.loaded && state != AdState.loading) { + adsCache.loaded = false; + adsCache.preload(); + } else { + Log.d("[$name] onHidden ignore preload! AdsState: $state", tag: "Ads"); + } + }); + } +} diff --git a/guru_app/lib/ads/core/interstitial/interstitial_ads.dart b/guru_app/lib/ads/core/interstitial/interstitial_ads.dart new file mode 100644 index 0000000..3d2063f --- /dev/null +++ b/guru_app/lib/ads/core/interstitial/interstitial_ads.dart @@ -0,0 +1,11 @@ +/// Created by Haoyi on 5/6/21 + +part of '../ads.dart'; + +abstract class InterstitialAds extends SingleAds with AdsCache, AdsAudit { + @override + void init() { + super.init(); + addObserver(InterstitialAdsReportEventsObserver()); + } +} diff --git a/guru_app/lib/ads/core/interstitial/interstitial_ads_handler.dart b/guru_app/lib/ads/core/interstitial/interstitial_ads_handler.dart new file mode 100644 index 0000000..9275e1a --- /dev/null +++ b/guru_app/lib/ads/core/interstitial/interstitial_ads_handler.dart @@ -0,0 +1,102 @@ +/// Created by Haoyi on 5/10/21 +part of '../ads.dart'; + +class InterstitialAdsReportEventsObserver extends AdsLifecycleObserver { + @override + String get name => "InterstitialAdsReportEventsHandler"; + + void _apply(AdsBundle adsBundle, void Function(AdsAudit ads) callback) { + if (adsBundle.ads is AdsAudit) { + try { + callback(adsBundle.ads as AdsAudit); + } catch (error, stacktrace) { + Log.i('InterstitialAdsHandler apply error:$error, $stacktrace'); + } + } + } + + @override + void onRequestLoad(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + GuruAnalytics.instance.logEventEx("iads_load"); + }); + } + + @override + void onRequestReset(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + GuruAnalytics.instance.logEventEx("iads_rebuild"); + }); + } + + @override + void onAdLoaded(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + GuruAnalytics.instance.logEventEx("iads_loaded", + parameters: {"duration": ads.elapsedTimeInMillisSinceStartLoadAds}); + + AiBi.instance.adsLoaded(AdsType.interstitial, + adScene: ads.scene, duration: ads.elapsedTimeInMillisSinceStartLoadAds); + }); + } + + @override + void onAdLoadFailed(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + String errorCode = adsBundle.arguments["errorName"] ?? "Unknown"; + GuruAnalytics.instance.logEventEx("iads_failed", itemCategory: "load", parameters: { + "duration": ads.elapsedTimeInMillisSinceStartLoadAds, + "error_code": errorCode + }); + AiBi.instance.adsFailed(AdsType.interstitial, adScene: ads.scene); + }); + } + + @override + void onAdDisplayFailed(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + String errorCode = adsBundle.arguments["errorName"] ?? "Unknown"; + GuruAnalytics.instance.logEventEx("iads_failed", + itemCategory: "imp", + parameters: {"duration": ads.elapsedTimeInMillisSinceLoadedAds, "error_code": errorCode}); + Log.d( + "iads_display_failed creativeId:${adsBundle.getString("ad_creative_id")} networkName:${adsBundle.getString("ad_network_name")} errorCode:$errorCode duration:${ads.elapsedTimeInMillisSinceLoadedAds}", + syncFirebase: true); + AiBi.instance.adsFailed(AdsType.interstitial, adScene: ads.scene); + }); + } + + @override + void onAdDisplayed(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + GuruAnalytics.instance.logAdImpression("iads_imp", AdTypeName.AD_TYPE_INTERSTITIAL, + scene: ads.scene, + adName: ads.scene, + parameters: {"duration": "${ads.elapsedTimeInMillisSinceLoadedAds}"}); + Log.d( + "iads_imp creativeId:${adsBundle.getString("ad_creative_id")} networkName:${adsBundle.getString("ad_network_name")} duration:${ads.elapsedTimeInMillisSinceLoadedAds}", + syncFirebase: true); + AiBi.instance.adsImp(AdsType.interstitial, + adScene: ads.scene, + adRevenue: adsBundle.getDouble("ad_revenue", defValue: 0.0), + network: adsBundle.getString("ad_network_name", defValue: "unknown")); + }); + } + + @override + void onAdClicked(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + GuruAnalytics.instance.logAdClick("iads_clk", AdTypeName.AD_TYPE_INTERSTITIAL, + scene: ads.scene, adName: ads.scene); + AiBi.instance.adsClk(AdsType.interstitial, adScene: ads.scene); + }); + } + + @override + void onAdHidden(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + GuruAnalytics.instance.logEventEx("iads_close", itemCategory: ads.scene); + AiBi.instance.adsHide(AdsType.interstitial, adScene: ads.scene); + }); + } +} diff --git a/guru_app/lib/ads/core/rewards/rewarded_ads.dart b/guru_app/lib/ads/core/rewards/rewarded_ads.dart new file mode 100644 index 0000000..d61b971 --- /dev/null +++ b/guru_app/lib/ads/core/rewards/rewarded_ads.dart @@ -0,0 +1,11 @@ +/// Created by Haoyi on 5/26/21 + +part of '../ads.dart'; + +abstract class RewardedAds extends SingleAds with AdsCache, AdsAudit { + @override + void init() { + super.init(); + addObserver(RewardedAdsReportEventsObserver()); + } +} diff --git a/guru_app/lib/ads/core/rewards/rewarded_ads_handler.dart b/guru_app/lib/ads/core/rewards/rewarded_ads_handler.dart new file mode 100644 index 0000000..09aa677 --- /dev/null +++ b/guru_app/lib/ads/core/rewards/rewarded_ads_handler.dart @@ -0,0 +1,116 @@ +/// Created by Haoyi on 5/26/21 + +part of '../ads.dart'; + +class RewardedAdsReportEventsObserver extends AdsLifecycleObserver { + String get name => "RewardedAdsReportEventsHandler"; + + void _apply(AdsBundle adsBundle, void Function(RewardedAds ads) callback) { + if (adsBundle.ads is RewardedAds) { + try { + callback(adsBundle.ads as RewardedAds); + } catch (error, stacktrace) { + Log.w('RewardedAdsHandler apply error', error: error, stackTrace: stacktrace); + } + } + } + + @override + void onRequestLoad(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + GuruAnalytics.instance.logEventEx("rads_load"); + AiBi.instance.adsLoad(AdsType.rewarded, adScene: ads.scene); + }); + } + + @override + void onAdLoaded(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + GuruAnalytics.instance.logEventEx("rads_loaded", + parameters: {"duration": ads.elapsedTimeInMillisSinceStartLoadAds}); + AiBi.instance.adsLoaded(AdsType.rewarded, + adScene: ads.scene, duration: ads.elapsedTimeInMillisSinceStartLoadAds); + }); + } + + @override + void onRequestReset(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + GuruAnalytics.instance.logEventEx("rads_rebuild"); + }); + } + + @override + void onAdLoadFailed(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + String errorCode = adsBundle.arguments["errorName"] ?? "Unknown"; + GuruAnalytics.instance.logEventEx("rads_failed", itemCategory: "load", parameters: { + "duration": ads.elapsedTimeInMillisSinceStartLoadAds, + "error_code": errorCode + }); + AiBi.instance.adsFailed(AdsType.rewarded, adScene: ads.scene); + }); + } + + @override + void onAdDisplayFailed(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + String errorCode = adsBundle.arguments["errorName"] ?? "Unknown"; + GuruAnalytics.instance.logEventEx("rads_failed", + itemCategory: "imp", + parameters: {"duration": ads.elapsedTimeInMillisSinceLoadedAds, "error_code": errorCode}); + Log.d( + "rads_display_failed creativeId:${adsBundle.getString("ad_creative_id")} networkName:${adsBundle.getString("ad_network_name")} errorCode:$errorCode duration:${ads.elapsedTimeInMillisSinceLoadedAds}", + syncFirebase: true); + AiBi.instance.adsFailed(AdsType.rewarded, adScene: ads.scene); + }); + } + + @override + void onAdDisplayed(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + GuruAnalytics.instance.logAdImpression("rads_imp", AdTypeName.AD_TYPE_REWARDED_VIDEO, + scene: ads.scene, + adName: ads.scene, + parameters: {"duration": "${ads.elapsedTimeInMillisSinceLoadedAds}"}); + Log.d( + "rads_imp creativeId:${adsBundle.getString("ad_creative_id")} networkName:${adsBundle.getString("ad_network_name")} duration:${ads.elapsedTimeInMillisSinceLoadedAds}", + syncFirebase: true); + AiBi.instance.adsImp(AdsType.rewarded, + adScene: ads.scene, + adRevenue: adsBundle.getDouble("ad_revenue", defValue: 0), + network: adsBundle.getString("ad_network_name", defValue: "unknown")); + }); + } + + @override + void onAdClicked(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + GuruAnalytics.instance.logAdClick("rads_clk", AdTypeName.AD_TYPE_REWARDED_VIDEO, + scene: ads.scene, adName: ads.scene); + AiBi.instance.adsClk(AdsType.rewarded, adScene: ads.scene); + }); + } + + @override + void onAdHidden(AdsBundle adsBundle) { + _apply(adsBundle, (ads) { + GuruAnalytics.instance.logEventEx("rads_close", itemCategory: ads.scene); + AiBi.instance.adsHide(AdsType.rewarded, adScene: ads.scene); + }); + } + + @override + void onAdRewarded(AdsBundle adsBundle) { + _apply(adsBundle, (ads) async { + GuruAnalytics.instance.logEventEx("rads_rewarded", itemCategory: ads.scene); + final userRewardedCount = + await AppProperty.getInstance().getInt(PropertyKeys.userRewardedCount, defValue: 0); + if (userRewardedCount == 0) { + GuruAnalytics.instance.logEventEx("first_rads_rewarded", itemCategory: ads.scene); + } + await AppProperty.getInstance().setInt(PropertyKeys.userRewardedCount, userRewardedCount + 1); + AiBi.instance.adsRewarded(ads.scene); + }); + } +} diff --git a/guru_app/lib/ads/core/strategy/ad_unit.dart b/guru_app/lib/ads/core/strategy/ad_unit.dart new file mode 100644 index 0000000..746f93e --- /dev/null +++ b/guru_app/lib/ads/core/strategy/ad_unit.dart @@ -0,0 +1,17 @@ +import 'package:guru_app/ads/core/ads.dart'; +import 'package:guru_app/ads/core/strategy/handler/ad_unit_cache.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:guru_app/property/app_property.dart'; + +/// Created by Haoyi on 2023/6/28 + +abstract class AdUnit extends SingleAds with AdsAudit { + @override + final AdUnitId adUnitId; + final AdSlotId? amazonAdSlotId; + + AdUnit(this.adUnitId, {this.amazonAdSlotId}) { + // addObserver(AdUnitCacheObserver(adUnitId.id)); + addObserver(AdsAuditObserver("AdUnit[${adUnitId.id}]", tag: PropertyTags.strategyAds)); + } +} diff --git a/guru_app/lib/ads/core/strategy/handler/ad_unit_cache.dart b/guru_app/lib/ads/core/strategy/handler/ad_unit_cache.dart new file mode 100644 index 0000000..0ea6eb6 --- /dev/null +++ b/guru_app/lib/ads/core/strategy/handler/ad_unit_cache.dart @@ -0,0 +1,70 @@ +import 'package:guru_app/ads/core/ads.dart'; +import 'package:guru_app/ads/core/strategy/ad_unit.dart'; +import 'package:guru_app/property/app_property.dart'; +import 'package:guru_applovin_flutter/guru_applovin_flutter.dart'; + +import '../../../../guru_app.dart'; + +/// Created by Haoyi on 2023/6/28 + +mixin AdUnitCache on SingleAds { +} + +class AdUnitCacheObserver extends AdsLifecycleObserver { + final String adsName; + + AdUnitCacheObserver(this.adsName); + + @override + String get name => "$adsName-AdUnitCache"; + + void _apply(AdsBundle adsBundle, void Function(AdUnitCache) callback) { + if (adsBundle.ads is AdUnitCache) { + callback(adsBundle.ads as AdUnitCache); + } + } + + @override + void onAdDisplayed(AdsBundle adsBundle) { + _apply(adsBundle, (adsCache) { + adsCache.loaded = false; + }); + } + + @override + void onAdLoaded(AdsBundle adsBundle) { + Log.i("[$name] onAdLoaded! ", tag: PropertyTags.strategyAds); + _apply(adsBundle, (adsCache) { + adsCache.loaded = true; + }); + } + + @override + void onAdLoadFailed(AdsBundle adsBundle) { + Log.i("[$name] onAdLoadFailed! ", tag: PropertyTags.strategyAds); + _apply(adsBundle, (adsCache) async { + adsCache.loaded = false; + }); + } + + @override + void onAdDisplayFailed(AdsBundle adsBundle) { + Log.i("[$name] onAdDisplayFailed! ", tag: PropertyTags.strategyAds); + _apply(adsBundle, (adsCache) { + adsCache.loaded = false; + }); + } + + @override + void onAdHidden(AdsBundle adsBundle) { + Log.i("[$name] onHidden! ", tag: PropertyTags.strategyAds); + _apply(adsBundle, (adsCache) async { + final state = await adsCache.getStatus(); + if (state != AdStatus.LOADED && state != AdStatus.LOADING) { + adsCache.loaded = false; + } else { + Log.d("[$name] onHidden ignore preload! AdsState: $state", tag: PropertyTags.strategyAds); + } + }); + } +} \ No newline at end of file diff --git a/guru_app/lib/ads/core/strategy/interstitial/max_interstitial_ad_unit.dart b/guru_app/lib/ads/core/strategy/interstitial/max_interstitial_ad_unit.dart new file mode 100644 index 0000000..6cd1fef --- /dev/null +++ b/guru_app/lib/ads/core/strategy/interstitial/max_interstitial_ad_unit.dart @@ -0,0 +1,107 @@ +import 'package:guru_app/ads/core/ads.dart'; +import 'package:guru_app/ads/core/strategy/ad_unit.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:guru_app/property/app_property.dart'; +import 'package:guru_applovin_flutter/guru_applovin_flutter.dart'; +import 'package:guru_applovin_flutter/interstitial_ad.dart'; +import 'package:guru_utils/ads/data/ads_model.dart'; + +/// Created by Haoyi on 2023/6/28 + +class MaxInterstitialAdUnit extends AdUnit { + late InterstitialAd interstitialAd; + + @override + final String name; + + MaxInterstitialAdUnit.create(AdUnitId adUnitId, AdSlotId? amazonAdSlotId) + : name = "MaxInterAdUnit-${adUnitId.id}", + super(adUnitId, amazonAdSlotId: amazonAdSlotId) { + addObserver(InterstitialAdsReportEventsObserver()); + } + + @override + void init() { + super.init(); + interstitialAd = InterstitialAd( + adUnitId: adUnitId.id, adAmazonSlotId: amazonAdSlotId?.id, listener: dispatchEvent); + } + + @override + Map get eventsMapping => { + InterstitialAdEvent.onAdLoaded: AdsEvent.adLoaded, + InterstitialAdEvent.onAdLoadFailed: AdsEvent.adLoadFailed, + InterstitialAdEvent.onAdDisplayFailed: AdsEvent.adDisplayFailed, + InterstitialAdEvent.onAdDisplayed: AdsEvent.adDisplayed, + InterstitialAdEvent.onAdClicked: AdsEvent.adClick, + InterstitialAdEvent.onAdHidden: AdsEvent.adHidden, + }; + + @override + Future requestDispose() async { + try { + return await interstitialAd.dispose() ?? false; + } catch (error, stacktrace) { + Log.w("[$name] requestDispose error", error: error, stackTrace: stacktrace); + return false; + } + } + + @override + Future requestHide() async { + return false; + } + + @override + Future requestLoad() async { + try { + Log.w("[$name] requestLoad", tag: PropertyTags.strategyAds); + final result = await interstitialAd.load() ?? false; + if (result) { + return AdCause.success; + } else { + return AdCause.requestFailed; + } + } catch (error, stacktrace) { + Log.w("requestLoad error! $error $stacktrace", tag: PropertyTags.strategyAds); + return AdCause.internalError; + } + } + + @override + Future requestShow({required String scene, bool ignoreCheck = false}) async { + Log.d("[$name] requestShow", tag: PropertyTags.strategyAds, syncFirebase: true); + try { + final result = await interstitialAd.show() ?? false; + if (result) { + return AdCause.success; + } else { + return AdCause.requestFailed; + } + } catch (error, stacktrace) { + Log.w("requestShow error", + error: error, stackTrace: stacktrace, syncFirebase: true, tag: PropertyTags.strategyAds); + return AdCause.internalError; + } + } + + @override + Future checkLoaded() async { + try { + return await interstitialAd.isLoaded() ?? false; + } catch (error, stacktrace) { + return false; + } + } + + @override + Future getStatus() async { + try { + return await interstitialAd.getAdState(); + } catch (error, stacktrace) { + Log.w("[$name] getInterstitialAdStatus error", + error: error, stackTrace: stacktrace, syncFirebase: true, tag: PropertyTags.strategyAds); + return AdStatus.FAILED; + } + } +} diff --git a/guru_app/lib/ads/core/strategy/interstitial/max_strategy_interstitial_ads.dart b/guru_app/lib/ads/core/strategy/interstitial/max_strategy_interstitial_ads.dart new file mode 100644 index 0000000..861e298 --- /dev/null +++ b/guru_app/lib/ads/core/strategy/interstitial/max_strategy_interstitial_ads.dart @@ -0,0 +1,522 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/cupertino.dart'; +import 'package:guru_app/ads/ads_manager.dart'; +import 'package:guru_app/ads/core/ads.dart'; +import 'package:guru_app/ads/core/strategy/ad_unit.dart'; +import 'package:guru_app/ads/core/strategy/interstitial/max_interstitial_ad_unit.dart'; +import 'package:guru_app/ads/core/strategy/strategy_ads.dart'; +import 'package:guru_app/property/app_property.dart'; +import 'package:guru_utils/ads/ads_delegate.dart'; +import 'package:guru_utils/ads/data/ads_model.dart'; +import 'package:guru_utils/ads/handler/ads_handler.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; + +/// Created by Haoyi on 2023/6/28 + +typedef AdsEventDispatcher = void Function(AdsEvent, {Map arguments}); + +abstract class AdsStrategy extends AdsLifecycleObserver { + final AdsEventDispatcher eventDispatcher; + + AdsStrategy(this.eventDispatcher); + + bool get loaded; + + Stream get observableLoaded; + + Future requestLoad(); + + Future requestShow({required String scene}); + + Future requestHide(); + + Future requestReset(); + + Future getState(); + + Future requestDispose(); + + void dispatchEvent(AdsEvent adsEvent, + {Map arguments = const {}}) { + eventDispatcher.call(adsEvent, arguments: arguments); + } +} + +class AdUnitRetryAgent { + MaxInterstitialAdUnit? adUnit; + final RetryConfig retryConfig; + int retryAttempt = 0; + + Timer? _retryTimer; + + AdUnitRetryAgent(this.adUnit, this.retryConfig); + + void dispose() { + _retryTimer?.cancel(); + _retryTimer = null; + adUnit = null; + } + + Future retry() async { + final ad = adUnit; + if (_retryTimer?.isActive == true || ad == null) { + return false; + } + final adState = await ad.getState(); + if (adState == AdState.loaded) { + return false; + } + retryAttempt++; + final delaySecond = pow(2, retryAttempt).toInt(); + final config = retryConfig; + final duration = Duration(seconds: delaySecond.clamp(config.minInSecond, config.maxInSecond)); + _retryTimer = Timer(duration, () { + adUnit?.requestLoad(); + }); + return true; + } +} + +class MaxInterstitialStrategy extends AdsStrategy { + final List adUnits = []; + + MaxInterstitialAdUnit? _showingAdUnit; + + final BehaviorSubject _strategyAdsStateSubject = + BehaviorSubject.seeded(StrategyAdsState.init); + + StrategyAdsState get strategyAdsState => _strategyAdsStateSubject.value; + + set strategyAdsState(StrategyAdsState value) { + _strategyAdsStateSubject.add(value); + } + + Stream get observableStrategyAdsState => _strategyAdsStateSubject.stream; + + @override + bool get loaded => strategyAdsState == StrategyAdsState.loaded; + + final Map _loadedAdsArguments = {}; + + bool upcomingAdLoadedEvent = false; + + @override + Stream get observableLoaded => + observableStrategyAdsState.map((event) => event == StrategyAdsState.loaded); + + final List loadTimers = []; + + AdUnitRetryAgent? retryAgent; + + MaxInterstitialStrategy(List adIds, AdsEventDispatcher dispatcher) : super(dispatcher) { + for (var adId in adIds) { + adUnits.add(MaxInterstitialAdUnit.create(adId.adUnitId, adId.amazonAdSlotId) + ..addObserver(this) + ..init()); + } + } + + @override + void onAdLoaded(AdsBundle adsBundle) { + if (strategyAdsState != StrategyAdsState.loaded) { + disposeRetryAgent(); + strategyAdsState = StrategyAdsState.loaded; + _loadedAdsArguments.clear(); + _loadedAdsArguments.addAll(adsBundle.arguments); + if (_showingAdUnit == null) { + dispatchEvent(AdsEvent.adLoaded, arguments: adsBundle.arguments); + } else { + upcomingAdLoadedEvent = true; + } + } + } + + @override + void onAdLoadFailed(AdsBundle adsBundle) async { + // 如果 retryAgent 存在,证明无底价的广告正在加载,这个时候高价广告不需要加载 + // 这样的设计是该 strategy 的定义,如需修改,请重新定义一个新的 strategy + final _retryAgent = retryAgent; + if (_retryAgent != null) { + // 如果最在 retryAgent,将忽略其它的广告加载失败 + if (_retryAgent.adUnit != adsBundle.ads) { + return; + } + _retryAgent.retry(); + return; + } + final noReservePriceAdUnit = adUnits.safeLast; + // 如果是无底价广告加载失败,那么将会触发重试机制 + if (adsBundle.ads == noReservePriceAdUnit) { + retryAgent = AdUnitRetryAgent(noReservePriceAdUnit, RetryConfig(4, 30))..retry(); + return; + } + checkAndRetry(msg: "onAdLoadFailed"); + } + + @override + void onAdDisplayed(AdsBundle adsBundle) async { + _showingAdUnit = adsBundle.ads as MaxInterstitialAdUnit; + dispatchEvent(AdsEvent.adDisplayed, arguments: adsBundle.arguments); + } + + @override + void onAdDisplayFailed(AdsBundle adsBundle) async { + final failedAdUnit = adsBundle.ads as MaxInterstitialAdUnit; + for (var adUnit in adUnits) { + final state = await adUnit.getState(); + if (state != AdState.loaded || failedAdUnit == adUnit) { + continue; + } + Log.d("[${failedAdUnit.adUnitId}] AdDisplayFailed! use ${adUnit.adUnitId} Ads Instead", + tag: PropertyTags.strategyAds); + try { + final result = await adUnit.show(scene: failedAdUnit.scene); + if (result == AdCause.success) { + _showingAdUnit = adUnit; + return; + } else { + continue; + } + } catch (error, stacktrace) { + Log.w("requestShow error", + tag: PropertyTags.strategyAds, + error: error, + stackTrace: stacktrace, + syncFirebase: true); + continue; + } + } + dispatchEvent(AdsEvent.adDisplayFailed, arguments: adsBundle.arguments); + Log.d("onAdDisplayFailed [$runtimeType] Not Found valid ads! requestLoad", + tag: PropertyTags.strategyAds); + requestLoad(); + } + + @override + void onAdHidden(AdsBundle adsBundle) { + dispatchEvent(AdsEvent.adHidden, arguments: adsBundle.arguments); + Log.d("[${_showingAdUnit?.adUnitId}] onAdHidden!", tag: PropertyTags.strategyAds); + _showingAdUnit = null; + checkAndRetry(msg: "onAdHidden"); + } + + @override + void onAdClicked(AdsBundle adsBundle) { + dispatchEvent(AdsEvent.adClick, arguments: adsBundle.arguments); + } + + void disposeLoadTimer() { + for (var timer in loadTimers) { + timer.cancel(); + } + loadTimers.clear(); + } + + void disposeRetryAgent() { + retryAgent?.dispose(); + retryAgent = null; + } + + @override + Future requestLoad() async { + final loadAdUnits = []; + Log.d("[$runtimeType-$hashCode] requestLoad $strategyAdsState", tag: PropertyTags.strategyAds); + switch (strategyAdsState) { + case StrategyAdsState.init: + loadAdUnits.addAll(adUnits); + break; + case StrategyAdsState.loaded: + consumeUpcomingAdLoadedEvent(); + dispatchEvent(AdsEvent.adLoaded, arguments: _loadedAdsArguments); + return AdCause.success; + case StrategyAdsState.disposed: + return AdCause.loadFailed; + default: + strategyAdsState = StrategyAdsState.idle; + for (var adUnit in adUnits) { + final state = await adUnit.getState(); + Log.d("[$runtimeType] requestLoad check ${adUnit.adUnitId} is $state", + tag: PropertyTags.strategyAds); + if (state == AdState.loaded) { + strategyAdsState = StrategyAdsState.loaded; + consumeUpcomingAdLoadedEvent(); + dispatchEvent(AdsEvent.adLoaded, arguments: _loadedAdsArguments); + return AdCause.success; + } + if (state != AdState.loading && adUnit != _showingAdUnit) { + loadAdUnits.add(adUnit); + } + } + } + + if (loadAdUnits.isEmpty) { + return AdCause.success; + } + disposeLoadTimer(); + disposeRetryAgent(); + final timers = []; + const gap = Duration(seconds: 5); + Duration delay = Duration.zero; + AdCause result = AdCause.requestFailed; + for (var adUnit in loadAdUnits) { + try { + if (delay > Duration.zero) { + timers.add(Timer(delay, () async { + if (strategyAdsState != StrategyAdsState.disposed) { + final r = await adUnit.load(); + Log.d("[$runtimeType] request load ${adUnit.adUnitId} $r", + tag: PropertyTags.strategyAds); + } + })); + delay += gap; + } else { + result = await adUnit.load(); + if (result == AdCause.success) { + Log.d("[$runtimeType] request load ${adUnit.adUnitId} success", + tag: PropertyTags.strategyAds); + delay += gap; + } + } + } catch (error, stacktrace) { + Log.w("requestLoad error! $error $stacktrace", tag: PropertyTags.strategyAds); + continue; + } + } + loadTimers.addAll(timers); + return result; + } + + @override + Future requestShow({required String scene}) async { + final result = AdsManager.instance.canShowInterstitial(scene); + if (result != AdCause.success) { + return result; + } + if (strategyAdsState == StrategyAdsState.disposed) { + Log.d("[$runtimeType] requestShow Ads is disposed!", tag: PropertyTags.strategyAds); + return AdCause.displayFailed; + } + strategyAdsState = StrategyAdsState.idle; + for (var adUnit in adUnits) { + final state = await adUnit.getState(); + if (state != AdState.loaded) { + Log.d("[$runtimeType] requestShow Check!! Ads [${adUnit.adUnitId}] state is $state", + tag: PropertyTags.strategyAds); + continue; + } + + try { + final result = await adUnit.show(scene: scene); + Log.d("[$runtimeType] requestShow [${adUnit.adUnitId}]", tag: PropertyTags.strategyAds); + if (result == AdCause.success) { + _showingAdUnit = adUnit; + return AdCause.success; + } else { + continue; + } + } catch (error, stacktrace) { + Log.w("requestShow error ${adUnit.adUnitId}", + error: error, + stackTrace: stacktrace, + syncFirebase: true, + tag: PropertyTags.strategyAds); + continue; + } + } + Log.d("requestShow [$runtimeType] Not Found valid ads! requestLoad", + tag: PropertyTags.strategyAds); + requestLoad(); + return AdCause.requestFailed; + } + + bool consumeUpcomingAdLoadedEvent() { + final result = upcomingAdLoadedEvent; + upcomingAdLoadedEvent = false; + return result; + } + + @override + Future requestHide() async { + return false; + } + + @override + Future requestDispose() async { + for (var adUnit in adUnits) { + try { + Log.d("[$runtimeType] requestDispose [${adUnit.adUnitId}]", tag: PropertyTags.strategyAds); + await adUnit.dispose(); + } catch (error, stacktrace) { + Log.w("requestDispose error", + error: error, + stackTrace: stacktrace, + syncFirebase: true, + tag: PropertyTags.strategyAds); + } + } + adUnits.clear(); + strategyAdsState = StrategyAdsState.disposed; + return true; + } + + @override + Future getState() async { + final adStates = []; + for (var adUnit in adUnits) { + final state = await adUnit.getState(); + adStates.add(state); + } + + for (var state in adStates) { + if (state == AdState.loaded) { + return AdState.loaded; + } + } + + for (var state in adStates) { + if (state == AdState.loading) { + return AdState.loading; + } + } + + for (var state in adStates) { + if (state != AdState.failed) { + return AdState.created; + } + } + return AdState.failed; + } + + @override + Future requestReset() async { + return AdCause.internalError; + } + + void dispatchAdLoadedEvent() { + dispatchEvent(AdsEvent.adLoaded, arguments: _loadedAdsArguments); + } + + Future checkAndRetry({String? msg}) async { + if (strategyAdsState == StrategyAdsState.disposed) { + Log.d("[${msg ?? runtimeType}] checkAndRetry Ads is disposed!", + tag: PropertyTags.strategyAds); + return; + } + bool loading = false; + for (var adUnit in adUnits) { + try { + final state = await adUnit.getState(); + Log.d("[${msg ?? runtimeType}] checkAndRetry ad [${adUnit.adUnitId}] $state", + tag: PropertyTags.strategyAds); + if (state == AdState.loading) { + loading = true; + continue; + } + if (state == AdState.loaded) { + if (consumeUpcomingAdLoadedEvent()) { + dispatchEvent(AdsEvent.adLoaded, arguments: _loadedAdsArguments); + } + strategyAdsState = StrategyAdsState.loaded; + continue; + } + } catch (error, stacktrace) { + Log.w("check state error", + error: error, stackTrace: stacktrace, tag: PropertyTags.strategyAds); + } + } + if (loading) { + Log.d("[${msg ?? runtimeType}] checkAndRetry loading! waiting ads...", + tag: PropertyTags.strategyAds); + strategyAdsState = StrategyAdsState.idle; + return; + } + if (strategyAdsState != StrategyAdsState.loaded) { + Log.d("[${msg ?? runtimeType}] Not Found loaded ads! requestLoad", + tag: PropertyTags.strategyAds); + strategyAdsState = StrategyAdsState.init; + requestLoad(); + } + } +} + +class MaxStrategyInterstitialAds extends StrategyAds with AdsAudit { + MaxStrategyInterstitialAds.create(List adIds) : super() { + strategy = MaxInterstitialStrategy(adIds, dispatchEvent); + } + + @override + @mustCallSuper + Future dispose() async { + bool result = false; + try { + result = await strategy.requestDispose(); + } catch (error, stacktrace) { + Log.d("requestDispose error:$error $stacktrace", tag: PropertyTags.strategyAds); + } + onRequestDispose(AdsBundle.create(this)); + super.dispose(); + return result; + } + + @override + @mustCallSuper + Future reset() async { + AdCause result = AdCause.internalError; + try { + result = await strategy.requestReset(); + } catch (error, stacktrace) { + Log.d("requestReset error:$error $stacktrace", tag: PropertyTags.strategyAds); + } + onRequestReset(AdsBundle.create(this)); + return result == AdCause.success; + } + + @mustCallSuper + void init() {} + + @override + Future load() async { + final adCause = await strategy.requestLoad().catchError((error, stacktrace) { + Log.e("load error! ", + tag: PropertyTags.strategyAds, error: error, stackTrace: stacktrace, syncFirebase: true); + return AdCause.internalError; + }); + Log.d("[$runtimeType]request load complete!! $adCause", tag: PropertyTags.strategyAds); + onRequestLoad(AdsBundle.create(this, arguments: {"cause": adCause})); + return adCause; + } + + @override + Future hide() async { + onRequestHide(AdsBundle.create(this)); + return await strategy.requestHide(); + } + + @override + Future show({required String scene, bool ignoreCheck = false}) async { + final adCause = await strategy.requestShow(scene: scene).catchError((error, stacktrace) { + Log.e("show error! $error", stackTrace: stacktrace, syncFirebase: true); + return AdCause.internalError; + }); + Log.d("[$runtimeType]show $scene complete!! $adCause", + syncFirebase: true, tag: PropertyTags.strategyAds); + onRequestShow(AdsBundle.create(this, arguments: {"scene": scene, "cause": adCause})); + return adCause; + } + + @override + Future getState() async { + return await strategy.getState(); + } + + @override + void preload() { + if (AdsManager.instance.connectivityStatus != ConnectivityResult.none) { + load(); + } + } +} diff --git a/guru_app/lib/ads/core/strategy/strategy_ads.dart b/guru_app/lib/ads/core/strategy/strategy_ads.dart new file mode 100644 index 0000000..8d4c6c0 --- /dev/null +++ b/guru_app/lib/ads/core/strategy/strategy_ads.dart @@ -0,0 +1,60 @@ +import 'package:guru_app/ads/core/ads.dart'; +import 'package:guru_app/ads/core/strategy/interstitial/max_strategy_interstitial_ads.dart'; +import 'package:guru_utils/ads/ads_delegate.dart'; +import 'package:guru_utils/ads/data/ads_model.dart'; +import 'package:guru_utils/ads/handler/ads_handler.dart'; +import 'package:guru_utils/extensions/extensions.dart'; + +/// Created by Haoyi on 2023/6/27 + +enum StrategyAdsState { init, idle, loaded, disposed } + +enum StrategyAdsPhase { create, loading, loaded, displayed, } + +abstract class StrategyAds extends Ads { + final BehaviorSubject loadedSubject = BehaviorSubject.seeded(false); + + @override + set loaded(bool loaded) { + loadedSubject.addEx(loaded); + setProperty("isLoaded", loaded ? "true" : "false"); + } + + @override + bool get loaded => strategy.loaded; + + @override + Stream get observableLoaded => strategy.observableLoaded; + + late final AdsStrategy strategy; + + StrategyAds(); + + void dispatchEvent(AdsEvent adsEvent, + {Map arguments = const {}}) { + final adsBundle = AdsBundle.create(this, arguments: arguments); + switch (adsEvent) { + case AdsEvent.adLoaded: + onAdLoaded(adsBundle); + break; + case AdsEvent.adLoadFailed: + onAdLoadFailed(adsBundle); + break; + case AdsEvent.adDisplayed: + onAdDisplayed(adsBundle); + break; + case AdsEvent.adDisplayFailed: + onAdDisplayFailed(adsBundle); + break; + case AdsEvent.adClick: + onAdClicked(adsBundle); + break; + case AdsEvent.adHidden: + onAdHidden(adsBundle); + break; + case AdsEvent.adRewarded: + onAdRewarded(adsBundle); + break; + } + } +} diff --git a/guru_app/lib/ads/utils/ads_cpm_calibration.dart b/guru_app/lib/ads/utils/ads_cpm_calibration.dart new file mode 100644 index 0000000..db3ba44 --- /dev/null +++ b/guru_app/lib/ads/utils/ads_cpm_calibration.dart @@ -0,0 +1,6 @@ +/// Created by Haoyi on 2021/11/26 + +class AdsCpmCalibration { + static const defaultAndroidFacebookCpmCalibrationData = ''; + static const defaultIOSFacebookCpmCalibrationData = '{"data":{"list":[{"format":"inter","cpm":0.032877,"country":"us"},{"format":"reward","cpm":0.107767,"country":"us"},{"format":"inter","cpm":0.005034,"country":"in"},{"format":"reward","cpm":null,"country":"in"},{"format":"inter","cpm":0.011241,"country":"ca"},{"format":"reward","cpm":0.094526,"country":"ca"},{"format":"inter","cpm":0.003824,"country":"br"},{"format":"reward","cpm":0.009776,"country":"br"},{"format":"inter","cpm":0.049431,"country":"au"},{"format":"reward","cpm":0.056175,"country":"au"},{"format":"inter","cpm":0.012067,"country":"jp"},{"format":"reward","cpm":0.055358,"country":"jp"},{"format":"inter","cpm":0.01472,"country":"de"},{"format":"reward","cpm":0.020876,"country":"de"},{"format":"inter","cpm":0.011296,"country":"gb"},{"format":"reward","cpm":0.027093,"country":"gb"},{"format":"inter","cpm":0.006439,"country":"fr"},{"format":"reward","cpm":0.018981,"country":"fr"},{"format":"inter","cpm":null,"country":"tw"},{"format":"reward","cpm":null,"country":"tw"},{"format":"inter","cpm":null,"country":"kr"},{"format":"reward","cpm":null,"country":"kr"},{"format":"inter","cpm":0.010416,"country":"ph"},{"format":"reward","cpm":null,"country":"ph"},{"format":"inter","cpm":0.00599,"country":"ru"},{"format":"reward","cpm":0.004472,"country":"ru"},{"format":"inter","cpm":null,"country":"it"},{"format":"reward","cpm":null,"country":"it"},{"format":"inter","cpm":0.010365,"country":"es"},{"format":"reward","cpm":null,"country":"es"},{"format":"inter","cpm":null,"country":"hk"},{"format":"reward","cpm":null,"country":"hk"},{"format":"inter","cpm":0.007427,"country":"mx"},{"format":"reward","cpm":0.01035,"country":"mx"},{"format":"inter","cpm":null,"country":"nl"},{"format":"reward","cpm":null,"country":"nl"},{"format":"inter","cpm":null,"country":"sa"},{"format":"reward","cpm":null,"country":"sa"},{"format":"inter","cpm":null,"country":"be"},{"format":"reward","cpm":null,"country":"be"},{"format":"inter","cpm":null,"country":"se"},{"format":"reward","cpm":null,"country":"se"},{"format":"inter","cpm":null,"country":"sg"},{"format":"reward","cpm":null,"country":"sg"},{"format":"inter","cpm":null,"country":"cl"},{"format":"reward","cpm":null,"country":"cl"},{"format":"inter","cpm":0.01166,"country":"ch"},{"format":"reward","cpm":null,"country":"ch"},{"format":"inter","cpm":null,"country":"fi"},{"format":"reward","cpm":null,"country":"fi"},{"format":"inter","cpm":null,"country":"th"},{"format":"reward","cpm":null,"country":"th"},{"format":"inter","cpm":0.00109,"country":"pl"},{"format":"reward","cpm":null,"country":"pl"},{"format":"inter","cpm":null,"country":"dk"},{"format":"reward","cpm":null,"country":"dk"},{"format":"inter","cpm":null,"country":"ae"},{"format":"reward","cpm":null,"country":"ae"},{"format":"inter","cpm":0.009707,"country":"at"},{"format":"reward","cpm":0.011048,"country":"at"},{"format":"inter","cpm":0.004342,"country":"id"},{"format":"reward","cpm":null,"country":"id"},{"format":"inter","cpm":0.005622,"country":"vn"},{"format":"reward","cpm":0.005026,"country":"vn"},{"format":"inter","cpm":0.001684,"country":"tr"},{"format":"reward","cpm":null,"country":"tr"}]}}'; +} diff --git a/guru_app/lib/ads/utils/ads_exception.dart b/guru_app/lib/ads/utils/ads_exception.dart new file mode 100644 index 0000000..9f072bb --- /dev/null +++ b/guru_app/lib/ads/utils/ads_exception.dart @@ -0,0 +1,15 @@ +import "dart:core"; + +/// Created by Haoyi on 2021/11/29 +/// + +class NoAdsException implements Exception { + final String msg; + + NoAdsException(this.msg); + + @override + String toString() { + return 'NoAdsException{msg: $msg}'; + } +} diff --git a/guru_app/lib/aigc/bi/ai_bi.dart b/guru_app/lib/aigc/bi/ai_bi.dart new file mode 100644 index 0000000..5f7c013 --- /dev/null +++ b/guru_app/lib/aigc/bi/ai_bi.dart @@ -0,0 +1,2 @@ +import "package:guru_utils/aigc/bi/ai_bi.dart"; +export "package:guru_utils/aigc/bi/ai_bi.dart"; \ No newline at end of file diff --git a/guru_app/lib/analytics/data/analytics_model.dart b/guru_app/lib/analytics/data/analytics_model.dart new file mode 100644 index 0000000..64dd11a --- /dev/null +++ b/guru_app/lib/analytics/data/analytics_model.dart @@ -0,0 +1,74 @@ +import 'package:guru_analytics_flutter/events_constants.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:guru_utils/converts/converts.dart'; + +/// Created by Haoyi on 2023/2/9 +/// +part 'analytics_model.g.dart'; + +@JsonSerializable() +class AnalyticsConfig { + @JsonKey(name: "cap", defaultValue: ["firebase", "facebook", "guru"]) + @joinedStringConvert + final List capabilities; + + @JsonKey(name: "init_delay_s", defaultValue: 10) + final int delayedInSeconds; + + @JsonKey(name: "expired_d", defaultValue: 7) + final int expiredInDays; + + @JsonKey(name: "strategy", defaultValue: '') + final String strategy; + + @JsonKey(name: "enabled_strategy", defaultValue: false) + final bool enabledStrategy; + + AppEventCapabilities toAppEventCapabilities() { + int capValue = 0; + if (capabilities.contains("firebase")) { + capValue |= AppEventCapabilities.firebase; + } + if (capabilities.contains("facebook")) { + capValue |= AppEventCapabilities.facebook; + } + if (capabilities.contains("guru")) { + capValue |= AppEventCapabilities.guru; + } + return AppEventCapabilities(capValue); + } + + AnalyticsConfig(this.capabilities, this.delayedInSeconds, this.expiredInDays, this.strategy, + this.enabledStrategy); + + factory AnalyticsConfig.fromJson(Map json) => _$AnalyticsConfigFromJson(json); + + Map toJson() => _$AnalyticsConfigToJson(this); +} + +@JsonSerializable() +class UserIdentification { + @JsonKey(name: 'firebaseAppInstanceId', defaultValue: "") + final String firebaseAppInstanceId; + + @JsonKey(name: 'idfa') + final String? idfa; + + @JsonKey(name: "adid") + final String? adId; + + @JsonKey(name: "gpsAdid") + final String? gpsAdId; + + UserIdentification({this.firebaseAppInstanceId = '', this.idfa, this.adId, this.gpsAdId}); + + factory UserIdentification.fromJson(Map json) => + _$UserIdentificationFromJson(json); + + Map toJson() => _$UserIdentificationToJson(this); + + @override + String toString() { + return 'UserIdentification{firebaseAppInstanceId: $firebaseAppInstanceId, idfa: $idfa, adId: $adId, gpsAdId: $gpsAdId}'; + } +} diff --git a/guru_app/lib/analytics/data/analytics_model.g.dart b/guru_app/lib/analytics/data/analytics_model.g.dart new file mode 100644 index 0000000..47a34ce --- /dev/null +++ b/guru_app/lib/analytics/data/analytics_model.g.dart @@ -0,0 +1,43 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'analytics_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AnalyticsConfig _$AnalyticsConfigFromJson(Map json) => + AnalyticsConfig( + json['cap'] == null + ? ['firebase', 'facebook', 'guru'] + : joinedStringConvert.fromJson(json['cap'] as String), + json['init_delay_s'] as int? ?? 10, + json['expired_d'] as int? ?? 7, + json['strategy'] as String? ?? '', + json['enabled_strategy'] as bool? ?? false, + ); + +Map _$AnalyticsConfigToJson(AnalyticsConfig instance) => + { + 'cap': joinedStringConvert.toJson(instance.capabilities), + 'init_delay_s': instance.delayedInSeconds, + 'expired_d': instance.expiredInDays, + 'strategy': instance.strategy, + 'enabled_strategy': instance.enabledStrategy, + }; + +UserIdentification _$UserIdentificationFromJson(Map json) => + UserIdentification( + firebaseAppInstanceId: json['firebaseAppInstanceId'] as String? ?? '', + idfa: json['idfa'] as String?, + adId: json['adid'] as String?, + gpsAdId: json['gpsAdid'] as String?, + ); + +Map _$UserIdentificationToJson(UserIdentification instance) => + { + 'firebaseAppInstanceId': instance.firebaseAppInstanceId, + 'idfa': instance.idfa, + 'adid': instance.adId, + 'gpsAdid': instance.gpsAdId, + }; diff --git a/guru_app/lib/analytics/guru_analytics.dart b/guru_app/lib/analytics/guru_analytics.dart new file mode 100644 index 0000000..4cb9aaa --- /dev/null +++ b/guru_app/lib/analytics/guru_analytics.dart @@ -0,0 +1,599 @@ +/// Created by Haoyi on 2022/8/24 + +import 'dart:collection'; +import 'dart:core'; +import 'dart:io'; + +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:flutter/foundation.dart'; +import 'package:guru_analytics_flutter/event_logger.dart'; +import 'package:guru_analytics_flutter/event_logger_common.dart'; +import 'package:guru_analytics_flutter/events_constants.dart'; +import 'package:guru_analytics_flutter/guru/guru_event_logger.dart'; +import 'package:guru_analytics_flutter/guru/guru_statistic.dart'; +import 'package:guru_app/account/account_data_store.dart'; +import 'package:guru_app/ads/ads_manager.dart'; +import 'package:guru_app/ads/core/ads_config.dart'; +import 'package:guru_app/aigc/bi/ai_bi.dart'; +import 'package:guru_app/analytics/data/analytics_model.dart'; +import 'package:guru_app/analytics/strategy/guru_analytics_strategy.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/property_keys.dart'; +import 'package:guru_app/property/runtime_property.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_utils/device/device_info.dart'; +import 'package:guru_utils/device/device_utils.dart'; +import 'package:guru_utils/analytics/analytics.dart'; +import 'package:guru_utils/network/network_utils.dart'; +import 'package:intl/intl.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:adjust_sdk/adjust_ad_revenue.dart'; +import 'package:adjust_sdk/adjust_config.dart'; +export 'package:adjust_sdk/adjust.dart'; + +part 'modules/ads_analytics.dart'; + +part 'modules/adjust_aware.dart'; + +class GuruAnalytics extends Analytics with AdjustAware { + bool get release => !_mock && _enabledAnalytics && kReleaseMode; + + String appInstanceId = ""; + + static bool _mock = false; + + static bool _enabledAnalytics = true; + + static GuruAnalytics instance = GuruAnalytics._(); + + /// Name of virtual currency type. + static bool initialized = false; + + static final Map facebookEventMapping = {}; + + static String currentScreen = ""; + + static const errorEventCodes = { + 14, // 上报事件失败 + 22, // 网络状态不可用 + 101, // 调用api出错 + 102, // api返回结果错误 + 103, // 设置cacheControl出错 + 104, // 删除过期事件出错 + 105, // 从数据库取事件以及更改事件状态为正在上报出错 + 106, // dns 错误 + }; + + int latestFetchStatisticTs = 0; + + final BehaviorSubject guruEventStatistic = + BehaviorSubject.seeded(GuruStatistic.invalid); + + Stream get observableGuruEventStatistic => guruEventStatistic.stream; + + final BehaviorSubject userIdentificationSubject = + BehaviorSubject.seeded(UserIdentification()); + + UserIdentification get userIdentification => userIdentificationSubject.value; + + AppEventCapabilities get currentAppEventCapabilities => EventLogger.getCapabilities(); + + static void setMock() { + _mock = true; + } + + static void disableAnalytics() { + _enabledAnalytics = false; + } + + static void enableAnalytics() { + _enabledAnalytics = true; + } + + GuruAnalytics._(); + + String? getProperty(String key) { + return Analytics.userProperties[key]; + } + + void init() async { + Log.d( + "AnalyticsUtil init### Platform.localeName :${Platform.localeName} ${Intl.getCurrentLocale()}"); + if (!_mock && !initialized) { + final analyticsConfig = RemoteConfigManager.instance.getAnalyticsConfig(); + EventLogger.setCapabilities(analyticsConfig.toAppEventCapabilities()); + EventLogger.registerTransmitter(EventTransmitter({}, defaultHook: (name, parameters) { + recordEvents(name, parameters); + final fbEvent = facebookEventMapping[name]; + if (fbEvent == null) { + return; + } + Log.d("transmit EVENT [$name] => [$fbEvent]"); + EventLogger.facebookLogEvent(name: fbEvent); + })); + EventLogger.setGuruPriorityGetter((name, parameters) => + GuruApp.instance.conversionEvents.contains(name) + ? EventPriority.EMERGENCE + : EventPriority.DEFAULT); + String xDeviceInfo = ''; + try { + final deviceId = await AppProperty.getInstance().getDeviceId(); + final deviceInfo = await DeviceUtils.buildDeviceInfo(deviceId: deviceId); + xDeviceInfo = deviceInfo.toXDeviceInfo(); + } catch (error, stacktrace) { + Log.e("init deviceInfo error: $error, $stacktrace"); + } + + await GuruAnalyticsStrategy.instance.load(); + EventLogger.initialize( + appId: GuruApp.instance.appSpec.details.saasAppId, + deviceInfo: xDeviceInfo, + delayedInSeconds: analyticsConfig.delayedInSeconds, + eventExpiredInDays: analyticsConfig.expiredInDays, + callback: processAnalyticsCallback, + debug: true, + ); + _initEnvProperties(); + _logLocale(); + _logDeviceType(); + _logFirstOpen(); + Future.delayed(const Duration(seconds: 1), () { + initAdjust(); + initFbEventMapping(); + Log.d("register transmitter"); + }); + initialized = true; + // if (Platform.isAndroid) { + // _logPeerApps(); + // } + } + } + + void processAnalyticsCallback(int code, String? errorInfo) { + if (!errorEventCodes.contains(code)) { + return; + } + final parameters = { + "item_category": "error_event", + "item_name": code.toString(), + "country": AccountDataStore.instance.countryCode, + "network": AdsManager.instance.connectivityStatus.toString(), + }; + + if (errorInfo != null) { + parameters["err"] = errorInfo.length > 32 ? errorInfo.substring(0, 32) : errorInfo; + } + logFirebaseEvent("dev_audit", parameters); + // Guru Analytics Event(GAE) + Log.d("[GAE]($code)=>$errorInfo $parameters", tag: "Analytics"); + } + + void updateUserIdentification( + {String? firebaseAppInstanceId, String? idfa, String? adId, String? gpsAdId}) { + final latestUserIdentification = userIdentificationSubject.value; + bool changed = false; + String? changedFirebaseInstanceId = latestUserIdentification.firebaseAppInstanceId; + String? changedIdfa = latestUserIdentification.idfa; + String? changedAdId = latestUserIdentification.adId; + String? changedGpsAdId = latestUserIdentification.gpsAdId; + + if (firebaseAppInstanceId != null && + latestUserIdentification.firebaseAppInstanceId != firebaseAppInstanceId) { + changedFirebaseInstanceId = firebaseAppInstanceId; + changed = true; + } + if (idfa != null && latestUserIdentification.idfa != idfa) { + changedIdfa = idfa; + changed = true; + } + if (adId != null && latestUserIdentification.adId != adId) { + changedAdId = adId; + changed = true; + } + if (gpsAdId != null && latestUserIdentification.gpsAdId != gpsAdId) { + changedGpsAdId = gpsAdId; + changed = true; + } + if (changed) { + final newUserIdentification = UserIdentification( + firebaseAppInstanceId: changedFirebaseInstanceId ?? '', + idfa: changedIdfa, + adId: changedAdId, + gpsAdId: changedGpsAdId); + userIdentificationSubject.add(newUserIdentification); + Log.d("updateUserIdentification: $newUserIdentification"); + } + } + + void parseFbEventMapping() { + final fbEventMappingString = + RemoteConfigManager.instance.getString(RemoteConfigReservedConstants.fbEventMapping); + Log.d("parseFbEventMapping first: $fbEventMappingString"); + if (fbEventMappingString == null) { + return; + } + Map result = {}; + final eventEntries = fbEventMappingString.split(";"); + for (String eventEntryString in eventEntries) { + final eventEntry = eventEntryString.split(":"); + if (eventEntry.length == 2) { + result[eventEntry.first] = eventEntry.last; + } + } + facebookEventMapping.clear(); + facebookEventMapping.addAll(result); + Log.d("parseFbEventMapping: $result"); + } + + void initFbEventMapping() { + RemoteConfigManager.instance.observeConfig().listen((config) { + parseFbEventMapping(); + }); + parseFbEventMapping(); + } + + @override + Future getAppInstanceId() async { + if (appInstanceId.isNotEmpty != true) { + appInstanceId = await EventLogger.getAppInstanceId(); + RuntimeProperty.instance.setString(PropertyKeys.appInstanceId, appInstanceId); + } + return appInstanceId; + } + + void _initEnvProperties() async { + final bundle = await AppProperty.getInstance().loadValuesByTag(PropertyTags.analytics); + + final userId = bundle.getString(PropertyKeys.analyticsUserId); + if (userId != null) { + setUserId(userId); + } + + final adjustId = bundle.getString(PropertyKeys.analyticsAdjustId); + if (adjustId != null) { + setAdjustId(adjustId); + } + + final adId = bundle.getString(PropertyKeys.analyticsAdId); + if (adId != null) { + setAdId(adId); + } + refreshEventStatistic(); + + String? firebaseId = await getAppInstanceId(); + if (firebaseId.isEmpty) { + firebaseId = bundle.getString(PropertyKeys.analyticsFirebaseId); + } + if (firebaseId?.isNotEmpty == true) { + setFirebaseId(firebaseId!); + } + + final abProperties = RemoteConfigManager.instance.getABProperties(); + + final PropertyBundle propertyBundle = PropertyBundle(); + if (abProperties.isNotEmpty) { + for (var entry in abProperties.entries) { + setGuruUserProperty(entry.key, entry.value); + propertyBundle.setString(PropertyKeys.buildABTestProperty(entry.key), entry.value); + Log.d("setGuruUserProperty: ${entry.key} = ${entry.value}"); + } + } + AppProperty.getInstance().setProperties(propertyBundle); + } + + void _logFirstOpen() async { + int firstInstallTime = + RuntimeProperty.instance.getInt(PropertyKeys.firstInstallTime, defValue: -1); + if (firstInstallTime == -1) { + firstInstallTime = await AppProperty.getInstance() + .getOrCreateInt(PropertyKeys.firstInstallTime, DateTimeUtils.currentTimeInMillis()); + } + setUserProperty("first_open_time", firstInstallTime.toString()); + } + + void _logLocale() { + if (Platform.localeName.isNotEmpty == true) { + String lanCode = ""; + String countryCode = ""; + final currentLocale = Platform.localeName.split('_'); + if (currentLocale.isNotEmpty) { + setUserProperty("lang_code", currentLocale[0].toLowerCase()); + lanCode = currentLocale[0].toLowerCase(); + } + + if (currentLocale.length > 1) { + setUserProperty("country_code", currentLocale.last.toLowerCase()); + countryCode = currentLocale.last.toLowerCase(); + } + Log.d("## locale: [$currentLocale]"); + + if (lanCode.isNotEmpty && countryCode.isNotEmpty) { + // CountryCodes.init(Locale(lanCode, countryCode)); + } else { + // CountryCodes.init(); + } + } else { + // CountryCodes.init(); + } + } + + void _logDeviceType() async { + setUserProperty("device_type", DeviceUtils.isTablet() ? "tablet" : "phone"); + + final deviceId = await AppProperty.getInstance().getDeviceId(); + setDeviceId(deviceId); + } + + @override + Future setUserProperty(String key, String value) async { + recordEvents("setUserProperty", {key: value}); + recordProperty(key, value); + if (release) { + await EventLogger.setUserProperty(key, value); + } + } + + void setDeviceId(String deviceId) { + Log.d("setDeviceId: $deviceId"); + recordEvents("setDeviceId", {"userId": deviceId}); + recordProperty("deviceId", deviceId); + if (deviceId.isNotEmpty) { + AppProperty.getInstance().setAnalyticsDeviceId(deviceId); + if (release) { + EventLogger.setUserProperty("device_id", deviceId); + EventLogger.setDeviceId(deviceId); + } + } + } + + void setUserId(String userId) { + Log.d("setUserId: $userId"); + recordEvents("setUserId", {"userId": userId}); + recordProperty("userId", userId); + if (userId.isNotEmpty) { + AppProperty.getInstance().setUserId(userId); + if (release) { + EventLogger.setUserId(userId); + FirebaseCrashlytics.instance.setUserIdentifier(userId); + } + } + } + + void setAdjustId(String adjustId) { + Log.d("setAdjustId: $adjustId"); + recordEvents("setAdjustId", {"adjustId": adjustId}); + recordProperty("adjustId", adjustId); + if (adjustId.isNotEmpty) { + AppProperty.getInstance().setAdjustId(adjustId); + updateUserIdentification(adId: adjustId); + if (release) { + EventLogger.setAdjustId(adjustId); + } + } + } + + void setFirebaseId(String firebaseId) { + Log.d("setFirebaseId: $firebaseId"); + recordEvents("setFirebaseId", {"firebaseId": firebaseId}); + recordProperty("firebaseId", firebaseId); + if (firebaseId.isNotEmpty) { + AppProperty.getInstance().setFirebaseId(firebaseId); + updateUserIdentification(firebaseAppInstanceId: firebaseId); + if (release) { + EventLogger.setFirebaseId(firebaseId); + } + } + } + + void setAdId(String adId) { + Log.d("setAdId: $adId"); + recordEvents("setAdId", {"adId": adId}); + recordProperty("adId", adId); + AppProperty.getInstance().setAdId(adId); + updateUserIdentification(gpsAdId: adId); + if (release) { + EventLogger.setAdId(adId); + } + } + + void setIdfa(String idfa) { + Log.d("setIdfa: $idfa"); + recordEvents("setIdfa", {"idfa": idfa}); + recordProperty("idfa", idfa); + AppProperty.getInstance().setIdfa(idfa); + updateUserIdentification(idfa: idfa); + if (release) { + // 自打点中。idfa变是adId + EventLogger.setAdId(idfa); + } + } + + void logScreen(String screenName) { + recordEvents("logScreen", {"name": screenName}); + recordProperty("screen", screenName); + if (release) { + FirebaseCrashlytics.instance.log(screenName); + EventLogger.logScreen(screenName); + } + } + + @override + void setScreen(String screenName) { + if (currentScreen != screenName) { + currentScreen = screenName; + logScreen(screenName); + } + } + + @override + void logFirebase(String msg) async { + if (release) { + try { + FirebaseCrashlytics.instance.log(msg); + if (EventLogger.dumpLog) { + Log.d("[Firebase]: $msg"); + } + } catch (error, stacktrace) {} + } else { + Log.d("[Firebase]: $msg"); + } + } + + AppEventOptions? getOptions(String eventName) { + return GuruAnalyticsStrategy.instance.getStrategyRule(eventName)?.getAppEventOptions(); + } + + @override + void logEvent(String eventName, Map parameters, {AppEventOptions? options}) { + refreshEventStatistic(); + // Firebase Facebook log event + if (release) { + EventLogger.logEvent(eventName, parameters, options: options ?? getOptions(eventName)); + _logAdjustEvent(eventName, parameters); + } else { + Log.d("logEvent: $eventName $parameters"); + EventLogger.transmit(eventName, parameters); + } + } + + @override + void logEventEx(String eventName, + {String? itemCategory, + String? itemName, + double? value, + Map parameters = const {}, + AppEventOptions? options}) async { + Map map = Map.from(parameters); + if (itemCategory != null) { + map["item_category"] = itemCategory; + } + + if (itemName != null) { + map["item_name"] = itemName; + } + + if (value != null) { + map["value"] = value; + } + + logEvent(eventName, map, options: options); + } + + Future refreshEventStatistic({bool force = false}) async { + if (!GuruApp.instance.appSpec.deployment.enableAnalyticsStatistic) { + return; + } + final now = DateTimeUtils.currentTimeInMillis(); + if (force || (now - latestFetchStatisticTs > DateTimeUtils.minuteInMillis * 2)) { + EventLogger.getStatistic().then((statistic) { + Log.d("Event Statistic:$statistic"); + if (statistic != GuruStatistic.invalid && guruEventStatistic.addIfChanged(statistic)) { + setUserProperty("lgd", statistic.logged.toString()); + setUserProperty("uld", statistic.uploaded.toString()); + } + }); + latestFetchStatisticTs = now; + } + } + + Future zipGuruLogs() { + return EventLogger.zipGuruLogs(); + } + + Map filterOutNulls(Map parameters) { + final Map filtered = {}; + parameters.forEach((String key, dynamic value) { + if (value != null) { + filtered[key] = value; + } + }); + return filtered; + } + + @override + void logException(dynamic exception, {StackTrace? stacktrace}) async { + if (release) { + Log.d("exception! $exception"); + FirebaseCrashlytics.instance.log(exception.toString()); + FirebaseCrashlytics.instance + .recordError(exception, stacktrace, printDetails: EventLogger.dumpLog); + } else { + Log.w("Occur Error! $exception $stacktrace", stackTrace: stacktrace); + } + } + + void logPurchase(double amount, + {String currency = "", + String contentId = "", + String adPlatform = "", + Map parameters = const {}}) { + EventLogger.logFbPurchase(amount, + currency: currency, + contentId: contentId, + adPlatform: adPlatform, + additionParameters: parameters); + } + + void logEventShare({String? itemCategory, String? itemName}) { + logEvent("share", { + "item_category": itemCategory, + "item_name": itemName, + "content_type": itemCategory, + "item_id": itemName + }); + } + + void logSpendCredits(String contentId, String contentType, int price, + {required String virtualCurrencyName, required int balance, String scene = ''}) { + if (release) { + EventLogger.logSpendCredits(contentId, contentType, price, + virtualCurrencyName: virtualCurrencyName, balance: balance, scene: scene); + } else { + final parameters = { + "item_name": contentId, + "item_category": contentType, + "virtual_currency_name": virtualCurrencyName, + "value": price, + "balance": balance, + "scene": scene + }; + Log.d("logEvent: spend_virtual_currency $parameters"); + EventLogger.transmit("spend_virtual_currency", parameters); + } + AiBi.instance.spendVirtualCurrency(balance, price.toDouble(), contentType); + } + + Future logEarnVirtualCurrency({ + required String virtualCurrencyName, + required String method, + required int balance, + required int value, + }) async { + logEvent("earn_virtual_currency", { + "virtual_currency_name": virtualCurrencyName, + "item_category": method, + "value": value, + "balance": balance + }); + AiBi.instance.earnVirtualCurrency(balance, value.toDouble(), method); + } + + Future setGuruUserProperty(String key, String value) async { + return await EventLogger.setGuruUserProperty(key, value); + } + + Future logGuruEvent(String eventName, Map parameters) async { + EventLogger.guruLogEvent(name: eventName, parameters: parameters); + } + + Future logFirebaseEvent(String eventName, Map parameters) async { + if (release) { + EventLogger.firebaseLogEvent(name: eventName, parameters: parameters); + } else { + Log.d("logEvent: $eventName $parameters"); + } + EventLogger.transmit(eventName, parameters); + } +} diff --git a/guru_app/lib/analytics/modules/adjust_aware.dart b/guru_app/lib/analytics/modules/adjust_aware.dart new file mode 100644 index 0000000..c42adea --- /dev/null +++ b/guru_app/lib/analytics/modules/adjust_aware.dart @@ -0,0 +1,195 @@ +part of '../guru_analytics.dart'; + +/// Created by Haoyi on 2022/3/12 +typedef AdjustEventConverter = AdjustEvent Function(Map); + +class AdjustProfile { + final String appToken; + final Map eventNameMapping; + + final bool isEnabled; + + AdjustProfile({required this.appToken, required this.eventNameMapping}) + : isEnabled = appToken.isNotEmpty; + + static AdjustEvent createAdjustEvent(String eventToken, Map params) { + final adjustParams = Map.of(params); + final revenue = adjustParams.remove("revenue"); + final currency = adjustParams.remove("currency"); + final event = AdjustEvent(eventToken); + if (revenue is num && currency is String) { + event.setRevenue(revenue, currency); + } + for (var entry in adjustParams.entries) { + event.addCallbackParameter(entry.key, entry.value.toString()); + } + return event; + } +} + +mixin AdjustAware on Analytics { + static final List pendingAdjustEvents = []; + + // + static bool initializedAdjust = false; + + bool get enabledAdjust => GuruApp.instance.adjustProfile.isEnabled; + + static AdjustConfig _defaultAdjustConfigComposition(AdjustConfig adjustConfig) { + return adjustConfig; + } + + static AdjustConfig Function(AdjustConfig) adjustConfigComposition = + _defaultAdjustConfigComposition; + + static AdjustConfig buildAdjustConfig() { + final AdjustConfig config = AdjustConfig(GuruApp.instance.adjustProfile.appToken, + kReleaseMode ? AdjustEnvironment.production : AdjustEnvironment.sandbox); + config.fbAppId = GuruApp.instance.details.facebookAppId; + return adjustConfigComposition(config); + } + + Future initAdjust() async { + if (enabledAdjust) { + await _setupAdjustSessionCall(); + final adjustConfig = buildAdjustConfig(); + Adjust.start(adjustConfig); + initializedAdjust = true; + _trackAllPendingAdjustEvent(); + + final adId = await Adjust.getAdid(); + if (adId != null) { + GuruAnalytics.instance.setAdjustId(adId); + Log.d("initAdjust adId:$adId"); + } else { + // https://github.com/adjust/react_native_sdk/issues/90 + Log.d("adjustId is null! waiting 3s..and retry"); + Future.delayed(const Duration(seconds: 3), () async { + final adId = await Adjust.getAdid(); + if (adId != null) { + GuruAnalytics.instance.setAdjustId(adId); + Log.d("initAdjust adId:$adId"); + } else { + Log.d("initAdjust adId is null"); + } + }); + } + + final googleAdId = await Adjust.getGoogleAdId(); + if (googleAdId != null) { + GuruAnalytics.instance.setAdId(googleAdId); + Log.d("initAdjust googleAdId:$googleAdId"); + } + + final idfa = Platform.isIOS ? await Adjust.getIdfa() : null; + if (idfa != null) { + GuruAnalytics.instance.setIdfa(idfa); + Log.d("initAdjust idfa:$idfa"); + } + } + } + + // 接入Adjust后trackAdRevenueNew要放开 + void loadAdjustAdRevenue(ImpressionData impressionData) { + if (enabledAdjust) { + AdjustAdRevenue adRevenue = AdjustAdRevenue(AdjustConfig.AdRevenueSourceAppLovinMAX); + adRevenue.setRevenue(impressionData.publisherRevenue, "USD"); + adRevenue.adRevenueNetwork = impressionData.networkName; + adRevenue.adRevenueUnit = impressionData.unitId; + adRevenue.adRevenuePlacement = impressionData.networkPlacementId; + Adjust.trackAdRevenueNew(adRevenue); + recordEvents("[Adjust]trackAdRevenue", adRevenue.toMap); + } + } + +// + Future _setupAdjustSessionCall() async { + try { + final deviceId = await AppProperty.getInstance().getDeviceId(); + Adjust.addSessionCallbackParameter("device_id", deviceId); + } catch (error, stacktrace) { + Log.e("setupAdjustSessionCall error $error, $stacktrace"); + } + + final appInstanceId = await getAppInstanceId(); + Log.d("setupAdjustSessionCall $appInstanceId"); + Adjust.addSessionCallbackParameter("user_pseudo_id", appInstanceId); + } + + void logAdjust(String eventName, + {String? itemCategory, + String? itemName, + double? value, + Map parameters = const {}}) { + if (enabledAdjust) { + Map map = Map.from(parameters); + if (itemCategory != null) { + map["item_category"] = itemCategory; + } + if (itemName != null) { + map["item_name"] = itemName; + } + if (value != null) { + map["value"] = value; + } + _logAdjustEvent(eventName, map); + } + } + + void _trackAdjustEvent(AdjustEvent adjustEvent) { + if (!enabledAdjust) { + return; + } + if (!initializedAdjust) { + pendingAdjustEvents.add(adjustEvent); + Log.d("adjust not initialized!"); + return; + } + if (EventLogger.dumpLog || kDebugMode) { + Log.d("[adjust] ${adjustEvent.toMap}"); + } + if (pendingAdjustEvents.isNotEmpty) { + final events = List.of(pendingAdjustEvents); + pendingAdjustEvents.clear(); + for (var event in events) { + Adjust.trackEvent(event); + if (EventLogger.dumpLog || kDebugMode) { + Log.d("[adjust] ${event.toMap}"); + } + } + } + Adjust.trackEvent(adjustEvent); + } + + void _trackAllPendingAdjustEvent() { + if (!enabledAdjust) { + return; + } + final events = List.of(pendingAdjustEvents); + pendingAdjustEvents.clear(); + for (var event in events) { + Adjust.trackEvent(event); + if (EventLogger.dumpLog || kDebugMode) { + Log.d("[adjust] ${event.toMap}"); + } + } + } + + AdjustEventConverter? getAdjustEventConverter(String eventName) { + return GuruAnalyticsStrategy.instance.getAdjustEventConverter(eventName) ?? + GuruApp.instance.adjustProfile.eventNameMapping[eventName]; + } + +// + void _logAdjustEvent(String eventName, Map parameters) { + if (!enabledAdjust) { + return; + } + final AdjustEventConverter? adjustEventConverter = getAdjustEventConverter(eventName); + if (adjustEventConverter != null) { + AdjustEvent adjustEvent = adjustEventConverter(parameters); + Log.d("adjustEvent:${adjustEvent.toMap}"); + _trackAdjustEvent(adjustEvent); + } + } +} diff --git a/guru_app/lib/analytics/modules/ads_analytics.dart b/guru_app/lib/analytics/modules/ads_analytics.dart new file mode 100644 index 0000000..81ae8c0 --- /dev/null +++ b/guru_app/lib/analytics/modules/ads_analytics.dart @@ -0,0 +1,61 @@ +// /// Created by Haoyi on 2022/2/28 +// +// part of "../analytics.dart"; +// +// + +part of "../guru_analytics.dart"; + +extension AdsAnalytics on GuruAnalytics { + void logAdRevenue(double adRevenue, String adPlatform, String currency) { + // logEventEx(name, itemCategory: scene, itemName: adName); + if (release) { + EventLogger.logAdRevenue(adRevenue, adPlatform, currency); + } else { + Log.d("[firebase] logAdRevenue ${{ + "adRevenue": adRevenue, + "adPlatform": adPlatform, + "currency": currency + }}"); + } + } + + void logAdLtv(String phase, double ltv) { + if (release) { + EventLogger.logAdLtv(phase, ltv); + } else { + Log.d("[firebase] logAdLtv ${{"phase": phase, "ltv": ltv}}"); + } + } + + void logAdImpression(String name, String adType, + {String scene = "", String adName = "", Map parameters = const {}}) { + logEventEx(name, itemCategory: scene, itemName: adName, parameters: parameters); + if (release) { + EventLogger.logFbAdImpression(adType); + FirebaseCrashlytics.instance + .log("adImp: name($name) scene($scene) adName($adName) adType($adType)"); + } else { + Log.d("[facebook] logEvent logFbAdImpression: $adType"); + } + } + + void logAdImp(ImpressionData data) { + EventLogger.logAdImpression( + adPlatform: data.platform, + adSource: data.networkName, + adFormat: data.unitFormat, + adUnitName: data.unitName, + value: data.publisherRevenue, + currency: data.currency); + } + + void logAdClick(String name, String adType, {String scene = "", String adName = ""}) { + logEventEx(name, itemCategory: scene, itemName: adName); + if (release) { + EventLogger.logFbAdClick(adType); + } else { + Log.d("[facebook] logEvent logAdClick: $adType"); + } + } +} diff --git a/guru_app/lib/analytics/strategy/guru_analytics_strategy.dart b/guru_app/lib/analytics/strategy/guru_analytics_strategy.dart new file mode 100644 index 0000000..8b70826 --- /dev/null +++ b/guru_app/lib/analytics/strategy/guru_analytics_strategy.dart @@ -0,0 +1,590 @@ +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 included; + final Set 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> 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 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 = {}; + final excluded = {}; + 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 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 priorityRules = []; + final SplayTreeMap explicitRules = SplayTreeMap(); + final Map iosAdjustEventConverters = {}; + final Map androidAdjustEventConverts = {}; + + bool loaded = false; + + final LinkedLruHashMap eventRules = LinkedLruHashMap(maximumSize: 128); + + GuruAnalyticsStrategy._(); + + static final GuruAnalyticsStrategy instance = GuruAnalyticsStrategy._(); + + void reset() { + priorityRules.clear(); + explicitRules.clear(); + } + + static const guruAnalyticsStrategyExtension = ".gas"; // Guru Analytics Strategy + + Future 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 strategyTextStream = file!.openRead().transform(utf8.decoder); + + StrategyRule? newDefaultRule; + final List newPriorityRules = []; + final Map 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); + } + } +} diff --git a/guru_app/lib/api/custom_transformer.dart b/guru_app/lib/api/custom_transformer.dart new file mode 100644 index 0000000..04fe4e1 --- /dev/null +++ b/guru_app/lib/api/custom_transformer.dart @@ -0,0 +1,46 @@ +import 'dart:convert'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:http_parser/http_parser.dart'; + +/// Created by Haoyi on 2021-06-3. +/// +class CustomTransformer extends DefaultTransformer { + CustomTransformer() : super(jsonDecodeCallback: _parseJson); + + @override + Future transformRequest(RequestOptions options) async { + var data = options.data ?? ''; + if (data is! String) { + if (_isJsonMime(options.contentType)) { + return await _encodeToJson(options.data); + } else if (data is Map) { + return Transformer.urlEncodeMap(data); + } + } + return data.toString(); + } + + bool _isJsonMime(String? contentType) { + if (contentType == null) return false; + return MediaType.parse(contentType).mimeType.toLowerCase() == Headers.jsonMimeType.mimeType; + } + + Future _encodeToJson(dynamic data) async { + return await compute(jsonEncode, data); + } +} + +// Must be top-level function +_parseAndDecode(String response) { + return jsonDecode(response); +} + +_parseJson(String text) { + return compute(_parseAndDecode, text); +} + +platformLogPrint(Object object) { + Log.v("[NETWORK] " + object.toString()); +} diff --git a/guru_app/lib/api/data/orders/orders_model.dart b/guru_app/lib/api/data/orders/orders_model.dart new file mode 100644 index 0000000..5ea9603 --- /dev/null +++ b/guru_app/lib/api/data/orders/orders_model.dart @@ -0,0 +1,138 @@ +import 'dart:io'; + +import 'package:guru_app/analytics/data/analytics_model.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'orders_model.g.dart'; + +/// Created by Haoyi on 2022/7/27 + +@JsonSerializable() +class OrderUserInfo { + @JsonKey(name: 'level', defaultValue: "0") + final String level; + + OrderUserInfo(this.level); + + factory OrderUserInfo.fromJson(Map json) => + _$OrderUserInfoFromJson(json); + + Map toJson() => _$OrderUserInfoToJson(this); +} + +class OrderType { + static const inapp = 0; + static const subs = 1; +} + +@JsonSerializable() +class OrdersReport { + // android + @JsonKey(name: 'orderType', defaultValue: 0) + int? orderType; + @JsonKey(name: 'packageName') + String? packageName; + @JsonKey(name: 'productId') + String? productId; + @JsonKey(name: 'subscriptionId') + String? subscriptionId; + @JsonKey(name: 'token') + String? token; + @JsonKey(name: 'offerId') + String? offerId; + + @JsonKey(name: 'basePlanId') + String? basePlanId; + + // ios + @JsonKey(name: 'bundleId') + String? bundleId; + @JsonKey(name: 'receipt') + String? receipt; + @JsonKey(name: 'sku') + String? sku; + @JsonKey(name: 'country') + String? countryCode; + + // general + @JsonKey(name: 'price') + String? price; + @JsonKey(name: 'currency') + String? currency; + + @JsonKey(name: 'userInfo') + OrderUserInfo? orderUserInfo; + + @JsonKey(name: "eventConfig") + UserIdentification? userIdentification; + + OrdersReport( + {this.orderType, + this.token, + this.packageName, + this.productId, + this.subscriptionId, + this.bundleId, + this.receipt, + this.price, + this.currency, + this.sku, + this.countryCode, + this.orderUserInfo, + this.userIdentification, + this.offerId, + this.basePlanId}); + + @override + String toString() { + final StringBuffer sb = StringBuffer(); + sb.writeln("[OrdersReport]"); + sb.writeln(" productId: $productId"); + sb.writeln(" price: $price"); + sb.writeln(" currency: $currency"); + sb.writeln(" userIdentification: $userIdentification"); + if (Platform.isAndroid) { + sb.writeln(" orderType: $orderType"); + sb.writeln(" packageName: $packageName"); + sb.writeln(" subscriptionId: $subscriptionId"); + sb.writeln(" token: $token"); + sb.writeln(" offerId: $offerId"); + sb.writeln(" basePlanId: $basePlanId"); + } else if (Platform.isIOS) { + sb.writeln(" bundleId: $bundleId"); + sb.writeln(" receipt: $receipt"); + sb.writeln(" sku: $sku"); + sb.writeln(" countryCode: $countryCode"); + } + return sb + .toString(); //'OrdersReport{orderType: $orderType, packageName: $packageName, productId: $productId, subscriptionId: $subscriptionId, token: $token, bundleId: $bundleId, receipt: $receipt, price: $price, currency: $currency, introductoryPrice: $introductoryPrice}'; + } + + factory OrdersReport.fromJson(Map json) => + _$OrdersReportFromJson(json); + + Map toJson() => _$OrdersReportToJson(this); +} + +@JsonSerializable() +class OrdersResponse { + @JsonKey(name: 'usdPrice', defaultValue: 0.0) + final double usdPrice; + + @JsonKey(name: 'test', defaultValue: false) + final bool test; + + bool get isTestOrder => test; + + OrdersResponse(this.usdPrice, this.test); + + factory OrdersResponse.fromJson(Map json) => + _$OrdersResponseFromJson(json); + + Map toJson() => _$OrdersResponseToJson(this); + + @override + String toString() { + return 'OrdersResponse{usdPrice:$usdPrice, test:$test}'; + } +} diff --git a/guru_app/lib/api/data/orders/orders_model.g.dart b/guru_app/lib/api/data/orders/orders_model.g.dart new file mode 100644 index 0000000..2f08f47 --- /dev/null +++ b/guru_app/lib/api/data/orders/orders_model.g.dart @@ -0,0 +1,71 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'orders_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +OrderUserInfo _$OrderUserInfoFromJson(Map json) => + OrderUserInfo( + json['level'] as String? ?? '0', + ); + +Map _$OrderUserInfoToJson(OrderUserInfo instance) => + { + 'level': instance.level, + }; + +OrdersReport _$OrdersReportFromJson(Map json) => OrdersReport( + orderType: json['orderType'] as int? ?? 0, + token: json['token'] as String?, + packageName: json['packageName'] as String?, + productId: json['productId'] as String?, + subscriptionId: json['subscriptionId'] as String?, + bundleId: json['bundleId'] as String?, + receipt: json['receipt'] as String?, + price: json['price'] as String?, + currency: json['currency'] as String?, + sku: json['sku'] as String?, + countryCode: json['country'] as String?, + orderUserInfo: json['userInfo'] == null + ? null + : OrderUserInfo.fromJson(json['userInfo'] as Map), + userIdentification: json['eventConfig'] == null + ? null + : UserIdentification.fromJson( + json['eventConfig'] as Map), + offerId: json['offerId'] as String?, + basePlanId: json['basePlanId'] as String?, + ); + +Map _$OrdersReportToJson(OrdersReport instance) => + { + 'orderType': instance.orderType, + 'packageName': instance.packageName, + 'productId': instance.productId, + 'subscriptionId': instance.subscriptionId, + 'token': instance.token, + 'offerId': instance.offerId, + 'basePlanId': instance.basePlanId, + 'bundleId': instance.bundleId, + 'receipt': instance.receipt, + 'sku': instance.sku, + 'country': instance.countryCode, + 'price': instance.price, + 'currency': instance.currency, + 'userInfo': instance.orderUserInfo, + 'eventConfig': instance.userIdentification, + }; + +OrdersResponse _$OrdersResponseFromJson(Map json) => + OrdersResponse( + (json['usdPrice'] as num?)?.toDouble() ?? 0.0, + json['test'] as bool? ?? false, + ); + +Map _$OrdersResponseToJson(OrdersResponse instance) => + { + 'usdPrice': instance.usdPrice, + 'test': instance.test, + }; diff --git a/guru_app/lib/api/guru_api.dart b/guru_app/lib/api/guru_api.dart new file mode 100644 index 0000000..a1a93fe --- /dev/null +++ b/guru_app/lib/api/guru_api.dart @@ -0,0 +1,158 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:guru_app/account/account_data_store.dart'; +import 'package:guru_app/account/model/user.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:guru_app/property/app_property.dart'; +import 'package:guru_utils/device/device_info.dart'; +import 'package:guru_utils/device/device_utils.dart'; +import 'package:retrofit/retrofit.dart'; +import 'custom_transformer.dart'; +import 'data/orders/orders_model.dart'; + +/// Created by Haoyi on 6/3/21 +part 'modules/guru_api_extension.dart'; + +part 'guru_api.g.dart'; + +abstract class DioBuilder { + Dio build(); +} + +class GuruDioBuilder extends DioBuilder { + final AccountDataStore accountDataStore; + + GuruDioBuilder() : accountDataStore = AccountDataStore.instance; + + Options toOptions(RequestOptions ro) { + return Options( + method: ro.method, + sendTimeout: ro.sendTimeout, + receiveTimeout: ro.receiveTimeout, + extra: ro.extra, + headers: ro.headers, + responseType: ro.responseType, + contentType: ro.contentType, + validateStatus: ro.validateStatus, + receiveDataWhenStatusError: ro.receiveDataWhenStatusError, + followRedirects: ro.followRedirects, + maxRedirects: ro.maxRedirects, + requestEncoder: ro.requestEncoder, + responseDecoder: ro.responseDecoder, + listFormat: ro.listFormat); + } + + @override + Dio build() { + Dio dio = Dio() + ..transformer = CustomTransformer() + ..options.connectTimeout = Duration(milliseconds: GuruApp.instance.appSpec.deployment.apiConnectTimeout) + ..options.receiveTimeout = Duration(milliseconds: GuruApp.instance.appSpec.deployment.apiReceiveTimeout); + + dio.interceptors.add(InterceptorsWrapper( + onRequest: (RequestOptions options, RequestInterceptorHandler handler) async { + DeviceInfo? deviceInfo = accountDataStore.currentDevice; + final deviceId = await AppProperty.getInstance().getDeviceId(); + deviceInfo ??= + await DeviceUtils.buildDeviceInfo(deviceId: deviceId, firebasePushToken: "", uid: ""); + final token = accountDataStore.saasToken; + options.headers + .addAll({"X-APP-ID": GuruApp.instance.details.saasAppId, "X-ACCESS-TOKEN": token ?? ''}); + options.headers.addAll({"X-DEVICE-INFO": Uri.encodeFull(deviceInfo.toXDeviceInfo())}); + handler.next(options); + }, onResponse: (Response response, ResponseInterceptorHandler handler) { + // Log.v("### onResponse ${response.data}"); + response.data = response.data["data"] ?? response.data; + handler.next(response); + }, onError: (DioError err, ErrorInterceptorHandler handler) async { + final token = accountDataStore.saasToken; + final response = err.response; + Log.v("### onError ${err.toString()}"); + if (response != null && token != null && response.statusCode == 401) { + // dio.lock(); + try { + Log.v("accountDataStore.refreshAuth()"); + await accountDataStore.refreshAuth(); //获取新token + } catch (e) { + // Log.v("[NETWORK]: RefreshToken Failed."); + handler.reject(err); + } finally { + // dio.unlock(); + } + final options = err.requestOptions.copyWith(); + options.headers["X-ACCESS-TOKEN"] = accountDataStore.saasToken; + try { + final response = await dio.request(options.path, + data: options.data, + queryParameters: options.queryParameters, + cancelToken: options.cancelToken, + onReceiveProgress: options.onReceiveProgress, + options: toOptions(options)); + handler.resolve(response); + } catch (error, stacktrace) { + Log.v("re-request error:$error $stacktrace"); + handler.reject(err); + } + } else { + handler.reject(err); + } + })); + dio.interceptors + .add(LogInterceptor(requestBody: true, responseBody: true, logPrint: platformLogPrint)); + return dio; + } +} + +@RestApi() +abstract class GuruApiMethods { + factory GuruApiMethods(Dio dio, {String baseUrl}) = _GuruApiMethods; + + static GuruApiMethods create(GuruDioBuilder dioBuilder, String baseUrl) { + return GuruApiMethods(dioBuilder.build(), baseUrl: baseUrl); + } + + // 上报 Device, 初次或者 token 更新后 + @POST("/device/api/v1/devices") + Future reportDevice(@Body() DeviceInfo body); + + // Auth + @POST("/auth/api/v1/tokens/provider/secret") + Future signInWithAnonymous(@Body() AnonymousLoginReqBody body); + + @POST("/auth/api/v1/renewals/token") + Future refreshSaasToken(); + + @POST("/auth/api/v1/renewals/firebase") + Future renewFirebaseToken(); + + @POST("/order/api/v1/orders/ios") + Future iOSOrdersReport(@Body() OrdersReport body); + + @POST("/order/api/v1/orders/android") + Future androidOrdersReport(@Body() OrdersReport body); +} + +class GuruApi { + static const String _saasApiDevHost = "https://dev.saas.castbox.fm"; + static const String _saasApiReleaseHost = "https://saas.castbox.fm"; + + static bool useReleaseApi = kReleaseMode; + + static final GuruApi _releaseApi = + GuruApi._(GuruApiMethods.create(GuruDioBuilder(), _saasApiReleaseHost)); + static final GuruApi _debugApi = + GuruApi._(GuruApiMethods.create(GuruDioBuilder(), _saasApiDevHost)); + + final GuruApiMethods _methods; + + static GuruApi get instance => useReleaseApi ? _releaseApi : _debugApi; + + static String get saasApiHost => useReleaseApi ? _saasApiReleaseHost : _saasApiDevHost; + + GuruApiMethods get methods => _methods; + + GuruApi._(this._methods); +} diff --git a/guru_app/lib/api/guru_api.g.dart b/guru_app/lib/api/guru_api.g.dart new file mode 100644 index 0000000..dc68f39 --- /dev/null +++ b/guru_app/lib/api/guru_api.g.dart @@ -0,0 +1,215 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'guru_api.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers + +class _GuruApiMethods implements GuruApiMethods { + _GuruApiMethods( + this._dio, { + this.baseUrl, + }); + + final Dio _dio; + + String? baseUrl; + + @override + Future reportDevice(DeviceInfo body) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(body.toJson()); + final _result = await _dio.fetch(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/device/api/v1/devices', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = _result.data; + return value; + } + + @override + Future signInWithAnonymous(AnonymousLoginReqBody body) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(body.toJson()); + final _result = + await _dio.fetch>(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/auth/api/v1/tokens/provider/secret', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = SaasUser.fromJson(_result.data!); + return value; + } + + @override + Future refreshSaasToken() async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final Map? _data = null; + final _result = + await _dio.fetch>(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/auth/api/v1/renewals/token', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = SaasUser.fromJson(_result.data!); + return value; + } + + @override + Future renewFirebaseToken() async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final Map? _data = null; + final _result = await _dio + .fetch>(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/auth/api/v1/renewals/firebase', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = FirebaseTokenData.fromJson(_result.data!); + return value; + } + + @override + Future iOSOrdersReport(OrdersReport body) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(body.toJson()); + final _result = await _dio + .fetch>(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/order/api/v1/orders/ios', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = OrdersResponse.fromJson(_result.data!); + return value; + } + + @override + Future androidOrdersReport(OrdersReport body) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(body.toJson()); + final _result = await _dio + .fetch>(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/order/api/v1/orders/android', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = OrdersResponse.fromJson(_result.data!); + return value; + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls( + String dioBaseUrl, + String? baseUrl, + ) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} diff --git a/guru_app/lib/api/modules/guru_api_extension.dart b/guru_app/lib/api/modules/guru_api_extension.dart new file mode 100644 index 0000000..039212a --- /dev/null +++ b/guru_app/lib/api/modules/guru_api_extension.dart @@ -0,0 +1,25 @@ +/// Created by Haoyi on 6/4/21 +/// +part of "../guru_api.dart"; + +extension GuruApiExtension on GuruApi { + Future signInWithAnonymous({required String secret}) async { + return await methods.signInWithAnonymous(AnonymousLoginReqBody(secret: secret)); + } + + Future reportDevice(DeviceInfo deviceInfo) async { + return await methods.reportDevice(deviceInfo); + } + + Future renewFirebaseToken() async { + return await methods.renewFirebaseToken(); + } + + Future reportOrders(OrdersReport body) async { + if (Platform.isAndroid) { + return await methods.androidOrdersReport(body); + } else { + return await methods.iOSOrdersReport(body); + } + } +} diff --git a/guru_app/lib/app/app_models.dart b/guru_app/lib/app/app_models.dart new file mode 100644 index 0000000..5a69bf6 --- /dev/null +++ b/guru_app/lib/app/app_models.dart @@ -0,0 +1,184 @@ +import 'dart:io'; + +import 'package:guru_app/firebase/messaging/remote_messaging_manager.dart'; +import 'package:json_annotation/json_annotation.dart'; + +/// Created by Haoyi on 2022/8/29 + +part 'app_models.g.dart'; + +@JsonSerializable() +class AppDetails { + @JsonKey(name: "saas_app_id") + final String saasAppId; + @JsonKey(name: "authority") + final String authority; + @JsonKey(name: "storage_prefix") + final String storagePrefix; + @JsonKey(name: "default_cdn_prefix") + final String defaultCdnPrefix; + @JsonKey(name: "android_gp_url") + final String androidGooglePlayUrl; + @JsonKey(name: "ios_spp_store_url") + final String iosAppStoreUrl; + @JsonKey(name: "policy_url") + final String policyUrl; + @JsonKey(name: "terms_url") + final String termsUrl; + @JsonKey(name: "email_url") + final String emailUrl; + @JsonKey(name: "package_name") + final String packageName; + @JsonKey(name: "bundle_id") + final String bundleId; + @JsonKey(name: "facebook_app_id") + final String facebookAppId; + + String get appId => Platform.isAndroid ? packageName : bundleId; + + AppDetails( + {required this.saasAppId, + required this.authority, + required this.storagePrefix, + required this.defaultCdnPrefix, + required this.androidGooglePlayUrl, + this.iosAppStoreUrl = '', + required this.policyUrl, + required this.termsUrl, + required this.emailUrl, + required this.packageName, + required this.bundleId, + required this.facebookAppId}); + + factory AppDetails.fromJson(Map json) => _$AppDetailsFromJson(json); + + Map toJson() => _$AppDetailsToJson(this); +} + +@JsonSerializable() +class Deployment { + static const int defaultIgcBalanceSecret = 2654404609; + static const int defaultLogFileSizeLimit = 1024 * 1024 * 10; + static const int defaultLogFileCount = 7; + static const int defaultPersistentLogLevel = 2; + static const int defaultApiTimeout = 15000; // 15s + static const int defaultIosSandboxSubsRenewalSpeed = 2; + static const int defaultTrackingNotificationPermissionPassLimitTimes = 10; + + @JsonKey(name: "property_cache_size", defaultValue: 256) + final int propertyCacheSize; + + @JsonKey(name: "enable_dithering", defaultValue: true) + final bool enableDithering; + + @JsonKey(name: "disable_rewards_ads", defaultValue: false) + final bool disableRewardsAds; + + @JsonKey(name: "enable_analytics_statistic", defaultValue: true) + final bool enableAnalyticsStatistic; + + @JsonKey(name: "auto_restore_iap", defaultValue: true) + final bool autoRestoreIap; + + @JsonKey(name: "init_igc", defaultValue: 500) + final int initIgc; + + @JsonKey(name: "igc_balance_secret", defaultValue: defaultIgcBalanceSecret) + final int igcBalanceSecret; + + @JsonKey(name: "sync_account_profile", defaultValue: true) + final bool syncAccountProfile; + + @JsonKey(name: "auto_request_notification_permission", defaultValue: false) + final bool autoRequestNotificationPermission; + + @JsonKey(name: "log_file_size_limit", defaultValue: defaultLogFileSizeLimit) + final int logFileSizeLimit; + + @JsonKey(name: "log_file_count", defaultValue: defaultLogFileCount) + final int logFileCount; + + @JsonKey(name: "persistent_log_level", defaultValue: defaultPersistentLogLevel) + final int persistentLogLevel; + + @JsonKey(name: "ios_validate_receipt_password") + final String? iosValidateReceiptPassword; + + @JsonKey(name: "conversion_events", defaultValue: {}) + final Set conversionEvents; + + @JsonKey(name: "api_connect_timeout", defaultValue: defaultApiTimeout) + final int apiConnectTimeout; + + @JsonKey(name: "api_receive_timeout", defaultValue: defaultApiTimeout) + final int apiReceiveTimeout; + + @JsonKey(name: "ios_sandbox_subs_renewal_speed", defaultValue: defaultIosSandboxSubsRenewalSpeed) + final int iosSandboxSubsRenewalSpeed; + + @JsonKey(name: "ads_compliant_initialization", defaultValue: false) + final bool adsCompliantInitialization; + + @JsonKey(name: "notification_permission_prompt_trigger", defaultValue: PromptTrigger.rationale) + final PromptTrigger notificationPermissionPromptTrigger; + + @JsonKey(name: "tracking_notification_permission_pass", defaultValue: false) + final bool trackingNotificationPermissionPass; + + @JsonKey( + name: "tracking_notification_permission_pass_limit_times", + defaultValue: defaultTrackingNotificationPermissionPassLimitTimes) + final int trackingNotificationPermissionPassLimitTimes; + + @JsonKey(name: "enabled_guru_analytics_strategy", defaultValue: false) + final bool enabledGuruAnalyticsStrategy; + + @JsonKey(name: "allow_interstitial_as_alternative_reward", defaultValue: false) + final bool allowInterstitialAsAlternativeReward; + + @JsonKey(name: "show_internal_ads_when_banner_unavailable", defaultValue: false) + final bool showInternalAdsWhenBannerUnavailable; + + Deployment( + {this.propertyCacheSize = 256, + this.enableDithering = true, + this.disableRewardsAds = false, + this.enableAnalyticsStatistic = true, + this.autoRestoreIap = false, + this.initIgc = 0, + this.igcBalanceSecret = defaultIgcBalanceSecret, + this.syncAccountProfile = true, + this.autoRequestNotificationPermission = false, + this.logFileSizeLimit = defaultLogFileSizeLimit, + this.logFileCount = defaultLogFileCount, + this.persistentLogLevel = defaultPersistentLogLevel, + this.iosValidateReceiptPassword, + this.conversionEvents = const {}, + this.apiConnectTimeout = defaultApiTimeout, + this.apiReceiveTimeout = defaultApiTimeout, + this.iosSandboxSubsRenewalSpeed = defaultIosSandboxSubsRenewalSpeed, + this.adsCompliantInitialization = false, + this.notificationPermissionPromptTrigger = PromptTrigger.rationale, + this.trackingNotificationPermissionPass = false, + this.trackingNotificationPermissionPassLimitTimes = + defaultTrackingNotificationPermissionPassLimitTimes, + this.enabledGuruAnalyticsStrategy = false, + this.allowInterstitialAsAlternativeReward = false, + this.showInternalAdsWhenBannerUnavailable = false}); + + factory Deployment.fromJson(Map json) => _$DeploymentFromJson(json); + + Map toJson() => _$DeploymentToJson(this); +} + +@JsonSerializable() +class RemoteDeployment { + @JsonKey(name: "keep_screen_on_duration_m", defaultValue: 0) + final int keepScreenOnDuration; + + RemoteDeployment({this.keepScreenOnDuration = 0}); + + factory RemoteDeployment.fromJson(Map json) => _$RemoteDeploymentFromJson(json); + + Map toJson() => _$RemoteDeploymentToJson(this); +} diff --git a/guru_app/lib/app/app_models.g.dart b/guru_app/lib/app/app_models.g.dart new file mode 100644 index 0000000..14d032d --- /dev/null +++ b/guru_app/lib/app/app_models.g.dart @@ -0,0 +1,131 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'app_models.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AppDetails _$AppDetailsFromJson(Map json) => AppDetails( + saasAppId: json['saas_app_id'] as String, + authority: json['authority'] as String, + storagePrefix: json['storage_prefix'] as String, + defaultCdnPrefix: json['default_cdn_prefix'] as String, + androidGooglePlayUrl: json['android_gp_url'] as String, + iosAppStoreUrl: json['ios_spp_store_url'] as String? ?? '', + policyUrl: json['policy_url'] as String, + termsUrl: json['terms_url'] as String, + emailUrl: json['email_url'] as String, + packageName: json['package_name'] as String, + bundleId: json['bundle_id'] as String, + facebookAppId: json['facebook_app_id'] as String, + ); + +Map _$AppDetailsToJson(AppDetails instance) => + { + 'saas_app_id': instance.saasAppId, + 'authority': instance.authority, + 'storage_prefix': instance.storagePrefix, + 'default_cdn_prefix': instance.defaultCdnPrefix, + 'android_gp_url': instance.androidGooglePlayUrl, + 'ios_spp_store_url': instance.iosAppStoreUrl, + 'policy_url': instance.policyUrl, + 'terms_url': instance.termsUrl, + 'email_url': instance.emailUrl, + 'package_name': instance.packageName, + 'bundle_id': instance.bundleId, + 'facebook_app_id': instance.facebookAppId, + }; + +Deployment _$DeploymentFromJson(Map json) => Deployment( + propertyCacheSize: json['property_cache_size'] as int? ?? 256, + enableDithering: json['enable_dithering'] as bool? ?? true, + disableRewardsAds: json['disable_rewards_ads'] as bool? ?? false, + enableAnalyticsStatistic: + json['enable_analytics_statistic'] as bool? ?? true, + autoRestoreIap: json['auto_restore_iap'] as bool? ?? true, + initIgc: json['init_igc'] as int? ?? 500, + igcBalanceSecret: json['igc_balance_secret'] as int? ?? 2654404609, + syncAccountProfile: json['sync_account_profile'] as bool? ?? true, + autoRequestNotificationPermission: + json['auto_request_notification_permission'] as bool? ?? false, + logFileSizeLimit: json['log_file_size_limit'] as int? ?? 10485760, + logFileCount: json['log_file_count'] as int? ?? 7, + persistentLogLevel: json['persistent_log_level'] as int? ?? 2, + iosValidateReceiptPassword: + json['ios_validate_receipt_password'] as String?, + conversionEvents: (json['conversion_events'] as List?) + ?.map((e) => e as String) + .toSet() ?? + {}, + apiConnectTimeout: json['api_connect_timeout'] as int? ?? 15000, + apiReceiveTimeout: json['api_receive_timeout'] as int? ?? 15000, + iosSandboxSubsRenewalSpeed: + json['ios_sandbox_subs_renewal_speed'] as int? ?? 2, + adsCompliantInitialization: + json['ads_compliant_initialization'] as bool? ?? false, + notificationPermissionPromptTrigger: $enumDecodeNullable( + _$PromptTriggerEnumMap, + json['notification_permission_prompt_trigger']) ?? + PromptTrigger.rationale, + trackingNotificationPermissionPass: + json['tracking_notification_permission_pass'] as bool? ?? false, + trackingNotificationPermissionPassLimitTimes: + json['tracking_notification_permission_pass_limit_times'] as int? ?? + 10, + enabledGuruAnalyticsStrategy: + json['enabled_guru_analytics_strategy'] as bool? ?? false, + allowInterstitialAsAlternativeReward: + json['allow_interstitial_as_alternative_reward'] as bool? ?? false, + showInternalAdsWhenBannerUnavailable: + json['show_internal_ads_when_banner_unavailable'] as bool? ?? false, + ); + +Map _$DeploymentToJson(Deployment instance) => + { + 'property_cache_size': instance.propertyCacheSize, + 'enable_dithering': instance.enableDithering, + 'disable_rewards_ads': instance.disableRewardsAds, + 'enable_analytics_statistic': instance.enableAnalyticsStatistic, + 'auto_restore_iap': instance.autoRestoreIap, + 'init_igc': instance.initIgc, + 'igc_balance_secret': instance.igcBalanceSecret, + 'sync_account_profile': instance.syncAccountProfile, + 'auto_request_notification_permission': + instance.autoRequestNotificationPermission, + 'log_file_size_limit': instance.logFileSizeLimit, + 'log_file_count': instance.logFileCount, + 'persistent_log_level': instance.persistentLogLevel, + 'ios_validate_receipt_password': instance.iosValidateReceiptPassword, + 'conversion_events': instance.conversionEvents.toList(), + 'api_connect_timeout': instance.apiConnectTimeout, + 'api_receive_timeout': instance.apiReceiveTimeout, + 'ios_sandbox_subs_renewal_speed': instance.iosSandboxSubsRenewalSpeed, + 'ads_compliant_initialization': instance.adsCompliantInitialization, + 'notification_permission_prompt_trigger': + _$PromptTriggerEnumMap[instance.notificationPermissionPromptTrigger]!, + 'tracking_notification_permission_pass': + instance.trackingNotificationPermissionPass, + 'tracking_notification_permission_pass_limit_times': + instance.trackingNotificationPermissionPassLimitTimes, + 'enabled_guru_analytics_strategy': instance.enabledGuruAnalyticsStrategy, + 'allow_interstitial_as_alternative_reward': + instance.allowInterstitialAsAlternativeReward, + 'show_internal_ads_when_banner_unavailable': + instance.showInternalAdsWhenBannerUnavailable, + }; + +const _$PromptTriggerEnumMap = { + PromptTrigger.rationale: 0, + PromptTrigger.request: 1, +}; + +RemoteDeployment _$RemoteDeploymentFromJson(Map json) => + RemoteDeployment( + keepScreenOnDuration: json['keep_screen_on_duration_m'] as int? ?? 0, + ); + +Map _$RemoteDeploymentToJson(RemoteDeployment instance) => + { + 'keep_screen_on_duration_m': instance.keepScreenOnDuration, + }; diff --git a/guru_app/lib/controller/account_aware.dart b/guru_app/lib/controller/account_aware.dart new file mode 100644 index 0000000..296036e --- /dev/null +++ b/guru_app/lib/controller/account_aware.dart @@ -0,0 +1,92 @@ +// +// +// import 'package:guru_app/account/account_data_store.dart'; +// import 'package:guru_app/account/model/account_profile.dart'; +// import 'package:guru_app/account/model/user.dart'; +// import 'package:guru_utils/controller/controller.dart'; +// +// /// Created by Haoyi on 2022/5/23 +// +// mixin AccountAware on LifecycleController { +// AccountDataStore get accountDataStore => AccountDataStore.instance; +// +// Stream get observableAccountProfile => accountDataStore.observableAccountProfile; +// +// String? get saasToken => accountDataStore.saasToken; +// +// String? get uid => accountDataStore.uid; +// +// AccountProfile? get accountProfile => accountDataStore.accountProfile; +// +// String? get nickname => accountDataStore.nickname; +// +// String? get countryCode => accountDataStore.countryCode; +// +// SaasUser? get user => accountDataStore.user; +// +// DeviceInfo? get currentDevice => accountDataStore.currentDevice; +// +// String? get userAvatar => accountProfile?.avatar; +// +// CumulativeInt? get bestScore => accountProfile?.bestScore; +// +// bool get accountInitialized => accountDataStore.initialized; +// +// Stream get observableNickname => +// observableAccountProfile.map((accountProfile) => accountProfile?.nickname); +// +// Stream get observableAccountInitialized => accountDataStore.observableInitialized; +// +// void initAccount() { +// Injector.provide().init(); +// } +// +// Future updateAccountProfile( +// {String? nickname, String? avatar, CumulativeInt? bestScore, String? countryCode}) async { +// final accountService = Injector.provide(); +// // final rankService = Injector.provide(); +// return await accountService.modifyProfile( +// nickname: nickname, avatar: avatar, bestScore: bestScore, countryCode: countryCode); +// // if (result) { +// // await rankService.refreshAccountProfile(); +// // } +// return true; +// } +// +// // Future uploadBestScore() async { +// // final accountService = Injector.provide(); +// // final rankService = Injector.provide(); +// // final latestBestScore = StatisticManager.instance.peekBestScore(); +// // final accountBestScore = bestScore ?? CumulativeInt.zero; +// // +// // Log.w("uploadBestScore latestBestScore! $latestBestScore $accountBestScore", +// // syncFirebase: true); +// // if (latestBestScore > accountBestScore) { +// // String? changedCountryCode = DeviceUtils.buildLocaleInfo().countryCode.toUpperCase(); +// // if (DartExt.isBlank(changedCountryCode) || countryCode == changedCountryCode) { +// // changedCountryCode = null; +// // } +// // try { +// // await accountService.modifyProfile( +// // bestScore: latestBestScore, countryCode: changedCountryCode); +// // } catch (error, stacktrace) { +// // Log.w("modifyProfile error!", error: error, stackTrace: stacktrace, syncFirebase: true); +// // } +// // } +// // await rankService.uploadBestScore(latestBestScore); +// // return true; +// // } +// // +// // RankData buildEmptyRankData(String boardId, {CumulativeInt? bestScore}) { +// // return RankData( +// // boardId, +// // uid ?? "", +// // -1, +// // bestScore ?? this.bestScore, +// // LbUserInfo( +// // nickname: nickname ?? "", +// // countryCode: DeviceUtils.buildLocaleInfo().countryCode.toUpperCase(), +// // avatar: "avatar_1", +// // attr: UserAttr.real)); +// // } +// } diff --git a/guru_app/lib/controller/assets_aware.dart b/guru_app/lib/controller/assets_aware.dart new file mode 100644 index 0000000..bfb9a1c --- /dev/null +++ b/guru_app/lib/controller/assets_aware.dart @@ -0,0 +1,87 @@ +import 'package:guru_app/financial/asset/assets_model.dart'; +import 'package:guru_app/financial/asset/assets_store.dart'; +import 'package:guru_app/financial/financial_manager.dart'; +import 'package:guru_app/financial/iap/iap_manager.dart'; +import 'package:guru_app/financial/iap/iap_model.dart'; +import 'package:guru_app/financial/igc/igc_manager.dart'; +import 'package:guru_app/financial/igc/igc_model.dart'; +import 'package:guru_app/financial/product/product_store.dart'; +import 'package:guru_app/financial/reward/reward_manager.dart'; +import 'package:guru_app/financial/reward/reward_model.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:guru_app/test/test_guru_app_creator.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/controller/controller.dart'; + +/// Created by Haoyi on 2022/4/7 + +mixin AssetsAware on LifecycleController { + final BehaviorSubject> _productStoreSubject = + BehaviorSubject.seeded(ProductStore()); + + ProductStore get currentProductStore => _productStoreSubject.value; + + AssetsStore get currentIapAssetStore => IapManager.instance.purchasedStore; + + AssetsStore get currentRewardedStore => RewardManager.instance.rewardedStore; + + Stream> get observableProductStore => _productStoreSubject.stream; + + Stream> get observableIapPurchased => IapManager.instance.observableAssetStore; + + Stream> get observableRewarded => RewardManager.instance.observableAssetStore; + + Stream> get observableAssets => FinancialManager.instance.observableAssets; + + int _latestRefreshIapProductTimestamp = 0; + + bool get isIapCanceled => IapManager.instance.latestIapCause == IapCause.canceled; + + bool get isIapError => IapManager.instance.latestIapCause == IapCause.error; + + int get currentIgcBalance => IgcManager.instance.currentBalance; + + Stream get observableIgcBalance => IgcManager.instance.observableCurrentBalance; + + Future restorePurchases() async { + return await IapManager.instance.restorePurchases(); + } + + Future clearIapAssets() async { + return await IapManager.instance.clearAssetRecord(); + } + + void observeIapProducts(Set intents) { + addSubscription(IapManager.instance.observableProductDetails.listen((details) async { + final productStore = await IapManager.instance.buildProducts(intents); + _productStoreSubject.addEx(productStore); + })); + } + + void refreshIapProducts() async { + final now = DateTimeUtils.currentTimeInMillis(); + if (now - _latestRefreshIapProductTimestamp > DateTimeUtils.minuteInMillis) { + IapManager.instance.refreshProducts(); + _latestRefreshIapProductTimestamp = now; + } else { + Log.w("refreshIapProducts Too Frequency!", tag: "IAP"); + } + } + + Future buildRewardProduct(TransactionIntent intent) { + return RewardManager.instance.buildRewardProduct(intent); + } + + Future requestProduct(Product product, {String from = ""}) async { + if (product is IapProduct) { + return await IapManager.instance.buy(product); + } else if (product is IgcProduct) { + return await IgcManager.instance.purchase(product); + } else if (product is RewardProduct) { + return await RewardManager.instance.claim(product); + } else { + return false; + } + } +} diff --git a/guru_app/lib/controller/gems_controller.dart b/guru_app/lib/controller/gems_controller.dart new file mode 100644 index 0000000..2574cd7 --- /dev/null +++ b/guru_app/lib/controller/gems_controller.dart @@ -0,0 +1,61 @@ +import 'dart:math'; + +import 'package:get/get.dart' hide Rx; + +import 'dart:ui' as ui show Image; + +/// Created by Haoyi on 2022/7/16 +/// +/// +// abstract class GemsController extends AdsController +// with AssetsAware, InterstitialAware, RewardedAware, VisualFeastAware { +// Future loadGemsResource(VisualFeastEngine engine) async { +// final imageFutures = [ +// Flame.images.load("ic_gem.png"), +// Flame.images.load("ic_gem_add.png"), +// ]; +// final loadedResources = await Future.wait([ +// Future.wait(imageFutures), +// // Future.wait(lottieFutures) +// ]); +// final images = loadedResources[0] as List; +// addSprite("gem", VisualFeastSprite.fromImage(images[0])); +// addSprite("gemAdd", VisualFeastSprite.fromImage(images[1])); +// } +// +// void startClaim(int gems, String method, {bool useBg = true, VoidCallback? onCompleted}) async { +// final engine = createEngine(onCompleted: onCompleted); +// await loadGemsResource(engine); +// +// final designSpec = GemsRewardsDesignSpec.get(); +// final gemsBarSpec = designSpec.buildGemBarSpec(); +// final gemsBar = GemsBar( +// gemBarSpec: gemsBarSpec, +// gemSprite: getSprite("gem"), +// gemAddSprite: getSprite("gemAdd"), +// assetsAware: this); +// final size = designSpec.measuredSize / 2; +// final gemsReward = GemsReward( +// Rect.fromCenter( +// center: Offset(size.width, size.height + gemsBarSpec.gemRect.width * 2), +// width: gemsBarSpec.gemRect.width, +// height: gemsBarSpec.gemRect.width), onFirstGemComplete: () { +// claimGems(gems, method); +// }); +// final background = Background(gemsBarSpec); +// final gemsHeight = gemsBarSpec.gemRect.width; +// final gemsText = GemsText(gems, Offset(size.width, size.height), +// Offset(size.width, size.height - gemsHeight * 2)); +// engine.attachRenders( +// ListUtils.filterOutNulls([useBg ? background : null, gemsBar, gemsReward, gemsText])); +// +// dispatch(engine); +// } +// +// Future claimGems(int gems, String method) async { +// onClaimed(gems, method); +// } +// +// void onClaimed(int gems, String method) { +// } +// } diff --git a/guru_app/lib/database/creators/creators.dart b/guru_app/lib/database/creators/creators.dart new file mode 100644 index 0000000..53b329b --- /dev/null +++ b/guru_app/lib/database/creators/creators.dart @@ -0,0 +1,13 @@ +import 'package:guru_app/financial/data/db/order_database.dart'; +import 'package:guru_utils/database/database.dart'; +import 'package:guru_utils/property/storage/db/property_database.dart'; + +/// Created by Haoyi on 2023/1/13 + +final List _creatorV1 = [PropertyEntity.createTable]; + +final List _creatorV2 = [OrderEntity.createTable]; + +class Creators { + static final List creators = [..._creatorV1, ..._creatorV2]; +} diff --git a/guru_app/lib/database/guru_db.dart b/guru_app/lib/database/guru_db.dart new file mode 100644 index 0000000..f4fb968 --- /dev/null +++ b/guru_app/lib/database/guru_db.dart @@ -0,0 +1,29 @@ +import 'package:guru_app/database/creators/creators.dart'; +import 'package:guru_utils/database/database.dart'; +import 'package:guru_utils/property/storage/property_storage.dart'; +import 'package:guru_utils/property/storage/db/property_database.dart'; +import "migrations/migrations.dart"; + +/// Created by Haoyi on 2022/9/7 + +abstract class _GuruDB extends AppDatabase with PropertyStorage {} + +class GuruDB extends _GuruDB with PropertyDatabase { + static final GuruDB instance = GuruDB._(); + + GuruDB._() { + setDatabase(this); + } + + @override + String get dbName => "guru"; + + @override + List get migrations => Migrations.migrations; + + @override + List get tableCreators => Creators.creators; + + @override + int get version => 3; +} diff --git a/guru_app/lib/database/migrations/migration_v1_to_v2.dart b/guru_app/lib/database/migrations/migration_v1_to_v2.dart new file mode 100644 index 0000000..ff9a50c --- /dev/null +++ b/guru_app/lib/database/migrations/migration_v1_to_v2.dart @@ -0,0 +1,13 @@ +/// Created by Haoyi on 2020/5/22 +/// +part of "migrations.dart"; + +class _MigrationV1toV2 implements Migration { + @override + Future migrate(Transaction transaction) async { + await OrderEntity.createTable(transaction); + return MigrateResult.success; + } +} + +final migration1to2 = _MigrationV1toV2(); diff --git a/guru_app/lib/database/migrations/migration_v2_to_v3.dart b/guru_app/lib/database/migrations/migration_v2_to_v3.dart new file mode 100644 index 0000000..cc76599 --- /dev/null +++ b/guru_app/lib/database/migrations/migration_v2_to_v3.dart @@ -0,0 +1,22 @@ +/// Created by Haoyi on 2023/2/16 + +part of "migrations.dart"; + +class _MigrationV2toV3 implements Migration { + @override + Future migrate(Transaction transaction) async { + // 由于这里无法保证所在平台是否支持IF NOT EXISTS,所以这里用try catch来处理 + try { + await transaction + .execute( + "ALTER TABLE ${OrderEntity.tbName} ADD ${OrderEntity.dbCategory} TEXT DEFAULT ''"); + await transaction.execute( + "CREATE INDEX trans_category_idx ON ${OrderEntity.tbName} (${OrderEntity.dbCategory});"); + } catch(error, stacktrace) { + Log.w("ignore alter cmd!"); + } + return MigrateResult.success; + } +} + +final migration2to3 = _MigrationV2toV3(); diff --git a/guru_app/lib/database/migrations/migrations.dart b/guru_app/lib/database/migrations/migrations.dart new file mode 100644 index 0000000..0e4500d --- /dev/null +++ b/guru_app/lib/database/migrations/migrations.dart @@ -0,0 +1,13 @@ +import 'package:guru_app/financial/data/db/order_database.dart'; +import 'package:guru_utils/database/database.dart'; +import 'package:guru_utils/log/log.dart'; + +part "migration_v1_to_v2.dart"; +part 'migration_v2_to_v3.dart'; + +/// Created by @Haoyi on 2020/5/22 +/// + +class Migrations { + static final migrations = [migration1to2, migration2to3]; +} diff --git a/guru_app/lib/financial/asset/assets_model.dart b/guru_app/lib/financial/asset/assets_model.dart new file mode 100644 index 0000000..197dd3e --- /dev/null +++ b/guru_app/lib/financial/asset/assets_model.dart @@ -0,0 +1,11 @@ +import 'package:guru_app/financial/data/db/order_database.dart'; +import 'package:guru_app/financial/product/product_model.dart'; + +/// Created by Haoyi on 6/1/21 + +class Asset { + final ProductId productId; + final OrderEntity order; + + Asset(this.productId, this.order); +} \ No newline at end of file diff --git a/guru_app/lib/financial/asset/assets_store.dart b/guru_app/lib/financial/asset/assets_store.dart new file mode 100644 index 0000000..d8472b1 --- /dev/null +++ b/guru_app/lib/financial/asset/assets_store.dart @@ -0,0 +1,70 @@ +import 'package:guru_app/financial/asset/assets_model.dart'; +import 'package:guru_app/financial/product/product_model.dart'; + +/// Created by Haoyi on 6/1/21 +/// + +class AssetsStore { + final bool isActive; + final Map data = {}; + + AssetsStore.inactive() : isActive = false; + + AssetsStore() : isActive = true; + + Map toStringMap() { + final result = {}; + int index = 0; + for (var entry in data.entries) { + result["${index++}"] = entry.toString(); + } + return result; + } + + void forEach(void Function(ProductId productId, T asset) callback) { + for (var element in data.entries) { + callback.call(element.key, element.value); + } + } + + void removeWhere(bool Function(ProductId productId, T asset) callback) { + data.removeWhere((key, value) => callback.call(key, value)); + } + + void addAsset(T asset) { + data[asset.productId] = asset; + } + + void clearAsset({String? category, TransactionMethod? method}) { + data.removeWhere((key, value) => + (category == null || value.order.category == category) && + (method == null || value.order.method == method.index)); + } + + void addAllAssets(List assets) { + for (var asset in assets) { + addAsset(asset); + } + } + + T? getAsset(ProductId? productId) { + return productId?.isValid() == true ? data[productId] : null; + } + + bool isOwned(ProductId productId) { + return data.containsKey(productId); + } + + bool existsAssets(Iterable productIds) { + for (var productId in productIds) { + if (isOwned(productId)) { + return true; + } + } + return false; + } + + AssetsStore clone() { + return AssetsStore()..data.addAll(data); + } +} diff --git a/guru_app/lib/financial/data/db/order_database.dart b/guru_app/lib/financial/data/db/order_database.dart new file mode 100644 index 0000000..3a353d2 --- /dev/null +++ b/guru_app/lib/financial/data/db/order_database.dart @@ -0,0 +1,391 @@ +import 'package:guru_app/database/guru_db.dart'; +import 'package:guru_app/financial/product/product_model.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_utils/id/id_utils.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../../manifest/manifest.dart'; + +/// Created by Haoyi on 2022/9/5 + +part 'order_database.g.dart'; + +@JsonSerializable() +class AssetEntity { + static const tbName = "assets"; // Product Transaction Table + static const dbSku = "sku"; + static const dbState = "state"; + static const dbAttribute = "attr"; // 商品属性(所有,消耗品,订阅) + static const dbMethod = "method"; // 购买方式(免费获取,金币购买,IAP购买,激励视频购买) + static const dbCurrency = "currency"; // 货币类型(法币,虚拟币) + static const dbCost = "cost"; // 消费多少 + static const dbTimestamp = "ts"; + static const dbManifest = "manifest"; // 本次交易中的清单 +} + +@JsonSerializable() +class OrderEntity { + static const tbName = "orders"; // Product Transaction Table + static const dbOrderId = "oid"; + static const dbSku = "sku"; + static const dbState = "state"; + static const dbErrorInfo = "err_info"; + static const dbAttribute = "attr"; // 商品属性(所有,消耗品,订阅) + static const dbMethod = "method"; // 购买方式(免费获取,金币购买,IAP购买,激励视频购买) + static const dbCurrency = "currency"; // 货币类型(法币,虚拟币) + static const dbCategory = "category"; + static const dbCost = "cost"; // 消费多少 + static const dbTimestamp = "ts"; + static const dbManifest = "manifest"; // 本次交易中的清单 + + @JsonKey(name: dbOrderId) + final String orderId; + + @JsonKey(name: dbSku) + final String sku; + + @JsonKey(name: dbState) + final int state; + + @JsonKey(name: dbAttribute) + final int attr; + + @JsonKey(name: dbMethod) + final int method; + + @JsonKey(name: dbErrorInfo, defaultValue: "") + final String errorInfo; + + @JsonKey(name: dbCurrency, defaultValue: "") + final String currency; + + @JsonKey(name: dbCost, defaultValue: 0.0) + final double cost; + + @JsonKey(name: dbTimestamp, defaultValue: 0) + final int timestamp; + + @JsonKey(name: dbCategory, defaultValue: "") + final String category; + + @JsonKey(name: dbManifest) + @manifestStringConvert + final Manifest? manifest; + + @JsonKey(ignore: true) + ProductId? _productId; + + OrderEntity( + {required this.orderId, + required this.sku, + required this.state, + required this.attr, + required this.method, + required this.currency, + required this.cost, + required this.timestamp, + required this.category, + required this.manifest, + this.errorInfo = ""}); + + OrderEntity success() { + return OrderEntity( + orderId: orderId, + sku: sku, + state: TransactionState.success, + attr: attr, + method: method, + currency: currency, + cost: cost, + category: category, + timestamp: DateTimeUtils.currentTimeInMillis(), + manifest: manifest); + } + + OrderEntity error(String errorInfo) { + return OrderEntity( + orderId: orderId, + sku: sku, + state: TransactionState.error, + attr: attr, + method: method, + currency: currency, + cost: cost, + category: category, + timestamp: DateTimeUtils.currentTimeInMillis(), + manifest: manifest, + errorInfo: errorInfo); + } + + OrderEntity expired() { + return OrderEntity( + orderId: orderId, + sku: sku, + state: TransactionState.expired, + attr: attr, + method: method, + currency: currency, + cost: cost, + category: category, + timestamp: DateTimeUtils.currentTimeInMillis(), + manifest: manifest, + errorInfo: "Subscription Expired"); + } + + bool get isConsumable => attr == TransactionAttributes.consumable; + + bool get isAsset => attr == TransactionAttributes.asset; + + bool get isSubscription => attr == TransactionAttributes.subscriptions; + + bool get isSuccess => state == TransactionState.success; + + static Future createTable(Transaction delegate) async { + // v1 + const v1Fields = "$dbOrderId TEXT PRIMARY KEY," + "$dbSku TEXT NOT NULL," + "$dbState INTEGER NOT NULL," + "$dbAttribute INTEGER NOT NULL," + "$dbMethod INTEGER NOT NULL," + "$dbErrorInfo TEXT DEFAULT ''," + "$dbCurrency TEXT NOT NULL," + "$dbCost REAL DEFAULT 0.0," + "$dbTimestamp INTEGER NOT NULL," + "$dbManifest TEXT DEFAULT '',"; + + const v2Fields = "$dbCategory TEXT DEFAULT ''"; + + const cmd = "CREATE TABLE $tbName (" + "$v1Fields" + "$v2Fields" + ");"; + + Log.v("#### cmd: $cmd"); + + await delegate.execute(cmd); + await delegate.execute("CREATE INDEX trans_sku_idx ON $tbName ($dbSku);"); + await delegate.execute("CREATE INDEX trans_category_idx ON $tbName ($dbCategory);"); + } + + @override + String toString() { + return 'OrderEntity{tid: $dbOrderId, sku: $sku, state: $state, attr: $attr, method: $method, errorInfo: $errorInfo, currency: $currency, cost: $cost, timestamp: $timestamp, manifest: $manifest}'; + } + + factory OrderEntity.fromMap(Map json) => _$OrderEntityFromJson(json); + + Map toMap() => _$OrderEntityToJson(this); + + Map toUpdateMap() => toMap()..remove(dbOrderId); + + ProductId get productId { + _productId ??= GuruApp.instance.defineProductId(sku, attr, TransactionMethod.values[method]); + return _productId!; + } +} + +extension OrderDatabase on GuruDB { + Future> loadAllOrders() async { + final db = getDb(); + final result = await db.rawQuery("SELECT * FROM ${OrderEntity.tbName}"); + if (result.isNotEmpty) { + return {for (var map in result) map[OrderEntity.dbOrderId] as String: map.toString()}; + } + return {}; + } + + Future> selectOrders( + {required TransactionMethod method, + required List attrs, + int state = TransactionState.success}) async { + final db = getDb(); + final List conditions = [ + "${OrderEntity.dbMethod} = ${method.index}", + "${OrderEntity.dbState} = $state" + ]; + if (attrs.isNotEmpty) { + conditions.add("${OrderEntity.dbAttribute} IN (${attrs.map((attr) => '"$attr"').join(",")})"); + } + final result = + await db.rawQuery("SELECT * FROM ${OrderEntity.tbName} WHERE ${conditions.join(" AND ")}"); + if (result.isNotEmpty) { + return result.map((map) => OrderEntity.fromMap(map)).toList(); + } + return []; + } + + Future> getCompleteOrders(ProductId productId) async { + final db = getDb(); + String where = + "${OrderEntity.dbSku} = '${productId.sku}' AND ${OrderEntity.dbState} = ${TransactionState.success}"; + final result = await db.rawQuery("SELECT * FROM ${OrderEntity.tbName} WHERE $where"); + if (result.isNotEmpty) { + return result.map((map) => OrderEntity.fromMap(map)).toList(); + } + return []; + } + + Future> getPendingOrders(ProductId productId, + {TransactionMethod method = TransactionMethod.iap}) async { + final db = getDb(); + String where = + "${OrderEntity.dbSku} = '${productId.sku}' AND ${OrderEntity.dbMethod} = ${method.index} AND ${OrderEntity.dbState} = ${TransactionState.pending}"; + final result = await db.rawQuery( + "SELECT * FROM ${OrderEntity.tbName} WHERE $where ORDER BY ${OrderEntity.dbTimestamp} DESC"); + if (result.isNotEmpty) { + return result.map((map) => OrderEntity.fromMap(map)).toList(); + } + return []; + } + + Future> completePendingOrders(Set productIds, + {TransactionMethod method = TransactionMethod.iap}) async { + final db = getDb(); + final batch = db.batch(); + final updateValues = { + OrderEntity.dbState: TransactionState.success, + OrderEntity.dbTimestamp: DateTimeUtils.currentTimeInMillis() + }; + for (var productId in productIds) { + final String where = + "${OrderEntity.dbSku} = '${productId.sku}' AND ${OrderEntity.dbMethod} = ${method.index} AND ${OrderEntity.dbState} = ${TransactionState.pending}"; + if (productId.isConsumable) { + batch.delete( + OrderEntity.tbName, + where: where, + ); + } else { + batch.update( + OrderEntity.tbName, + updateValues, + where: where, + ); + } + } + await batch.commit(); + return updateValues; + } + + Future removePendingOrders(Set productIds, + {TransactionMethod method = TransactionMethod.iap}) async { + final db = getDb(); + final batch = db.batch(); + for (var productId in productIds) { + final String where = + "${OrderEntity.dbSku} = '${productId.sku}' AND ${OrderEntity.dbMethod} = ${method.index} AND ${OrderEntity.dbState} = ${TransactionState.pending}"; + batch.delete( + OrderEntity.tbName, + where: where, + ); + } + batch.commit(); + return true; + } + + Future upsertOrder({required OrderEntity order}) { + return runInTransaction((txn) async { + final upsertMap = order.toMap(); + if (!order.isConsumable) { + final result = await txn.rawQuery( + "SELECT * FROM ${OrderEntity.tbName} WHERE ${OrderEntity.dbSku} = '${order.sku}'"); + if (result.isNotEmpty) { + upsertMap[OrderEntity.dbOrderId] = result.first[OrderEntity.dbOrderId] ?? order.orderId; + } + } + await txn.insert(OrderEntity.tbName, upsertMap, conflictAlgorithm: ConflictAlgorithm.replace); + return true; + }); + } + + Future replaceOrderBySku({required OrderEntity order}) { + return runInTransaction((txn) async { + final replaceMap = order.toMap(); + if (!order.isConsumable) { + await txn.rawDelete( + "DELETE FROM ${OrderEntity.tbName} WHERE ${OrderEntity.dbSku} = '${order.sku}'"); + } + await txn.insert(OrderEntity.tbName, replaceMap, + conflictAlgorithm: ConflictAlgorithm.replace); + return true; + }); + } + + Future completeOrder({required OrderEntity order}) async { + final db = getDb(); + if (order.isConsumable) { + await db.rawDelete( + "DELETE FROM ${OrderEntity.tbName} WHERE ${OrderEntity.dbOrderId} = '${order.orderId}'"); + } else { + await db.insert(OrderEntity.tbName, order.toMap(), + conflictAlgorithm: ConflictAlgorithm.replace); + } + return true; + } + + Future upsertOrders(List orders) { + final possessiveOrders = orders.where((order) => !order.isConsumable).toList(); + final skus = possessiveOrders.map((order) => "'${order.sku}'").toList(); + + return runInTransaction((txn) async { + await txn.rawDelete( + "DELETE FROM ${OrderEntity.tbName} WHERE ${OrderEntity.dbSku} IN (${skus.join(",")})"); + for (var upsertOrder in possessiveOrders) { + final upsertOrderJson = upsertOrder.toMap(); + await txn.insert(OrderEntity.tbName, upsertOrderJson, + conflictAlgorithm: ConflictAlgorithm.replace); + } + return true; + }); + } + + Future deleteOrder({required OrderEntity order}) async { + final result = await getDb().rawDelete( + "DELETE FROM ${OrderEntity.tbName} WHERE ${OrderEntity.dbOrderId} = '${order.orderId}'"); + return result > 0; + } + + Future deleteOrdersBySkus(Set skus) async { + final result = await getDb().rawDelete( + "DELETE FROM ${OrderEntity.tbName} WHERE ${OrderEntity.dbSku} IN (${skus.map((sku) => "'$sku'").join(",")})"); + return result > 0; + } + + Future deletePendingOrderBySku({required String sku}) async { + return runInTransaction((txn) async { + String where = + "${OrderEntity.dbSku} = '$sku' AND ${OrderEntity.dbState} = ${TransactionState.pending}"; + final result = await txn.rawQuery( + "SELECT * FROM ${OrderEntity.tbName} WHERE $where ORDER BY ${OrderEntity.dbTimestamp} DESC"); + if (result.isNotEmpty) { + final order = OrderEntity.fromMap(result[0]); + await txn.rawDelete( + "DELETE FROM ${OrderEntity.tbName} WHERE ${OrderEntity.dbOrderId} = '${order.orderId}'"); + return true; + } + return false; + }); + } + + Future clearTransaction() async { + final db = getDb(); + db.delete(OrderEntity.tbName); + } + + Future clearOrders({String? category, TransactionMethod? method}) async { + final db = getDb(); + final whereList = []; + if (category != null) { + whereList.add("${OrderEntity.dbCategory} = '$category'"); + } + if (method != null) { + whereList.add("${OrderEntity.dbMethod} = ${method.index}"); + } + final whereCondition = whereList.join(" AND "); + Log.d("clearOrders: $whereCondition"); + db.rawDelete("DELETE FROM ${OrderEntity.tbName} WHERE $whereCondition"); + return true; + } +} diff --git a/guru_app/lib/financial/data/db/order_database.g.dart b/guru_app/lib/financial/data/db/order_database.g.dart new file mode 100644 index 0000000..e11e9be --- /dev/null +++ b/guru_app/lib/financial/data/db/order_database.g.dart @@ -0,0 +1,55 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'order_database.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AssetEntity _$AssetEntityFromJson(Map json) => AssetEntity(); + +Map _$AssetEntityToJson(AssetEntity instance) => + {}; + +OrderEntity _$OrderEntityFromJson(Map json) => OrderEntity( + orderId: json['oid'] as String, + sku: json['sku'] as String, + state: json['state'] as int, + attr: json['attr'] as int, + method: json['method'] as int, + currency: json['currency'] as String? ?? '', + cost: (json['cost'] as num?)?.toDouble() ?? 0.0, + timestamp: json['ts'] as int? ?? 0, + category: json['category'] as String? ?? '', + manifest: _$JsonConverterFromJson( + json['manifest'], manifestStringConvert.fromJson), + errorInfo: json['err_info'] as String? ?? '', + ); + +Map _$OrderEntityToJson(OrderEntity instance) => + { + 'oid': instance.orderId, + 'sku': instance.sku, + 'state': instance.state, + 'attr': instance.attr, + 'method': instance.method, + 'err_info': instance.errorInfo, + 'currency': instance.currency, + 'cost': instance.cost, + 'ts': instance.timestamp, + 'category': instance.category, + 'manifest': _$JsonConverterToJson( + instance.manifest, manifestStringConvert.toJson), + }; + +Value? _$JsonConverterFromJson( + Object? json, + Value? Function(Json json) fromJson, +) => + json == null ? null : fromJson(json as Json); + +Json? _$JsonConverterToJson( + Value? value, + Json? Function(Value value) toJson, +) => + value == null ? null : toJson(value); diff --git a/guru_app/lib/financial/data/models/orders/orders_model.dart b/guru_app/lib/financial/data/models/orders/orders_model.dart new file mode 100644 index 0000000..e01aa35 --- /dev/null +++ b/guru_app/lib/financial/data/models/orders/orders_model.dart @@ -0,0 +1,94 @@ +import 'dart:io'; + +import 'package:json_annotation/json_annotation.dart'; + +part 'orders_model.g.dart'; + +/// Created by Haoyi on 2022/7/27 + +@JsonSerializable() +class OrdersReport { + // android + @JsonKey(name: 'orderType', defaultValue: 0) + int? orderType; + @JsonKey(name: 'packageName') + String? packageName; + @JsonKey(name: 'productId') + String? productId; + @JsonKey(name: 'subscriptionId') + String? subscriptionId; + @JsonKey(name: 'token') + String? token; + + // ios + @JsonKey(name: 'bundleId') + String? bundleId; + @JsonKey(name: 'receipt') + String? receipt; + @JsonKey(name: 'sku') + String? sku; + @JsonKey(name: 'country') + String? countryCode; + + // general + @JsonKey(name: 'price') + String? price; + @JsonKey(name: 'currency') + String? currency; + + OrdersReport( + {this.orderType, + this.token, + this.packageName, + this.productId, + this.subscriptionId, + this.bundleId, + this.receipt, + this.price, + this.currency, + this.sku, + this.countryCode}); + + @override + String toString() { + final StringBuffer sb = StringBuffer(); + sb.writeln("[OrdersReport]"); + sb.writeln(" productId: $productId"); + sb.writeln(" price: $price"); + sb.writeln(" currency: $currency"); + if (Platform.isAndroid) { + sb.writeln(" orderType: $orderType"); + sb.writeln(" packageName: $packageName"); + sb.writeln(" subscriptionId: $subscriptionId"); + sb.writeln(" token: $token"); + } else if (Platform.isIOS) { + sb.writeln(" bundleId: $bundleId"); + sb.writeln(" receipt: $receipt"); + sb.writeln(" sku: $sku"); + sb.writeln(" countryCode: $countryCode"); + } + return sb + .toString(); //'OrdersReport{orderType: $orderType, packageName: $packageName, productId: $productId, subscriptionId: $subscriptionId, token: $token, bundleId: $bundleId, receipt: $receipt, price: $price, currency: $currency, introductoryPrice: $introductoryPrice}'; + } + + factory OrdersReport.fromJson(Map json) => _$OrdersReportFromJson(json); + + Map toJson() => _$OrdersReportToJson(this); +} + +@JsonSerializable() +class OrdersResponse { + @JsonKey(name: 'usdPrice', defaultValue: 0.0) + double usdPrice; + + OrdersResponse(this.usdPrice); + + factory OrdersResponse.fromJson(Map json) => _$OrdersResponseFromJson(json); + + Map toJson() => _$OrdersResponseToJson(this); + + @override + String toString() { + return 'OrdersResponse{usdPrice:$usdPrice}'; + } +} diff --git a/guru_app/lib/financial/data/models/orders/orders_model.g.dart b/guru_app/lib/financial/data/models/orders/orders_model.g.dart new file mode 100644 index 0000000..dd28d2f --- /dev/null +++ b/guru_app/lib/financial/data/models/orders/orders_model.g.dart @@ -0,0 +1,46 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'orders_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +OrdersReport _$OrdersReportFromJson(Map json) => OrdersReport( + orderType: json['orderType'] as int? ?? 0, + token: json['token'] as String?, + packageName: json['packageName'] as String?, + productId: json['productId'] as String?, + subscriptionId: json['subscriptionId'] as String?, + bundleId: json['bundleId'] as String?, + receipt: json['receipt'] as String?, + price: json['price'] as String?, + currency: json['currency'] as String?, + sku: json['sku'] as String?, + countryCode: json['country'] as String?, + ); + +Map _$OrdersReportToJson(OrdersReport instance) => + { + 'orderType': instance.orderType, + 'packageName': instance.packageName, + 'productId': instance.productId, + 'subscriptionId': instance.subscriptionId, + 'token': instance.token, + 'bundleId': instance.bundleId, + 'receipt': instance.receipt, + 'sku': instance.sku, + 'country': instance.countryCode, + 'price': instance.price, + 'currency': instance.currency, + }; + +OrdersResponse _$OrdersResponseFromJson(Map json) => + OrdersResponse( + (json['usdPrice'] as num?)?.toDouble() ?? 0.0, + ); + +Map _$OrdersResponseToJson(OrdersResponse instance) => + { + 'usdPrice': instance.usdPrice, + }; diff --git a/guru_app/lib/financial/financial_manager.dart b/guru_app/lib/financial/financial_manager.dart new file mode 100644 index 0000000..9c40cf0 --- /dev/null +++ b/guru_app/lib/financial/financial_manager.dart @@ -0,0 +1,59 @@ +import 'package:guru_app/financial/asset/assets_model.dart'; +import 'package:guru_app/financial/asset/assets_store.dart'; +import 'package:guru_app/financial/iap/iap_manager.dart'; +import 'package:guru_app/financial/iap/iap_model.dart'; +import 'package:guru_app/financial/igc/igc_manager.dart'; +import 'package:guru_app/financial/reward/reward_manager.dart'; +import 'package:guru_utils/extensions/extensions.dart'; + +/// Created by Haoyi on 2021/7/2 + +class FinancialManager { + static final FinancialManager instance = FinancialManager._(); + + FinancialManager._(); + +// final RewardAssetService rewardService; + + Stream> get observableAssets => Rx.combineLatest3, + AssetsStore, AssetsStore, AssetsStore>( + IapManager.instance.observableAssetStore, + IgcManager.instance.observableAssetStore, + RewardManager.instance.observableAssetStore, (iapPurchased, gemAssets, rewarded) { + return _merge(iapPurchased: iapPurchased, gemAssets: gemAssets, rewarded: rewarded); + }); + + AssetsStore get currentAssets => _merge( + iapPurchased: IapManager.instance.purchasedStore, + gemAssets: IgcManager.instance.purchasedStore, + rewarded: RewardManager.instance.rewardedStore); + + static AssetsStore _merge( + {required AssetsStore iapPurchased, + required AssetsStore gemAssets, + required AssetsStore rewarded}) { + final result = AssetsStore(); + if (iapPurchased.isActive) { + iapPurchased.forEach((productId, possessions) { + result.addAsset(possessions); + }); + } + if (gemAssets.isActive) { + gemAssets.forEach((productId, possessions) { + result.addAsset(possessions); + }); + } + if (rewarded.isActive) { + rewarded.forEach((productId, possessions) { + result.addAsset(possessions); + }); + } + return result; + } + + void init() { + IapManager.instance.init(); + IgcManager.instance.init(); + RewardManager.instance.init(); + } +} diff --git a/guru_app/lib/financial/iap/iap_manager.dart b/guru_app/lib/financial/iap/iap_manager.dart new file mode 100644 index 0000000..475327d --- /dev/null +++ b/guru_app/lib/financial/iap/iap_manager.dart @@ -0,0 +1,1466 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter/rendering.dart'; +import 'package:guru_analytics_flutter/events_constants.dart'; +import 'package:guru_app/account/account_data_store.dart'; +import 'package:guru_app/api/data/orders/orders_model.dart'; +import 'package:guru_app/api/guru_api.dart'; +import 'package:guru_app/database/guru_db.dart'; +import 'package:guru_app/financial/asset/assets_model.dart'; +import 'package:guru_app/financial/asset/assets_store.dart'; +import 'package:guru_app/financial/data/db/order_database.dart'; +import 'package:guru_app/financial/iap/iap_model.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/product/product_store.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:guru_app/property/app_property.dart'; +import 'package:guru_app/property/property_keys.dart'; +import 'package:guru_app/property/settings/guru_settings.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/math/math_utils.dart'; +import 'package:guru_utils/tuple/tuple.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; +import 'package:in_app_purchase_android/billing_client_wrappers.dart'; +import 'package:in_app_purchase_android/in_app_purchase_android.dart'; +import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; +import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; + +/// Created by Haoyi on 2022/6/10 +/// + +enum IapCause { success, error, canceled } + +class IapManager { + static final IapManager _instance = IapManager._(); + + static IapManager get instance => _instance; + + static final ProductDetailsResponse _emptyResponse = + ProductDetailsResponse(productDetails: [], notFoundIDs: [], error: null); + + final BehaviorSubject> _productDetailsSubject = + BehaviorSubject.seeded({}); + final BehaviorSubject> _iapStoreSubject = + BehaviorSubject.seeded(AssetsStore.inactive()); + + final Map iapRequestMap = + HashMap(); + + Stream> get observableProductDetails => + _productDetailsSubject.stream; + + Stream> get observableAssetStore => + _iapStoreSubject.stream; + + Map get loadedProductDetails => + _productDetailsSubject.value; + + AssetsStore get purchasedStore => _iapStoreSubject.value; + + final BehaviorSubject availableSubject = BehaviorSubject.seeded(false); + + Stream get observableAvailable => availableSubject.stream; + + bool get iapAvailable => availableSubject.value; + + final InAppPurchase _inAppPurchase; + + StreamSubscription? subscription; + + Timer? restorePointsTimer; + + IapManager._() : _inAppPurchase = InAppPurchase.instance; + + IapCause latestIapCause = IapCause.success; + + bool _restorePurchase = false; + + final iapRevenueAppEventOptions = AppEventOptions( + capabilities: const AppEventCapabilities( + AppEventCapabilities.firebase | AppEventCapabilities.guru), + firebaseParamsConvertor: _iapRevenueToValue, + guruParamsConvertor: _iapRevenueToValue); + + static Map _iapRevenueToValue(Map params) { + final result = Map.of(params); + final revenue = result.remove("revenue"); + if (revenue != null) { + result["value"] = revenue; + } + return result; + } + + void init() async { + final iapCount = await AppProperty.getInstance().getIapCount(); + if (iapCount > 0) { + GuruAnalytics.instance + .setUserProperty("purchase_count", iapCount.toString()); + GuruAnalytics.instance.setUserProperty("is_iap_user", "true"); + } else { + GuruAnalytics.instance.setUserProperty("is_iap_user", "false"); + } + try { + await reloadOrders(); + } catch (error, stacktrace) { + Log.w("reloadOrders error! $error", + stackTrace: stacktrace, syncCrashlytics: true, syncFirebase: true); + } + if (subscription == null) { + final Stream> purchaseUpdated = + _inAppPurchase.purchaseStream; + subscription = purchaseUpdated.listen( + (List purchaseDetailsList) { + _listenToPurchaseUpdated(purchaseDetailsList); + }, + onDone: () {}, + onError: (Object error) { + // handle error here. + Log.e("iap error:$error"); + }); + Log.i("iap service initialize completed"); + } + Log.i("iap service initialized"); + + _checkAndLoad(); + + try { + await AccountDataStore.instance.observableSaasUser + .firstWhere((saasUser) => saasUser?.isValid == true); + Future.delayed(const Duration(seconds: 5), () { + reportFailedOrders(); + }); + } catch (error, stacktrace) { + Log.w("wait account error! $error", stackTrace: stacktrace); + } finally {} + } + + Future reloadOrders() async { + final transactions = await GuruDB.instance.selectOrders( + method: TransactionMethod.iap, + attrs: [ + TransactionAttributes.asset, + TransactionAttributes.subscriptions + ]); + final newAssetStore = AssetsStore(); + Log.d("reloadOrders ${transactions.length}"); + for (var transaction in transactions) { + final productId = transaction.productId; + Log.d(" ==> reloadOrder:${transaction.sku} $productId"); + newAssetStore.addAsset(Asset(productId, transaction)); + } + _iapStoreSubject.addEx(newAssetStore); + } + + void _checkAndLoad() async { + var available = false; + var retry = 0; + Log.i("_checkAndLoad"); + do { + final seconds = min(MathUtils.fibonacci(retry, offset: 2), 900); + await Future.delayed(Duration(seconds: seconds)); + available = + await _inAppPurchase.isAvailable().catchError((error, stacktrace) { + Log.w("isAvailable error:$error", stackTrace: stacktrace); + return false; + }); + Log.d("_checkAndLoad:$retry available:$available"); + retry++; + } while (!available); + availableSubject.addEx(true); + try { + await refreshProducts(); + if (GuruApp.instance.appSpec.deployment.autoRestoreIap || + GuruApp.instance.appSpec.productProfile.hasSubs()) { + await restorePurchases(); + } + } catch (error, stacktrace) { + Log.w("restorePurchases error:$error", + stackTrace: stacktrace, syncCrashlytics: true, syncFirebase: true); + } + } + + Future isAvailable() async { + return await _inAppPurchase.isAvailable(); + } + + void _processIapError() async { + latestIapCause = IapCause.error; + for (var iapRequest in iapRequestMap.values) { + iapRequest.response(false); + final iapErrorMsg = "_processIapError:${iapRequest.productId}"; + Log.w(iapErrorMsg, + error: PurchaseError(iapErrorMsg), + syncFirebase: true, + syncCrashlytics: true); + try { + await GuruDB.instance + .upsertOrder(order: iapRequest.order.error(iapErrorMsg)); + } catch (error, stacktrace) { + Log.w("_processIapError upsert error! $error", syncFirebase: true); + } + } + iapRequestMap.clear(); + } + + void _processIapCancel() async { + latestIapCause = IapCause.canceled; + Log.d("_processIapCancel"); + for (var iapRequest in iapRequestMap.values) { + final order = iapRequest.order; + iapRequest.response(false); + try { + await GuruDB.instance.deleteOrder(order: order); + } catch (error, stacktrace) { + Log.w("_processIapCancel deleteOrder error! $error", + syncFirebase: true); + } + } + iapRequestMap.clear(); + // final iapErrorMsg = "_processIapCancel:$productId"; + // Log.w(iapErrorMsg, + // error: PurchaseError(iapErrorMsg), syncFirebase: true, syncCrashlytics: true); + } + + // void _listenToPurchased() async { + // InAppPurchase.instance.purchaseStream.listen((purchaseDetailsList) { + // if (purchaseDetailsList.isEmpty) { + // return; + // } + // final subscriptionDetails = {}; + // for (var details in purchaseDetailsList) { + // Log.d(" details:${details.productID} ${details.status}"); + // final productId = + // GuruApp.instance.findProductId(sku: details.productID) ?? ProductId.invalid; + // if (productId.isSubscription) { + // subscriptionDetails[productId] = details; + // } + // } + // if (Platform.isIOS) { + // checkSubscriptionForIos(subscriptionDetails); + // } + // }); + // } + + String dumpProductAndPurchased( + ProductDetails details, PurchaseDetails purchaseDetails) { + final StringBuffer sb = StringBuffer(); + + if (Platform.isAndroid) { + try { + GooglePlayPurchaseDetails googlePlayDetails = + purchaseDetails as GooglePlayPurchaseDetails; + GooglePlayProductDetails googlePlayProduct = + details as GooglePlayProductDetails; + Log.d( + "Android Product/Purchase ${buildGooglePlayDetailsString(googlePlayProduct, googlePlayDetails)}"); + } catch (error, stacktrace) {} + } else if (Platform.isIOS) { + AppStorePurchaseDetails appleDetails = + purchaseDetails as AppStorePurchaseDetails; + AppStoreProductDetails appleProduct = details as AppStoreProductDetails; + sb.writeln("#### purchase ####"); + sb.writeln("productID: ${appleDetails.productID}"); + sb.writeln("purchaseID: ${appleDetails.purchaseID}"); + sb.writeln("transactionDate: ${appleDetails.transactionDate}"); + sb.writeln("verificationData: ${appleDetails.verificationData}"); + sb.writeln("status: ${appleDetails.status}"); + sb.writeln("skPaymentTransaction:"); + sb.writeln( + " =>originalTransaction:${appleDetails.skPaymentTransaction.originalTransaction}"); + sb.writeln( + " =>${appleDetails.skPaymentTransaction.originalTransaction?.payment}"); + sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionState}:"); + sb.writeln( + " =>${appleDetails.skPaymentTransaction.transactionIdentifier}:"); + sb.writeln("\n#### product ####"); + sb.writeln("currencyCode: ${appleProduct.currencyCode}"); + sb.writeln("rawPrice: ${appleProduct.rawPrice}"); + sb.writeln("currencyCode: ${appleProduct.currencyCode}"); + sb.writeln("currencyCode skProduct"); + sb.writeln( + " =>localizedTitle: ${appleProduct.skProduct.localizedTitle}"); + sb.writeln( + " =>localizedDescription: ${appleProduct.skProduct.localizedDescription}"); + sb.writeln(" =>priceLocale: ${appleProduct.skProduct.priceLocale}"); + sb.writeln( + " =>productIdentifier: ${appleProduct.skProduct.productIdentifier}"); + sb.writeln( + " =>subscriptionGroupIdentifier: ${appleProduct.skProduct.subscriptionGroupIdentifier}"); + sb.writeln(" =>appleProduct.skProduct.priceLocale"); + sb.writeln(" ->{appleProduct.skProduct.priceLocale}"); + + Log.d("IOS Product/Purchase ${sb.toString()}"); + } + return sb.toString(); + } + + static final monthRenewalDurations = [ + 3 * DateTimeUtils.minuteInMillis, + 5 * DateTimeUtils.minuteInMillis, + 15 * DateTimeUtils.minuteInMillis, + 30 * DateTimeUtils.minuteInMillis, + DateTimeUtils.hourInMillis + ]; + + static final weekRenewalDurations = [ + 3 * DateTimeUtils.minuteInMillis, + 3 * DateTimeUtils.minuteInMillis, + 5 * DateTimeUtils.minuteInMillis, + 10 * DateTimeUtils.minuteInMillis, + 15 * DateTimeUtils.minuteInMillis + ]; + + int getIOSPeriodInterval(int numberOfUnits, SKSubscriptionPeriodUnit unit) { + if (GuruSettings.instance.debugMode.get()) { + final renewalSpeed = GuruApp + .instance.appSpec.deployment.iosSandboxSubsRenewalSpeed + .clamp(1, 5); + switch (unit) { + case SKSubscriptionPeriodUnit.day: + return numberOfUnits * weekRenewalDurations[renewalSpeed - 1] ~/ 7; + case SKSubscriptionPeriodUnit.week: + return numberOfUnits * weekRenewalDurations[renewalSpeed - 1]; + case SKSubscriptionPeriodUnit.month: + return numberOfUnits * monthRenewalDurations[renewalSpeed - 1]; + case SKSubscriptionPeriodUnit.year: + return numberOfUnits * monthRenewalDurations[renewalSpeed - 1] * 12; + } + } else { + switch (unit) { + case SKSubscriptionPeriodUnit.day: + return numberOfUnits * DateTimeUtils.dayInMillis; + case SKSubscriptionPeriodUnit.week: + return numberOfUnits * DateTimeUtils.weekInMillis; + case SKSubscriptionPeriodUnit.month: + return numberOfUnits * DateTimeUtils.dayInMillis * 31; + case SKSubscriptionPeriodUnit.year: + return numberOfUnits * DateTimeUtils.dayInMillis * 366; + } + } + } + + Future processRestoredSubscription( + List subscriptionPurchased) async { + List purchasedDetails = subscriptionPurchased; + if (Platform.isIOS) { + purchasedDetails = buildLatestPurchasedPlanForIos(subscriptionPurchased); + } + final newPurchasedStore = purchasedStore.clone(); + final expiredSkus = {}; + // 由于Android的订阅项目在失效后,这里将不会返回,因此需要判断这里的newPurchasedStore是否存在对应的purchased + // 如果存在将会在后面进行处理,如果不存在。这里将会从purchasedStore中删除 + if (Platform.isAndroid) { + final purchasedSkus = purchasedDetails.map((e) => e.productID).toSet(); + newPurchasedStore.removeWhere((productId, asset) { + final expired = + productId.isSubscription && !purchasedSkus.contains(productId.sku); + Log.i("remove expired subscription[$productId] expired:$expired"); + if (expired) { + expiredSkus.add(asset.productId.sku); + } + return expired; + }); + } + + for (var purchased in purchasedDetails) { + final productId = + GuruApp.instance.findProductId(sku: purchased.productID); + if (productId == null) { + Log.w("productId is null! ${purchased.productID}"); + continue; + } + final productDetails = loadedProductDetails[productId]; + if (productDetails == null) { + Log.w("product is null! ${purchased.productID}"); + continue; + } + purchased.transactionDate; + bool validPurchase = false; + if (Platform.isAndroid) { + validPurchase = true; + } else if (Platform.isIOS) { + final appleProduct = productDetails as AppStoreProductDetails; + final period = appleProduct.skProduct.subscriptionPeriod; + if (period != null) { + final numberOfUnits = period.numberOfUnits; + final unit = period.unit; + final int validInterval = getIOSPeriodInterval(numberOfUnits, unit); + final transactionTs = + int.tryParse(purchased.transactionDate ?? "") ?? 0; + final now = DateTimeUtils.currentTimeInMillis(); + validPurchase = transactionTs + validInterval < now; + Log.d( + "productID: ${purchased.productID}) purchaseID: ${purchased.purchaseID}[$numberOfUnits][$unit] $transactionTs + $validInterval < $now ($validPurchase)", + tag: PropertyTags.iap); + } + } + if (validPurchase) { + Log.d( + "[Restored Subscription] productID: ${purchased.productID}) purchaseID: ${purchased.purchaseID}", + tag: PropertyTags.iap); + final asset = newPurchasedStore.getAsset(productId); + late OrderEntity newOrder; + if (asset == null) { + final product = await _createProduct( + productId.createIntent(scene: "restore"), productDetails); + newOrder = product.createOrder().success(); + } else { + newOrder = asset.order.success(); + } + try { + await GuruDB.instance.replaceOrderBySku(order: newOrder); + } catch (error, stacktrace) { + Log.w("Failed to upsert order: $error $stacktrace", + tag: PropertyTags.iap); + } + final newAsset = Asset(productId, newOrder); + newPurchasedStore.addAsset(newAsset); + } else { + expiredSkus.add(productId.sku); + Log.d( + "Subscription is expired ${purchased.productID}) ${purchased.purchaseID} ${purchased.transactionDate}"); + // 这里暂不清newPurchasedStore,下次重进后该订阅信息会失效 + } + } + + if (expiredSkus.isNotEmpty) { + Log.i("expired orders:${expiredSkus.length}}"); + try { + await GuruDB.instance.deleteOrdersBySkus(expiredSkus); + } catch (error, stacktrace) { + Log.w("Failed to upsert order: $error $stacktrace", + tag: PropertyTags.iap); + } + } + _iapStoreSubject.addEx(newPurchasedStore); + Log.d( + "[RestoredSubscription] update purchasedStore ${newPurchasedStore.data.length}"); + } + + List buildLatestPurchasedPlanForIos( + List purchaseDetails) { + if (purchaseDetails.isEmpty) { + return []; + } + final rawTransactionIds = purchaseDetails + .map((details) => (details as AppStorePurchaseDetails) + .skPaymentTransaction + .originalTransaction + ?.transactionIdentifier) + .where((element) => element != null) + .cast() + .toSet(); + Log.d("rawTransactionIds:$rawTransactionIds"); + final sortedPurchaseDetails = purchaseDetails.toList(); + sortedPurchaseDetails.sort((a, b) => + (int.tryParse(b.transactionDate ?? '') ?? 0) + .compareTo(int.tryParse(a.transactionDate ?? '') ?? 0)); + sortedPurchaseDetails.retainWhere((details) { + var detail = details as AppStorePurchaseDetails; + Log.d( + "checkSubscriptionForIos ${detail.skPaymentTransaction.originalTransaction?.transactionIdentifier} ${detail.transactionDate} ${detail.skPaymentTransaction.transactionTimeStamp}"); + return rawTransactionIds.remove(detail + .skPaymentTransaction.originalTransaction?.transactionIdentifier); + }); + + return sortedPurchaseDetails; + } + + void checkSubscriptionForIos(List purchaseDetails) { + if (purchaseDetails.isEmpty) { + return; + } + final rawTransactionIds = purchaseDetails + .map((details) => (details as AppStorePurchaseDetails) + .skPaymentTransaction + .originalTransaction + ?.transactionIdentifier) + .where((element) => element != null) + .cast() + .toSet(); + Log.d("rawTransactionIds:$rawTransactionIds"); + final sortedPurchaseDetails = purchaseDetails.toList(); + sortedPurchaseDetails.sort((a, b) => + (int.tryParse(b.transactionDate ?? '') ?? 0) + .compareTo(int.tryParse(a.transactionDate ?? '') ?? 0)); + sortedPurchaseDetails.retainWhere((details) { + var detail = details as AppStorePurchaseDetails; + Log.d( + "checkSubscriptionForIos ${detail.skPaymentTransaction.originalTransaction?.transactionIdentifier} ${detail.transactionDate} ${detail.skPaymentTransaction.transactionTimeStamp}"); + return rawTransactionIds.remove(detail + .skPaymentTransaction.originalTransaction?.transactionIdentifier); + }); + + for (var details in sortedPurchaseDetails) { + Log.d( + "checkSubscriptionForIos ${details.productID} ${details.status} ${details.transactionDate}"); + final productId = GuruApp.instance.findProductId(sku: details.productID); + final productDetails = loadedProductDetails[productId]; + if (productDetails != null) { + dumpProductAndPurchased(productDetails, details); + } + } + } + + void _listenToPurchaseUpdated( + List purchaseDetailsList) async { + final List> restoredIapPurchases = []; + final List> pendingCompletePurchase = []; + final List subscriptionPurchases = []; + bool existsRestored = false; + bool needRestore = false; + Log.d("_listenToPurchaseUpdated ${purchaseDetailsList.length}"); + if (purchaseDetailsList.isEmpty) { + if (_restorePurchase) { + try { + await processRestoredSubscription(subscriptionPurchases); + } catch (error, stacktrace) { + Log.w( + "purchaseDetailsList is EMPTY! processRestoredSubscription error! $error $stacktrace", + syncFirebase: true, + syncCrashlytics: true); + } + _restorePurchase = false; + } + return; + } + for (var details in purchaseDetailsList) { + final productId = + GuruApp.instance.findProductId(sku: details.productID) ?? + ProductId.invalid; + Log.d( + "[details]: $productId [${details.productID}] ${details.purchaseID} ${details.status} ${details.pendingCompletePurchase}"); + GuruAnalytics.instance.logGuruEvent('dev_iap_update', { + "sku": details.productID, + "orderId": details.purchaseID, + "status": "${details.status.index}" + }); + switch (details.status) { + case PurchaseStatus.purchased: + if (details.productID == "") { + if (GuruApp.instance.productProfile.pointsIapIds.isNotEmpty) { + Log.w( + "details.productID is empty And Exists PointsIap! ${details.purchaseID}! need restore"); + needRestore = true; + } else { + Log.w("details.productID is empty ${details.purchaseID}! ignore!!"); + } + continue; + } + + final productDetails = loadedProductDetails[productId]; + if (productDetails != null) { + await _completePurchase(productId, productDetails, details); + } + + Log.d("completePurchase ${details.productID} ${details.purchaseID}"); + break; + case PurchaseStatus.restored: + _restorePurchase = false; + existsRestored = true; + if (productId.isAsset) { + restoredIapPurchases.add(Tuple2(productId, details)); + Log.d("restore possessive iap:$productId"); + } else if (productId.isSubscription) { + Log.w("restore subscription product!", syncFirebase: true); + subscriptionPurchases.add(details); + } + // 如果是未完成的商品或是恢复出了消耗品,都需要手动完成 + if (Platform.isAndroid) { + final originPurchaseState = (details as GooglePlayPurchaseDetails) + .billingClientPurchase + .purchaseState; + Log.d( + "restore android ${details.pendingCompletePurchase} $productId $originPurchaseState"); + if (originPurchaseState == PurchaseStateWrapper.purchased) { + if (productId.isConsumable || + (details.pendingCompletePurchase && productId.isAsset)) { + Log.w("restore consumable product!", syncFirebase: true); + pendingCompletePurchase.add(Tuple2(productId, details)); + } + } + } else { + if (details.pendingCompletePurchase) { + Log.d("restore ios pendingCompletePurchase: $productId"); + await _inAppPurchase.completePurchase(details); + } + } + break; + case PurchaseStatus.error: + _processIapError(); + if (details.pendingCompletePurchase) { + await _inAppPurchase.completePurchase(details); + } + break; + case PurchaseStatus.canceled: + _processIapCancel(); + if (details.pendingCompletePurchase) { + await _inAppPurchase.completePurchase(details); + } + break; + default: + break; + } + Log.d( + "_listenToPurchaseUpdated2:$productId [${details.productID}] ${details.purchaseID} ${details.status} ${details.pendingCompletePurchase}"); + } + if (existsRestored) { + if (pendingCompletePurchase.isNotEmpty) { + await completeAllPurchases(pendingCompletePurchase); + Log.d("manual complete/consume all purchases!", + syncFirebase: true, syncCrashlytics: true); + } + + if (restoredIapPurchases.isNotEmpty) { + try { + await processRestoredPurchases(restoredIapPurchases); + } catch (error, stacktrace) { + Log.w("processRestoredPurchases error! $error $stacktrace", + syncFirebase: true, syncCrashlytics: true); + } + } + try { + await processRestoredSubscription(subscriptionPurchases); + } catch (error, stacktrace) { + Log.w("processRestoredSubscription error! $error $stacktrace", + syncFirebase: true, syncCrashlytics: true); + } + } + if (needRestore) { + restorePointsTimer?.cancel(); + restorePointsTimer = Timer(const Duration(seconds: 1), () { + restorePurchases(); + }); + } + } + + Future processRestoredPurchases( + List> restoredIapPurchases) async { + final newPurchased = purchasedStore.clone(); + final currentLoadedProductDetails = loadedProductDetails; + final upsertOrders = []; + for (var iapPurchased in restoredIapPurchases) { + final productId = iapPurchased.item1; + final asset = newPurchased.getAsset(iapPurchased.item1); + final productDetails = currentLoadedProductDetails[productId]; + final order = asset?.order; + // 证明是已经购买过的 + if (order != null) { + // 如果没有购买成功,那么就重新创建一个 + if (!order.isSuccess) { + final newOrder = order.success(); + upsertOrders.add(newOrder); + } + } else if (productDetails != null) { + final product = await _createProduct( + productId.createIntent(scene: "restore"), productDetails); + final newOrder = product.createOrder().success(); + upsertOrders.add(newOrder); + } + } + if (upsertOrders.isNotEmpty) { + final List updatedOrder = []; + try { + await GuruDB.instance.upsertOrders(upsertOrders); + updatedOrder.addAll(upsertOrders); + } catch (error, stacktrace) { + Log.w("upsertOrders error:$error $stacktrace", + syncCrashlytics: true, syncFirebase: true); + for (var order in upsertOrders) { + try { + await GuruDB.instance.upsertOrder(order: order); + updatedOrder.add(order); + } catch (error1, stacktrace1) { + Log.w("upsertOrder(${order.sku}) error:$error1 $stacktrace1", + syncFirebase: true); + } + } + } + final assets = + updatedOrder.map((order) => Asset(order.productId, order)).toList(); + newPurchased.addAllAssets(assets); + } + _iapStoreSubject.addEx(newPurchased); + Log.d("[RestoredPurchases] update purchasedStore ${upsertOrders.length}"); + } + + Future reportFailedOrders() async { + final failedIapOrders = + await AppProperty.getInstance().loadAllFailedIapOrders(); + failedIapOrders.forEach((key, value) async { + try { + final order = OrdersReport.fromJson(json.decode(value)); + final result = await GuruApi.instance.reportOrders(order); + if (result.usdPrice > 0) { + logRevenue( + result.usdPrice, order.productId ?? order.subscriptionId); + } + AppProperty.getInstance().removeReportSuccessOrder(key); + } catch (error, stacktrace) {} + }); + Log.i("reportFailedOrders success!"); + } + + String buildGooglePlayDetailsString( + GooglePlayProductDetails googlePlayProduct, + GooglePlayPurchaseDetails googlePlayDetails) { + final StringBuffer sb = StringBuffer(); + sb.writeln("#### purchase ####"); + + sb.writeln("productID: ${googlePlayDetails.productID}"); + sb.writeln("purchaseID: ${googlePlayDetails.purchaseID}"); + sb.writeln("transactionDate: ${googlePlayDetails.transactionDate}"); + + sb.writeln("status: ${googlePlayDetails.status}"); + sb.writeln("verificationData:"); + sb.writeln( + " => localVerificationData: ${googlePlayDetails.verificationData.localVerificationData}"); + sb.writeln( + " => serverVerificationData: ${googlePlayDetails.verificationData.localVerificationData}"); + sb.writeln(" => source: ${googlePlayDetails.verificationData.source}"); + sb.writeln("\n#### product ####"); + sb.writeln("price: ${googlePlayProduct.price}"); + sb.writeln("rawPrice: ${googlePlayProduct.rawPrice}"); + sb.writeln("currencyCode: ${googlePlayProduct.currencyCode}"); + sb.writeln("currencySymbol: ${googlePlayProduct.currencySymbol}"); + sb.writeln("productDetails:"); + + final productDetails = googlePlayProduct.productDetails; + sb.writeln(" => description: ${productDetails.name}"); + sb.writeln(" => freeTrialPeriod: ${productDetails.title}"); + sb.writeln(" => description: ${productDetails.description}"); + sb.writeln(" => freeTrialPeriod: ${productDetails.productType}"); + + final oneTimeDetails = productDetails.oneTimePurchaseOfferDetails; + if (oneTimeDetails != null) { + sb.writeln(" => oneTimeDetails:"); + sb.writeln(" - formattedPrice: ${oneTimeDetails.formattedPrice}"); + sb.writeln( + " - priceAmountMicros: ${oneTimeDetails.priceAmountMicros}"); + sb.writeln( + " - priceCurrencyCode: ${oneTimeDetails.priceCurrencyCode}"); + } + + final subscriptionOfferDetails = productDetails.subscriptionOfferDetails; + if (subscriptionOfferDetails != null && + subscriptionOfferDetails.isNotEmpty) { + for (var offer in subscriptionOfferDetails) { + sb.writeln(" => sub offer: ${offer.offerId}"); + sb.writeln(" - basePlanId: ${offer.basePlanId}"); + sb.writeln(" - offerTag: ${offer.offerTags}"); + sb.writeln(" - offerIdToken: ${offer.offerIdToken}"); + final pricingPhases = offer.pricingPhases; + for (var idx = 0; idx < pricingPhases.length; ++idx) { + final phase = pricingPhases[idx]; + sb.writeln(" - pricingPhase[$idx]:"); + sb.writeln(" * billingCycleCount: ${phase.billingCycleCount}"); + sb.writeln(" * billingPeriod: ${phase.billingPeriod}"); + sb.writeln(" * formattedPrice: ${phase.formattedPrice}"); + sb.writeln(" * priceAmountMicros: ${phase.priceAmountMicros}"); + sb.writeln(" * priceCurrencyCode: ${phase.priceCurrencyCode}"); + sb.writeln(" * recurrenceMode: ${phase.recurrenceMode}"); + } + } + } + + return sb.toString(); + } + + Future reportOrders(ProductId productId, ProductDetails details, + PurchaseDetails purchaseDetails, OrderEntity? order) async { + final OrdersReport ordersReport = OrdersReport(); + + if (Platform.isAndroid) { + ordersReport.token = + purchaseDetails.verificationData.serverVerificationData; + ordersReport.packageName = GuruApp.instance.details.packageName; + final manifest = order?.manifest; + final basePlanId = manifest?.basePlanId; + final offerId = manifest?.offerId; + if (productId.isSubscription && basePlanId != null && offerId != null) { + ordersReport.basePlanId = basePlanId; + ordersReport.offerId = offerId; + } + try { + GooglePlayPurchaseDetails googlePlayDetails = + purchaseDetails as GooglePlayPurchaseDetails; + GooglePlayProductDetails googlePlayProduct = + details as GooglePlayProductDetails; + Log.d( + "Android Product/Purchase ${buildGooglePlayDetailsString(googlePlayProduct, googlePlayDetails)}"); + } catch (error, stacktrace) {} + } else if (Platform.isIOS) { + AppStorePurchaseDetails appleDetails = + purchaseDetails as AppStorePurchaseDetails; + AppStoreProductDetails appleProduct = details as AppStoreProductDetails; + final StringBuffer sb = StringBuffer(); + sb.writeln("#### purchase ####"); + sb.writeln("productID: ${appleDetails.productID}"); + sb.writeln("purchaseID: ${appleDetails.purchaseID}"); + sb.writeln("transactionDate: ${appleDetails.transactionDate}"); + sb.writeln("verificationData: ${appleDetails.verificationData}"); + sb.writeln("status: ${appleDetails.status}"); + sb.writeln("skPaymentTransaction:"); + sb.writeln( + " =>originalTransaction:${appleDetails.skPaymentTransaction.originalTransaction}"); + sb.writeln( + " =>${appleDetails.skPaymentTransaction.originalTransaction?.payment}"); + sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionState}:"); + sb.writeln( + " =>${appleDetails.skPaymentTransaction.transactionIdentifier}:"); + sb.writeln("\n#### product ####"); + sb.writeln("currencyCode: ${appleProduct.currencyCode}"); + sb.writeln("rawPrice: ${appleProduct.rawPrice}"); + sb.writeln("currencyCode: ${appleProduct.currencyCode}"); + sb.writeln("currencyCode skProduct"); + sb.writeln( + " =>localizedTitle: ${appleProduct.skProduct.localizedTitle}"); + sb.writeln( + " =>localizedDescription: ${appleProduct.skProduct.localizedDescription}"); + sb.writeln(" =>priceLocale: ${appleProduct.skProduct.priceLocale}"); + sb.writeln( + " =>productIdentifier: ${appleProduct.skProduct.productIdentifier}"); + sb.writeln( + " =>subscriptionGroupIdentifier: ${appleProduct.skProduct.subscriptionGroupIdentifier}"); + sb.writeln(" =>appleProduct.skProduct.priceLocale"); + sb.writeln(" ->{appleProduct.skProduct.priceLocale}"); + + ordersReport.bundleId = GuruApp.instance.appSpec.details.bundleId; + ordersReport.receipt = + purchaseDetails.verificationData.serverVerificationData; + ordersReport.sku = appleDetails.productID; + ordersReport.countryCode = appleProduct.skProduct.priceLocale.countryCode; + Log.d("IOS Product/Purchase ${sb.toString()}"); + } + + if (productId.isSubscription) { + ordersReport.orderType = OrderType.subs; + ordersReport.subscriptionId = details.id; + } else { + ordersReport.orderType = OrderType.inapp; + ordersReport.productId = details.id; + } + ordersReport.price = details.rawPrice.toString(); + ordersReport.currency = details.currencyCode; + + ordersReport.orderUserInfo = + OrderUserInfo(GuruSettings.instance.bestLevel.get().toString()); + ordersReport.userIdentification = GuruAnalytics.instance.userIdentification; + + Log.d("orderReport:$ordersReport", tag: "Iap"); + try { + final result = await GuruApi.instance.reportOrders(ordersReport); + if ((result.usdPrice > 0) || + (result.usdPrice == 0 && result.isTestOrder)) { + logRevenue(result.usdPrice, purchaseDetails.productID); + Log.i("reportOrders success! $result"); + return; + } + Log.i("ignoreInvalidResult $result", tag: "Iap"); + } catch (error, stacktrace) { + Log.i("reportOrders error!", error: error, stackTrace: stacktrace); + } + AppProperty.getInstance().saveFailedIapOrders(ordersReport); + } + + Future logRevenue(double usdPrice, String? sku) async { + if (sku == null || sku.isEmpty) { + return; + } + final platform = Platform.isIOS ? "appstore" : "google_play"; + GuruAnalytics.instance.logAdRevenue(usdPrice, platform, "USD"); + final productId = + GuruApp.instance.findProductId(sku: sku) ?? ProductId.invalid; + GuruAnalytics.instance.logPurchase(usdPrice, + currency: 'USD', contentId: sku, adPlatform: platform); + if (productId.isSubscription) { + GuruAnalytics.instance.logEvent( + "sub_purchase", + { + "platform": platform, + "currency": "USD", + "revenue": usdPrice, + "product_id": sku, + }, + options: iapRevenueAppEventOptions); + } else { + GuruAnalytics.instance.logEvent( + "iap_purchase", + { + "platform": platform, + "currency": "USD", + "revenue": usdPrice, + "product_id": sku, + }, + options: iapRevenueAppEventOptions); + } + GuruAnalytics.instance.logGuruEvent("dev_iap_action", + {"item_category": "reported", "item_name": sku, "result": "true"}); + } + + Future _deliverManifest(ProductId productId, Manifest manifest) async { + bool result = false; + String cause = ''; + try { + result = await ManifestManager.instance + .deliver(manifest, TransactionMethod.iap) + .catchError((error) { + Log.w("applyManifest error:$error", + syncCrashlytics: true, syncFirebase: true); + }); + } catch (error, stacktrace) { + cause = error.toString(); + } + GuruAnalytics.instance.logGuruEvent("dev_iap_action", { + "item_category": "delivered", + "item_name": productId.sku, + "mc": manifest.category, + "result": result ? "true" : "false", + 'cause': cause, + }); + } + + Future _completeOrder(OrderEntity order) async { + bool result = false; + try { + final completedOrder = order.success(); + result = await GuruDB.instance.completeOrder(order: completedOrder); + } catch (error, stacktrace) { + Log.w("_completePurchase error.$error", + stackTrace: stacktrace, syncFirebase: true, syncCrashlytics: true); + } + + GuruAnalytics.instance.logGuruEvent("dev_iap_action", { + "item_category": "complete", + "item_name": order.productId.sku, + "mc": order.manifest?.category ?? "unknown", + "result": result ? "true" : "false" + }); + final manifest = order.manifest; + if (manifest != null) { + await _deliverManifest(order.productId, manifest); + } + if (order.isAsset || order.isSubscription) { + final changedPurchasedStore = purchasedStore.clone(); + changedPurchasedStore.addAsset(Asset(order.productId, order)); + _iapStoreSubject.addEx(changedPurchasedStore); + } + return true; + } + + Future completePoints( + ProductId productId, ProductDetails productDetails, PurchaseDetails details) async { + final count = await AppProperty.getInstance().increaseAndGetIapCount(); + GuruAnalytics.instance.setUserProperty("purchase_count", count.toString()); + try { + final cost = productDetails.rawPrice; + if (cost > 0) { + final double price = cost; + if (count == 1) { + GuruAnalytics.instance.logEventEx("first_iap", + itemName: productId.sku, + value: price, + parameters: {"currency": productDetails.currencyCode}); + GuruAnalytics.instance.setUserProperty("is_iap_user", "true"); + } + GuruAnalytics.instance.logEventEx(productId.iapEventName, + itemName: productId.sku, + value: price, + parameters: {"currency": productDetails.currencyCode}); + } + } catch (error, stacktrace) { + GuruAnalytics.instance.logException(error, stacktrace: stacktrace); + } + final intent = productId.createIntent(scene: "outside_points"); + final manifest = await ManifestManager.instance.createManifest(intent); + await _deliverManifest(productId, manifest); + // 这里不需要传 order,因为 points 商品是非订阅商品 + await reportOrders(productId, productDetails, details, null); + } + + Future _completePurchase(ProductId definedProductId, + ProductDetails originProductDetails, PurchaseDetails details) async { + ProductId productId = definedProductId; + await _inAppPurchase.completePurchase(details); + final count = await AppProperty.getInstance().increaseAndGetIapCount(); + GuruAnalytics.instance.setUserProperty("purchase_count", count.toString()); + Log.d("_completePurchase $productId ${details.pendingCompletePurchase}", + tag: "Iap"); + OrderEntity? resultOrder; + + IapRequest? iapRequest = iapRequestMap.remove(productId); + if (iapRequest == null) { + final offerProductIds = GuruApp.instance.offerProductIds(productId); + for (var offerProductId in offerProductIds) { + iapRequest = iapRequestMap.remove(offerProductId); + if (iapRequest != null) { + productId = offerProductId; + break; + } + } + } + if (iapRequest != null) { + resultOrder = iapRequest.order; + final result = await _completeOrder(iapRequest.order); + iapRequest.response(result); + } else { + Log.d("Not found iapRequest for $productId"); + final orders = await GuruDB.instance.getPendingOrders(productId); + if (orders.isNotEmpty) { + orders.sort((a, b) => b.timestamp.compareTo(a.timestamp)); + await _completeOrder(orders.first); + resultOrder = orders.first; + } + } + + final productDetails = iapRequest?.product.offerDetails ?? + iapRequest?.product.details ?? + loadedProductDetails[productId] ?? + originProductDetails; + + Log.d( + "productId:$productId productDetails:${productDetails.rawPrice} originProductDetails:${originProductDetails.rawPrice}"); + + try { + // final item = pendingTransaction.product.item; + final cost = productDetails.rawPrice; + if (cost > 0) { + final double price = cost; + if (count == 1) { + GuruAnalytics.instance.logEventEx("first_iap", + itemName: productId.sku, + value: price, + parameters: {"currency": productDetails.currencyCode}); + GuruAnalytics.instance.setUserProperty("is_iap_user", "true"); + } + GuruAnalytics.instance.logEventEx(productId.iapEventName, + itemName: productId.sku, + value: price, + parameters: {"currency": productDetails.currencyCode}); + } + } catch (error, stacktrace) { + GuruAnalytics.instance.logException(error, stacktrace: stacktrace); + } + if (productId.isSubscription) { + if (resultOrder != null) { + recordSubscription(resultOrder); + } + } + if (resultOrder != null) { + reportOrders(productId, productDetails, details, resultOrder); + } + + return resultOrder; + } + + Future recordSubscription(OrderEntity order) async { + final sku = order.sku; + final manifest = order.manifest; + final productId = ProductId.fromSku( + sku: sku, + attr: TransactionAttributes.subscriptions, + basePlan: manifest?.basePlanId, + offerId: manifest?.offerId); + + final group = GuruApp.instance.appSpec.productProfile.group(productId); + + final appProperty = AppProperty.getInstance(); + + await appProperty.getAndIncrease(PropertyKeys.subscriptionCount); + if (group != null) { + await appProperty + .getAndIncrease(PropertyKeys.buildGroupSubscriptionCount(group)); + } + await appProperty + .getAndIncrease(PropertyKeys.buildSubscriptionCount(productId)); + } + + Future createPurchaseManifest(TransactionIntent intent) { + return ManifestManager.instance.createManifest(intent); + } + + Future checkAndDistributeOfferDetails(ProductId productId, + ProductDetails? details, EligibilityCriteria eligibilityCriteria) async { + Log.d("checkAndDistributeOfferDetails $productId $eligibilityCriteria"); + switch (eligibilityCriteria) { + case EligibilityCriteria.newCustomerNeverHadSubscribedThisGroup: + final group = GuruApp.instance.appSpec.productProfile.group(productId); + if (group != null) { + final key = PropertyKeys.buildGroupSubscriptionCount(group); + final count = + await AppProperty.getInstance().getInt(key, defValue: 0); + Log.d(" ==> $key $count"); + return count > 0 ? null : details; + } + Log.d(" ==> not found group($group)! return null"); + break; + case EligibilityCriteria.newCustomerNeverHadThisSubscription: + final key = PropertyKeys.buildSubscriptionCount(productId); + final count = await AppProperty.getInstance().getInt(key, defValue: 0); + Log.d(" ==> $key $count"); + return count > 0 ? null : details; + case EligibilityCriteria.newCustomerNeverHadAnySubscription: + final count = await AppProperty.getInstance() + .getInt(PropertyKeys.subscriptionCount, defValue: 0); + Log.d(" ==> subscriptionCount $count"); + return count > 0 ? null : details; + default: + return details; + } + return null; + } + + Future _createProduct( + TransactionIntent intent, ProductDetails details) async { + final productId = intent.productId; + Manifest manifest = await ManifestManager.instance.createManifest(intent); + Log.d("createProduct ${productId.sku} ${productId.hasBasePlan}"); + ProductDetails baseDetails = details; + ProductDetails? offerDetails; + if (Platform.isAndroid && + productId.isSubscription && + productId.hasBasePlan) { + final googlePlayProductDetails = details as GooglePlayProductDetails; + final productDetails = googlePlayProductDetails.productDetails; + final subscriptionOfferDetails = productDetails.subscriptionOfferDetails; + final offerProductDetails = + GooglePlayProductDetails.fromProductDetails(productDetails); + final expectBasePlan = productId.basePlan; + final expectOfferId = productId.offerId; + Log.d( + "expectOfferId:$expectOfferId expectBasePlan:$expectBasePlan offers:${offerProductDetails.length}"); + + if (expectBasePlan != null && + subscriptionOfferDetails != null && + subscriptionOfferDetails.length >= offerProductDetails.length) { + for (int i = 0; i < subscriptionOfferDetails.length; i++) { + final offer = subscriptionOfferDetails[i]; + Log.d( + "$i expectOfferId:$expectOfferId offerId:${offer.offerId} expectBasePlan:$expectBasePlan basePlanId:${offer.basePlanId}"); + if (expectBasePlan != offer.basePlanId) { + continue; + } + if (offer.offerId == null) { + baseDetails = offerProductDetails[i]; + } else if (expectOfferId != null && expectOfferId == offer.offerId) { + offerDetails = offerProductDetails[i]; + } + } + try { + offerDetails = await checkAndDistributeOfferDetails( + productId, offerDetails, intent.eligibilityCriteria); + } catch (error, stacktrace) { + Log.w("checkAndDistributeOfferDetails error! $error $stacktrace"); + } + } + } + return Product.iap(productId, baseDetails, manifest, + offerDetails: offerDetails) as IapProduct; + } + + Future> buildProducts( + Set intents) async { + ProductStore iapStore = ProductStore(); + final _productDetails = loadedProductDetails; + for (var intent in intents) { + // 这里需要使用原始 ID 进行查找 + final productId = intent.productId.originId; + final details = _productDetails[productId]; + Log.d("buildProducts $productId $details"); + if (details == null) { + continue; + } + final product = await _createProduct(intent, details); + iapStore.putProduct(product); + if (intent.productId.hasOffer && !iapStore.existsProduct(productId)) { + final originProduct = await _createProduct( + productId.createIntent(scene: intent.scene), details); + iapStore.putProduct(originProduct); + } + } + + return iapStore; + } + + Future buy(IapProduct product) async { + final productId = product.productId.originId; + final asset = purchasedStore.getAsset(productId); + if (asset != null) { + Log.v("IAP buy ${asset.productId} direct success!"); + return true; + } + final pendingProduct = iapRequestMap[productId]; + if (pendingProduct != null) { + Log.v("_requestPurchases has pending product"); + return pendingProduct.completer.future; + } + final param = PurchaseParam( + productDetails: product.offerDetails ?? product.details, + applicationUserName: AccountDataStore.instance.user?.uid); + + late OrderEntity order; + try { + order = product.createOrder(); + await GuruDB.instance.upsertOrder(order: order); + } catch (error, stacktrace) { + Log.w("addOrder error! $error", + stackTrace: stacktrace, syncCrashlytics: true, syncFirebase: true); + return false; + } + + bool result = false; + if (product.isConsumable()) { + result = await _inAppPurchase.buyConsumable(purchaseParam: param); + } else { + result = await _inAppPurchase.buyNonConsumable(purchaseParam: param); + } + if (!result) { + Log.d( + "_requestPurchases error! ${product.productId} ${product.details.price}", + syncFirebase: true); + GuruAnalytics.instance.logGuruEvent("dev_iap_action", { + "item_category": "request", + "item_name": order.productId.sku, + "mc": order.manifest?.category ?? "unknown", + "result": "false", + "cause": "buy error" + }); + await GuruDB.instance.deleteOrder(order: order); + return false; + } else { + GuruAnalytics.instance.logGuruEvent("dev_iap_action", { + "item_category": "request", + "item_name": order.productId.sku, + "mc": order.manifest?.category ?? "unknown", + "result": "true" + }); + } + final completer = Completer(); + final iapRequest = IapRequest(product, order, completer); + iapRequestMap[productId] = iapRequest; + return await completer.future; + } + + Future clearAssetRecord() async { + if (!Platform.isAndroid) { + return; + } + final InAppPurchaseAndroidPlatformAddition androidAddition = _inAppPurchase + .getPlatformAddition(); + final response = await androidAddition.queryPastPurchases(); + for (var purchase in response.pastPurchases) { + androidAddition.consumePurchase(purchase); + Log.w( + "[clearAssetRecord] purchase clear:${purchase.productID} ${purchase.verificationData}"); + _inAppPurchase.completePurchase(purchase); + } + + await GuruDB.instance.clearOrders(method: TransactionMethod.iap); + final newPurchased = AssetsStore(); + _iapStoreSubject.addEx(newPurchased); + } + + Future manualConsumePurchase(PurchaseDetails purchase) async { + if (Platform.isAndroid) { + final InAppPurchaseAndroidPlatformAddition androidAddition = + _inAppPurchase + .getPlatformAddition(); + await androidAddition.consumePurchase(purchase); + _inAppPurchase.completePurchase(purchase); + await GuruDB.instance.deletePendingOrderBySku(sku: purchase.productID); + } + } + + Future manualConsumeAllPurchases( + List> tuples) async { + for (var tuple in tuples) { + try { + final productId = tuple.item1; + final purchase = tuple.item2; + await manualConsumePurchase(purchase); + } catch (error, stacktrace) { + Log.w("consumePurchase error! $error", + stackTrace: stacktrace, syncFirebase: true); + } + } + } + + Future completeAllPurchases( + List> tuples) async { + for (var tuple in tuples) { + try { + final productId = tuple.item1; + final details = tuple.item2; + final productDetails = loadedProductDetails[productId]; + if (productDetails != null) { + if (details.pendingCompletePurchase) { + GuruAnalytics.instance.logGuruEvent("dev_iap_action", { + "item_category": "pending_complete", + "item_name": productId.sku, + "result": "true", + }); + final order = + await _completePurchase(productId, productDetails, details); + } else { + GuruAnalytics.instance.logGuruEvent("dev_iap_action", { + "item_category": "pending_consume", + "item_name": productId.sku, + "result": "true", + }); + await manualConsumePurchase(details); + if (productId.isPoints) { + try { + await completePoints(productId, productDetails, details); + } catch (error, stacktrace) { + Log.w("completePoints error! $error", stackTrace: stacktrace, syncFirebase: true); + } + } + } + } + } catch (error, stacktrace) { + Log.w("consumePurchase error! $error", + stackTrace: stacktrace, syncFirebase: true); + } + } + } + +// +// Future> refreshAllProducts() async { +// final allProductIds = ProductIds.allIapProductIds; +// +// return await refreshProducts(allProductIds); +// } + + Map _filterProductSkus( + {required Set ids, + required Set attrs, + Set? validIds}) { + final List> entries = ids + .where((productId) => + (validIds?.contains(productId) != false) && + attrs.contains(productId.attr)) + .map((productId) => MapEntry(productId.sku, productId)) + .toList(); + return Map.fromEntries(entries); + } + + Future _queryProducts(Set skus) async { + try { + return await _inAppPurchase.queryProductDetails(skus); + } catch (error, stacktrace) { + Log.i("_getProducts error:$error $stacktrace"); + } + return _emptyResponse; + } + + Future restorePurchases() async { + Log.d("restorePurchases!"); + if (!iapAvailable) { + Log.w("ignore restorePurchases! iap service not available!", tag: "IAP"); + return; + } + if (!_restorePurchase) { + _restorePurchase = Platform.isAndroid; // 只有Android需要进行处理 + return await _inAppPurchase.restorePurchases(); + } + } + + Future refreshProducts() async { + if (!iapAvailable) { + Log.w("ignore refreshProducts! iap service not available!", tag: "IAP"); + return; + } + final validIds = GuruApp.instance.productProfile.oneOffChargeIapIds.toSet() + ..removeAll(loadedProductDetails.keys.toSet()); + final queryOneOffChargeSkuMap = _filterProductSkus( + ids: GuruApp.instance.productProfile.oneOffChargeIapIds, + attrs: TransactionAttributes.oneOffChargeAttributes, + validIds: validIds); + + Log.i("refreshProduct $queryOneOffChargeSkuMap", tag: "IAP"); + final Map detailsMap = {}; + + if (queryOneOffChargeSkuMap.isEmpty) { + Log.i("refreshProducts ignore! already loaded!", tag: "IAP"); + return; + } + + final queryProductIds = queryOneOffChargeSkuMap.keys.toSet(); + queryProductIds.addAll( + GuruApp.instance.productProfile.subscriptionsIapIds.map((e) => e.sku)); + Log.d("refresh product:", tag: "IAP"); + for (var productId in queryProductIds) { + Log.d(" => $productId", tag: "IAP"); + } + final response = + await _queryProducts(queryProductIds).catchError((error, stacktrace) { + Log.e("getProducts($queryOneOffChargeSkuMap}) error: $error $stacktrace", + tag: "IAP"); + return _emptyResponse; + }); + Log.i("refreshProduct COMPLETED:", tag: "IAP"); + for (var details in response.productDetails) { + Log.i(" => ${details.id}", tag: "IAP"); + } + + Log.i("refreshProduct notFoundId:", tag: "IAP"); + for (var id in response.notFoundIDs) { + Log.i(" => $id", tag: "IAP"); + } + + for (var details in response.productDetails) { + detailsMap.addAll(extractProducts(details)); + } + + GuruAnalytics.instance.logGuruEvent( + "dev_iap_action", {"item_category": "load", "result": "true"}); + final newProductDetails = Map.of(loadedProductDetails); + newProductDetails.addAll(detailsMap); + _productDetailsSubject.addEx(newProductDetails); + } + + Map extractProducts(ProductDetails details) { + final productId = GuruApp.instance.findProductId(sku: details.id); + final Map detailsMap = {}; + if (productId == null) { + return detailsMap; + } + + detailsMap[productId] = details; + + final ids = GuruApp.instance.offerProductIds(productId); + if (ids.isNotEmpty) { + final googlePlayProductDetails = details as GooglePlayProductDetails; + final productDetails = googlePlayProductDetails.productDetails; + final subscriptionOfferDetails = productDetails.subscriptionOfferDetails; + final offerProductDetails = + GooglePlayProductDetails.fromProductDetails(productDetails); + for (var id in ids) { + final expectBasePlan = id.basePlan; + final expectOfferId = id.offerId; + if (expectBasePlan != null && + subscriptionOfferDetails != null && + subscriptionOfferDetails.length == offerProductDetails.length) { + for (int i = 0; i < subscriptionOfferDetails.length; i++) { + final offer = subscriptionOfferDetails[i]; + Log.d( + "$i expectOfferId:$expectOfferId offerId:${offer.offerId} expectBasePlan:$expectBasePlan basePlanId:${offer.basePlanId}"); + if (expectBasePlan != offer.basePlanId || + expectOfferId != offer.offerId) { + continue; + } + detailsMap[id] = offerProductDetails[i]; + } + } + } + } + return detailsMap; + } +} diff --git a/guru_app/lib/financial/iap/iap_model.dart b/guru_app/lib/financial/iap/iap_model.dart new file mode 100644 index 0000000..351a78e --- /dev/null +++ b/guru_app/lib/financial/iap/iap_model.dart @@ -0,0 +1,105 @@ +import 'dart:async'; + +import 'package:guru_app/financial/asset/assets_model.dart'; +import 'package:guru_app/financial/data/db/order_database.dart'; +import 'package:guru_app/financial/manifest/manifest.dart'; +import 'package:guru_app/financial/product/product_model.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; +import 'package:guru_utils/id/id_utils.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; + +/// Created by Haoyi on 6/2/21 + +class IapRequest { + final IapProduct product; + final OrderEntity order; + final Completer completer; + + ProductId get productId => product.productId; + + IapRequest(this.product, this.order, this.completer); + + void response(bool result) { + if (!completer.isCompleted) { + completer.complete(result); + } + } +} + +class PurchaseError implements Exception { + final String msg; + + PurchaseError(this.msg); +} + +class IapProduct implements Product { + @override + final ProductId productId; + + final ProductDetails details; + + final ProductDetails? offerDetails; + + @override + final Manifest manifest; + + String get sku => productId.sku; + + IapProduct(this.productId, this.details, this.manifest, {this.offerDetails}); + + bool isConsumable() { + return productId.isConsumable; + } + + @override + String toString() { + return 'IapProduct{productId: $productId}'; + } + + @override + OrderEntity createOrder() { + return OrderEntity( + orderId: IdUtils.uuidV4(), + sku: productId.sku, + state: TransactionState.pending, + attr: productId.attr, + method: TransactionMethod.iap.index, + currency: details.currencyCode ?? "USD", + cost: details.rawPrice, + category: manifest.category, + timestamp: DateTimeUtils.currentTimeInMillis(), + manifest: manifest); + } + +// @override +// TransactionEntity buildTransaction({String? tid}) { +// return TransactionEntity( +// tid: tid, +// sku: productId.sku, +// state: TransactionStates.pending, +// attr: productId.attr, +// method: TransactionMethods.iap, +// currency: details.currencySymbol, +// cost: details.rawPrice, +// timestamp: DateTimeUtils.currentTimeInMillis(), +// manifest: manifest); +// } +} + +class IapAsset extends Asset { + final PurchaseDetails details; + + IapAsset(ProductId productId, OrderEntity entity, this.details) : super(productId, entity); + + IapAsset copyWith({OrderEntity? order, PurchaseDetails? details}) { + return IapAsset(productId, order ?? this.order, details ?? this.details); + } + + @override + String toString() { + return 'IapPurchased{productId: $productId, item: $details}'; + } + +// @override +// Manifest? get manifest => ; +} diff --git a/guru_app/lib/financial/iap/ios/in_app_receipt_ios.dart b/guru_app/lib/financial/iap/ios/in_app_receipt_ios.dart new file mode 100644 index 0000000..c62caab --- /dev/null +++ b/guru_app/lib/financial/iap/ios/in_app_receipt_ios.dart @@ -0,0 +1,319 @@ +import 'package:guru_app/financial/product/product_model.dart'; +import 'package:guru_utils/converts/converts.dart'; +import 'package:guru_utils/log/log.dart'; + +/// Created by Haoyi on 3/4/21 +import 'package:json_annotation/json_annotation.dart'; + + +part 'in_app_receipt_ios.g.dart'; + +enum SubscriptionType { autoRenewable, nonRenewing } + +class SubscriptionPeriod { + final Duration? period; + final Duration? trial; + + @override + String toString() { + return 'SubscriptionPeriod{period: $period, trial: $trial}'; + } + + SubscriptionPeriod(this.period, this.trial); +} + +class IosReceiptStatus { + /// Not decodable status + static const unknown = -2; + + /// No status returned + static const none = -1; + + /// valid statua + static const valid = 0; + + /// The App Store could not read the JSON object you provided. + static const jsonNotReadable = 21000; + + /// The data in the receipt-data property was malformed or missing. + static const malformedOrMissingData = 21002; + + /// The receipt could not be authenticated. + static const receiptCouldNotBeAuthenticated = 21003; + + /// The shared secret you provided does not match the shared secret on file for your account. + static const secretNotMatching = 21004; + + /// The receipt server is not currently available. + static const receiptServerUnavailable = 21005; + + /// This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response. + static const subscriptionExpired = 21006; + + /// This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead. + static const testReceipt = 21007; + + /// This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead. + static const productionEnvironment = 21008; + + bool isValid(int status) { + return status == IosReceiptStatus.valid; + } +} + +class ReceiptResult { + final int status; + final int expiredDateInMillis; + // final List items; + + ReceiptResult(this.status, {this.expiredDateInMillis = 0}); + + @override + String toString() { + return 'SubscriptionResult{state: $status, expiredDateInMillis: $expiredDateInMillis}'; + } +} + +@JsonSerializable() +class ReceiptData { + @JsonKey(name: "environment") + final String? environment; + + @JsonKey(name: "receipt") + final Receipt? receipt; + + @JsonKey(name: "latest_receipt_info", defaultValue: []) + final List? latestReceiptItems; + + @JsonKey(name: "pending_renewal_info", defaultValue: []) + final List? pendingRenewalInfoItems; + + @JsonKey(name: "latest_receipt", defaultValue: "") + final String? latestReceipt; + + @JsonKey(name: "status") + final int status; + + int get requestDateInMillis => receipt?.requestDateInMillis ?? 0; + + ReceiptData({this.environment, this.receipt, this.latestReceiptItems, this.pendingRenewalInfoItems, this.latestReceipt, required this.status}); + + factory ReceiptData.fromJson(Map json) => _$ReceiptDataFromJson(json); + + Map toJson() => _$ReceiptDataToJson(this); + + dumpLog() { + Log.d("========= RECEIPT DATE ========"); + Log.d(" [status] => $status"); + Log.d(" [receipt begin] ========>"); + receipt?.dumpLog(); + Log.d(" <======== [receipt end] "); + Log.d(" [latestReceiptItems] => ${latestReceiptItems?.length}"); + Log.d(" [pendingRenewalInfoItems] => ${pendingRenewalInfoItems?.length}"); + Log.d(" [latestReceipt] => $latestReceipt"); + Log.d("========= RECEIPT DATE ========"); + } + + List getCheckReceipts({SubscriptionType type = SubscriptionType.autoRenewable, List checkIds = const []}) { + final List receipts = []; + final Set platformCheckIds = checkIds.map((id) => id.sku).toSet(); + switch (type) { + case SubscriptionType.nonRenewing: + if (receipt?.inAppReceiptItems != null && receipt!.inAppReceiptItems.isNotEmpty) { + receipts.addAll(receipt!.inAppReceiptItems); + } + break; + default: + if (latestReceiptItems != null && latestReceiptItems!.isNotEmpty) { + receipts.addAll(latestReceiptItems!); + } + break; + } + dumpLog(); + Log.d("[validate]==> receipts.length:${receipts.length}"); + if (checkIds.isNotEmpty == true) { + return receipts.where((receipt) { + Log.d("[validate]==> productId:${receipt.productId} ${platformCheckIds.contains(receipt.productId)}"); + return platformCheckIds.contains(receipt.productId); + }).toList(); + } + return []; + } +} + +@JsonSerializable() +class Receipt { + @JsonKey(name: "receipt_type") + final String receiptType; + + @JsonKey(name: "adam_id") + final int adamId; + + @JsonKey(name: "app_item_id") + final int appItemId; + + @JsonKey(name: "bundle_id") + final String bundleId; + + @JsonKey(name: "application_version") + final String applicationVersion; + + @JsonKey(name: "download_id") + final int downloadId; + + @JsonKey(name: "version_external_identifier") + final int versionExternalIdentifier; + + @JsonKey(name: "receipt_creation_date_ms") + @intStringConvert + final int receiptCreationDateInMillis; + + @JsonKey(name: "request_date_ms") + @intStringConvert + final int requestDateInMillis; + + @JsonKey(name: "original_purchase_date_ms") + @intStringConvert + final int originalPurchaseDateInMillis; + + @JsonKey(name: "original_application_version") + final String originalApplicationVersion; + + @JsonKey(name: "in_app", defaultValue: []) + final List inAppReceiptItems; + + Receipt( + {required this.receiptType, + required this.adamId, + required this.appItemId, + required this.bundleId, + required this.applicationVersion, + required this.downloadId, + required this.versionExternalIdentifier, + required this.receiptCreationDateInMillis, + required this.requestDateInMillis, + required this.originalPurchaseDateInMillis, + required this.originalApplicationVersion, + required this.inAppReceiptItems}); + + factory Receipt.fromJson(Map json) => _$ReceiptFromJson(json); + + Map toJson() => _$ReceiptToJson(this); + + void dumpLog() { + Log.d(" [receiptType] => $receiptType"); + Log.d(" [adamId] => $adamId"); + Log.d(" [appItemId] => $appItemId"); + Log.d(" [bundleId] => $bundleId"); + Log.d(" [applicationVersion] => $applicationVersion"); + Log.d(" [downloadId] => $downloadId"); + Log.d(" [versionExternalIdentifier] => $versionExternalIdentifier"); + Log.d(" [receiptCreationDateInMillis] => $receiptCreationDateInMillis"); + Log.d(" [requestDateInMillis] => $requestDateInMillis"); + Log.d(" [originalPurchaseDateInMillis] => $originalPurchaseDateInMillis"); + Log.d(" [originalApplicationVersion] => $originalApplicationVersion"); + Log.d(" [inAppReceiptItems] => ${inAppReceiptItems.length}"); + } +} + +@JsonSerializable() +class ReceiptItem { + @JsonKey(name: "product_id") + final String productId; + + @JsonKey(name: "quantity") + @intStringConvert + final int quantity; + + @JsonKey(name: "transaction_id") + final String transactionId; + + @JsonKey(name: "original_transaction_id") + final String originalTransactionId; + + @JsonKey(name: "purchase_date_ms") + @intStringConvert + final int purchaseDateInMillis; + + @JsonKey(name: "expires_date_ms") + @intStringConvert + final int? expiresDateInMillis; + + @JsonKey(name: "expires_date") + final String? expiresDateInString; + + @JsonKey(name: "original_purchase_date_ms") + @intStringConvert + final int originalPurchaseDateInMillis; + + @JsonKey(name: "web_order_line_item_id") + final String? webOrderLineItemId; + + @JsonKey(name: "is_trial_period") + @boolStringConvert + final bool isTrialPeriod; + + @JsonKey(name: "is_in_intro_offer_period") + @boolStringConvert + final bool? isInIntroOfferPeriod; + + @JsonKey(name: "subscription_group_identifier") + final String? subscriptionGroupIdentifier; + + @JsonKey(name: "cancellation_date") + final String? cancellationDate; + + ReceiptItem( + {required this.productId, + required this.quantity, + required this.transactionId, + required this.originalTransactionId, + required this.purchaseDateInMillis, + required this.expiresDateInMillis, + required this.originalPurchaseDateInMillis, + required this.webOrderLineItemId, + required this.isTrialPeriod, + required this.isInIntroOfferPeriod, + required this.subscriptionGroupIdentifier, + required this.cancellationDate, + this.expiresDateInString}); + + factory ReceiptItem.fromJson(Map json) => _$ReceiptItemFromJson(json); + + Map toJson() => _$ReceiptItemToJson(this); +} + +@JsonSerializable() +class PendingRenewalInfo { + // @JsonKey(name: "expiration_intent") + // @INT_STRING_CONVERT + // final int expirationIntent; + + @JsonKey(name: "auto_renew_product_id") + final String autoRenewProductId; + + @JsonKey(name: "original_transaction_id") + final String originalTransactionId; + + // @JsonKey(name: "is_in_billing_retry_period") + // @INT_STRING_CONVERT + // final int isInBillingRetryPeriod; + + @JsonKey(name: "product_id") + final String productId; + + @JsonKey(name: "auto_renew_status") + @intStringConvert + final int autoRenewStatus; + + PendingRenewalInfo( + { + required this.autoRenewProductId, + required this.originalTransactionId, + required this.productId, + required this.autoRenewStatus}); + + factory PendingRenewalInfo.fromJson(Map json) => _$PendingRenewalInfoFromJson(json); + + Map toJson() => _$PendingRenewalInfoToJson(this); +} diff --git a/guru_app/lib/financial/iap/ios/in_app_receipt_ios.g.dart b/guru_app/lib/financial/iap/ios/in_app_receipt_ios.g.dart new file mode 100644 index 0000000..d100d43 --- /dev/null +++ b/guru_app/lib/financial/iap/ios/in_app_receipt_ios.g.dart @@ -0,0 +1,146 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'in_app_receipt_ios.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ReceiptData _$ReceiptDataFromJson(Map json) => ReceiptData( + environment: json['environment'] as String?, + receipt: json['receipt'] == null + ? null + : Receipt.fromJson(json['receipt'] as Map), + latestReceiptItems: (json['latest_receipt_info'] as List?) + ?.map((e) => ReceiptItem.fromJson(e as Map)) + .toList() ?? + [], + pendingRenewalInfoItems: (json['pending_renewal_info'] as List?) + ?.map( + (e) => PendingRenewalInfo.fromJson(e as Map)) + .toList() ?? + [], + latestReceipt: json['latest_receipt'] as String? ?? '', + status: json['status'] as int, + ); + +Map _$ReceiptDataToJson(ReceiptData instance) => + { + 'environment': instance.environment, + 'receipt': instance.receipt, + 'latest_receipt_info': instance.latestReceiptItems, + 'pending_renewal_info': instance.pendingRenewalInfoItems, + 'latest_receipt': instance.latestReceipt, + 'status': instance.status, + }; + +Receipt _$ReceiptFromJson(Map json) => Receipt( + receiptType: json['receipt_type'] as String, + adamId: json['adam_id'] as int, + appItemId: json['app_item_id'] as int, + bundleId: json['bundle_id'] as String, + applicationVersion: json['application_version'] as String, + downloadId: json['download_id'] as int, + versionExternalIdentifier: json['version_external_identifier'] as int, + receiptCreationDateInMillis: + intStringConvert.fromJson(json['receipt_creation_date_ms'] as String), + requestDateInMillis: + intStringConvert.fromJson(json['request_date_ms'] as String), + originalPurchaseDateInMillis: intStringConvert + .fromJson(json['original_purchase_date_ms'] as String), + originalApplicationVersion: + json['original_application_version'] as String, + inAppReceiptItems: (json['in_app'] as List?) + ?.map((e) => ReceiptItem.fromJson(e as Map)) + .toList() ?? + [], + ); + +Map _$ReceiptToJson(Receipt instance) => { + 'receipt_type': instance.receiptType, + 'adam_id': instance.adamId, + 'app_item_id': instance.appItemId, + 'bundle_id': instance.bundleId, + 'application_version': instance.applicationVersion, + 'download_id': instance.downloadId, + 'version_external_identifier': instance.versionExternalIdentifier, + 'receipt_creation_date_ms': + intStringConvert.toJson(instance.receiptCreationDateInMillis), + 'request_date_ms': intStringConvert.toJson(instance.requestDateInMillis), + 'original_purchase_date_ms': + intStringConvert.toJson(instance.originalPurchaseDateInMillis), + 'original_application_version': instance.originalApplicationVersion, + 'in_app': instance.inAppReceiptItems, + }; + +ReceiptItem _$ReceiptItemFromJson(Map json) => ReceiptItem( + productId: json['product_id'] as String, + quantity: intStringConvert.fromJson(json['quantity'] as String), + transactionId: json['transaction_id'] as String, + originalTransactionId: json['original_transaction_id'] as String, + purchaseDateInMillis: + intStringConvert.fromJson(json['purchase_date_ms'] as String), + expiresDateInMillis: _$JsonConverterFromJson( + json['expires_date_ms'], intStringConvert.fromJson), + originalPurchaseDateInMillis: intStringConvert + .fromJson(json['original_purchase_date_ms'] as String), + webOrderLineItemId: json['web_order_line_item_id'] as String?, + isTrialPeriod: + boolStringConvert.fromJson(json['is_trial_period'] as String), + isInIntroOfferPeriod: _$JsonConverterFromJson( + json['is_in_intro_offer_period'], boolStringConvert.fromJson), + subscriptionGroupIdentifier: + json['subscription_group_identifier'] as String?, + cancellationDate: json['cancellation_date'] as String?, + expiresDateInString: json['expires_date'] as String?, + ); + +Map _$ReceiptItemToJson(ReceiptItem instance) => + { + 'product_id': instance.productId, + 'quantity': intStringConvert.toJson(instance.quantity), + 'transaction_id': instance.transactionId, + 'original_transaction_id': instance.originalTransactionId, + 'purchase_date_ms': + intStringConvert.toJson(instance.purchaseDateInMillis), + 'expires_date_ms': _$JsonConverterToJson( + instance.expiresDateInMillis, intStringConvert.toJson), + 'expires_date': instance.expiresDateInString, + 'original_purchase_date_ms': + intStringConvert.toJson(instance.originalPurchaseDateInMillis), + 'web_order_line_item_id': instance.webOrderLineItemId, + 'is_trial_period': boolStringConvert.toJson(instance.isTrialPeriod), + 'is_in_intro_offer_period': _$JsonConverterToJson( + instance.isInIntroOfferPeriod, boolStringConvert.toJson), + 'subscription_group_identifier': instance.subscriptionGroupIdentifier, + 'cancellation_date': instance.cancellationDate, + }; + +Value? _$JsonConverterFromJson( + Object? json, + Value? Function(Json json) fromJson, +) => + json == null ? null : fromJson(json as Json); + +Json? _$JsonConverterToJson( + Value? value, + Json? Function(Value value) toJson, +) => + value == null ? null : toJson(value); + +PendingRenewalInfo _$PendingRenewalInfoFromJson(Map json) => + PendingRenewalInfo( + autoRenewProductId: json['auto_renew_product_id'] as String, + originalTransactionId: json['original_transaction_id'] as String, + productId: json['product_id'] as String, + autoRenewStatus: + intStringConvert.fromJson(json['auto_renew_status'] as String), + ); + +Map _$PendingRenewalInfoToJson(PendingRenewalInfo instance) => + { + 'auto_renew_product_id': instance.autoRenewProductId, + 'original_transaction_id': instance.originalTransactionId, + 'product_id': instance.productId, + 'auto_renew_status': intStringConvert.toJson(instance.autoRenewStatus), + }; diff --git a/guru_app/lib/financial/iap/ios/receipt_validator_ios.dart b/guru_app/lib/financial/iap/ios/receipt_validator_ios.dart new file mode 100644 index 0000000..77a70b9 --- /dev/null +++ b/guru_app/lib/financial/iap/ios/receipt_validator_ios.dart @@ -0,0 +1,110 @@ +import 'dart:convert'; + +import 'package:guru_app/guru_app.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; + +import 'in_app_receipt_ios.dart'; + +import 'package:http/http.dart' as http; + +import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; + +/// Created by Haoyi on 3/5/21 +/// + +class IosReceiptValidator { + final AppStorePurchaseDetails purchasedDetails; + + ReceiptData? _receiptData; + + IosReceiptValidator(this.purchasedDetails); + + Future _validateReceipt(bool isSandbox) async { + Log.d("[validate] isSandbox:$isSandbox"); + + final iosValidateReceiptPassword = + GuruApp.instance.appSpec.deployment.iosValidateReceiptPassword; + if (iosValidateReceiptPassword == null) { + return null; + } + + final Map headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }; + final Map receiptBody = { + "receipt-data": purchasedDetails.verificationData.serverVerificationData, + "password": iosValidateReceiptPassword + }; + + final String url = isSandbox + ? 'https://sandbox.itunes.apple.com/verifyReceipt' + : 'https://buy.itunes.apple.com/verifyReceipt'; + + final body = jsonEncode(receiptBody); + + try { + final response = await http.post(Uri.parse(url), headers: headers, body: body); + final json = jsonDecode(response.body); + return ReceiptData.fromJson(json); + } catch (error, stacktrace) { + return null; + } + } + + Future validate() async { + final receiptData = await _validateReceipt(false); + if (receiptData?.environment == "Sandbox" || receiptData?.status == 21007) { + return _validateReceipt(true); + } + return receiptData; + } + + Duration? _getReceiptDuration(SubscriptionType type, + {Duration duration = const Duration(seconds: 0)}) { + switch (type) { + case SubscriptionType.nonRenewing: + return duration; + default: + return null; + } + } + +// List _sortedReceipts(List items, {Duration? duration}) { +// final result = List.of(items); +// if (duration == null) { +// result.sort((a, b) => b.expiresDateInMillis.compareTo(a.expiresDateInMillis)); +// return result; +// } +// return result; +// } + +// Future verifySubscriptions(SubscriptionType type) async { +// final receiptData = await validate(); +// final duration = _getReceiptDuration(type); +// final checkReceipts = +// receiptData.getCheckReceipts(type: type, checkIds: ProductIds.iapPremiumIds); +// final nonCancelledReceipts = checkReceipts.where((receipt) => receipt.cancellationDate == null); +// if (nonCancelledReceipts.length > checkReceipts.length) { +// print( +// "[validate]==> receipt has ${nonCancelledReceipts.length} items, but only ${checkReceipts.length} were parsed"); +// } +// final sortedReceipts = _sortedReceipts(checkReceipts, duration: duration); +// +// if (sortedReceipts.isEmpty) { +// print("[validate]==> sortedReceipts is Empty"); +// return ReceiptResult(PurchaseStatus.notPurchased); +// } +// +// final firstReceiptItem = sortedReceipts[0]; +// print( +// "[validate]==> firstReceiptItem:${firstReceiptItem.expiresDateInMillis} request:${receiptData.requestDateInMillis}"); +// if (firstReceiptItem.expiresDateInMillis > receiptData.requestDateInMillis) { +// return ReceiptResult(PurchaseStatus.purchased, items: sortedReceipts); +// } else { +// return ReceiptResult(PurchaseStatus.expired, +// expiredDateInMillis: firstReceiptItem.expiresDateInMillis, items: sortedReceipts); +// } +// } +// } +} \ No newline at end of file diff --git a/guru_app/lib/financial/igc/igc_manager.dart b/guru_app/lib/financial/igc/igc_manager.dart new file mode 100644 index 0000000..13dbcab --- /dev/null +++ b/guru_app/lib/financial/igc/igc_manager.dart @@ -0,0 +1,166 @@ +import 'package:guru_app/analytics/guru_analytics.dart'; +import 'package:guru_app/database/guru_db.dart'; +import 'package:guru_app/financial/asset/assets_model.dart'; +import 'package:guru_app/financial/asset/assets_store.dart'; +import 'package:guru_app/financial/data/db/order_database.dart'; +import 'package:guru_app/financial/igc/igc_model.dart'; +import 'package:guru_app/financial/manifest/manifest_manager.dart'; +import 'package:guru_app/financial/product/product_model.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:guru_app/property/app_property.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/property/app_property.dart'; + +/// Created by Haoyi on 2023/2/18 + +class IgcManager { + static final IgcManager _instance = IgcManager._(); + + static IgcManager get instance => _instance; + + final BehaviorSubject _balanceSubject = BehaviorSubject.seeded(0); + + final BehaviorSubject> _assetStoreSubject = + BehaviorSubject.seeded(AssetsStore.inactive()); + + Stream> get observableAssetStore => _assetStoreSubject.stream; + + AssetsStore get purchasedStore => _assetStoreSubject.value; + + int get currentBalance => _balanceSubject.value; + + Stream get observableCurrentBalance => _balanceSubject.stream; + + final CompositeSubscription subscriptions = CompositeSubscription(); + + IgcManager._(); + + Future init() async { + final attachedGems = (await AppProperty.getInstance().isFirstUseGemsFeature()) + ? GuruApp.instance.appSpec.deployment.initIgc + : 0; + final _balance = await AppProperty.getInstance().accumulateIgc(attachedGems); + _balanceSubject.addEx(_balance); + GuruAnalytics.instance.setUserProperty("coin", _balance.toString()); + + final iapIgc = await AppProperty.getInstance().getIapIgc(); + GuruAnalytics.instance.setUserProperty("iap_coin", iapIgc.toString()); + + final noIapIgcGems = await AppProperty.getInstance().getNoIapIgc(); + GuruAnalytics.instance.setUserProperty("noniap_coin", noIapIgcGems.toString()); + await reloadAssets(); + } + + Future reloadAssets() async { + final orders = await GuruDB.instance + .selectOrders(method: TransactionMethod.igc, attrs: [TransactionAttributes.asset]); + final newAssetStore = AssetsStore(); + for (var order in orders) { + final productId = order.productId; + Log.v("init order:${order.sku} $productId"); + newAssetStore.addAsset(Asset(productId, order)); + } + _assetStoreSubject.addEx(newAssetStore); + } + + Future clearAssets({String? category, TransactionMethod? method}) async { + await GuruDB.instance.clearOrders(category: category, method: method); + final newAssetStore = purchasedStore.clone()..clearAsset(category: category, method: method); + _assetStoreSubject.addEx(newAssetStore); + } + + Future accumulate(int igc, TransactionMethod method, {String? scene}) async { + try { + int newBalance = await AppProperty.getInstance().accumulateIgc(igc); + try { + if (method == TransactionMethod.iap) { + final iapIgc = await AppProperty.getInstance().accumulateIapIgc(igc); + await GuruAnalytics.instance.setUserProperty("iap_coin", iapIgc.toString()); + } else { + final noniapIgc = await AppProperty.getInstance().accumulateNoIapIgc(igc); + await GuruAnalytics.instance.setUserProperty("noniap_coin", noniapIgc.toString()); + } + await GuruAnalytics.instance.setUserProperty("coin", newBalance.toString()); + } catch (throwable, stacktrace) { + Log.w("accumulate error $throwable", syncFirebase: true, syncCrashlytics: true); + } + _balanceSubject.add(newBalance); + + GuruAnalytics.instance.logEarnVirtualCurrency( + virtualCurrencyName: "coin", + method: scene ?? convertTransactionMethodName(method), + balance: newBalance, + value: igc); + return true; + } catch (error, stacktrace) { + Log.v("accumulate error:$error $stacktrace"); + } + return false; + } + + Future clear() async { + final result = await AppProperty.getInstance().clearAllIgc(); + if (result) { + _balanceSubject.add(0); + } + } + + Future buildIgcProduct(TransactionIntent intent) async { + final manifest = await ManifestManager.instance.createManifest(intent); + return IgcProduct(intent.productId, manifest, intent.igcCost); + } + + Future purchase(IgcProduct product) async { + Log.v("Igc buy"); + + final purchasedItem = purchasedStore.getAsset(product.productId); + if (purchasedItem != null) { + Log.v("Coin buy ${purchasedItem.productId} direct success!"); + return true; + } + return _requestPurchase(product); + } + + Future _requestPurchase(IgcProduct product) async { + if (currentBalance < product.cost || product.cost < 0) { + Log.v("_requestPurchase error! $currentBalance price:${product.cost}"); + return false; + } + try { + final int newBalance = await AppProperty.getInstance().consumeIgc(product.cost); + await GuruAnalytics.instance.setUserProperty("coin", newBalance.toString()); + + if (currentBalance != newBalance) { + _balanceSubject.add(newBalance); + } + + if (product.cost != 0) { + GuruAnalytics.instance.logSpendCredits( + product.productId.sku, product.manifest.category, product.cost, + virtualCurrencyName: "coin", balance: newBalance, scene: product.manifest.scene); + } + + if (!product.productId.isConsumable) { + final order = product.createOrder(); + await GuruDB.instance.upsertOrder(order: order).catchError((error) { + Log.v("upsertOrder error!$error"); + return false; + }); + final newPurchasedStore = purchasedStore.clone(); + newPurchasedStore.addAsset(Asset(product.productId, order)); + _assetStoreSubject.addEx(newPurchasedStore); + } + return true; + } catch (error, stacktrace) { + Log.v("error $error, $stacktrace"); + return false; + } + } + + void dispose() { + // _productStoreSubject.close(); + _assetStoreSubject.close(); + _balanceSubject.close(); + } +} diff --git a/guru_app/lib/financial/igc/igc_model.dart b/guru_app/lib/financial/igc/igc_model.dart new file mode 100644 index 0000000..d3fae18 --- /dev/null +++ b/guru_app/lib/financial/igc/igc_model.dart @@ -0,0 +1,47 @@ +/// Created by Haoyi on 2023/2/18 + +import 'package:guru_app/financial/data/db/order_database.dart'; +import 'package:guru_app/financial/manifest/manifest.dart'; +import 'package:guru_app/financial/product/product_model.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_utils/id/id_utils.dart'; + +/// Created by Haoyi on 2023/2/13 + +class IgcProduct implements Product { + @override + final ProductId productId; + + @override + final Manifest manifest; + + final int cost; + + String get sku => productId.sku; + + IgcProduct(this.productId, this.manifest, this.cost); + + bool isConsumable() { + return productId.isConsumable; + } + + @override + String toString() { + return 'IgcProduct{productId: $productId}'; + } + + @override + OrderEntity createOrder() { + return OrderEntity( + orderId: IdUtils.uuidV4(), + sku: productId.sku, + state: TransactionState.success, + attr: productId.attr, + method: TransactionMethod.igc.index, + currency: TransactionCurrency.igc, + cost: cost.toDouble(), + category: manifest.category, + timestamp: DateTimeUtils.currentTimeInMillis(), + manifest: manifest); + } +} diff --git a/guru_app/lib/financial/manifest/manifest.dart b/guru_app/lib/financial/manifest/manifest.dart new file mode 100644 index 0000000..7c6d681 --- /dev/null +++ b/guru_app/lib/financial/manifest/manifest.dart @@ -0,0 +1,17 @@ + + +/// Created by Haoyi on 2023/1/24 +export 'package:guru_utils/manifest/manifest.dart'; +export 'manifest_manager.dart'; +// +// class ReservedManifestFactory { +// static Future buildNoBannerAndInterstitialAds(TransactionIntent intent) async { +// if (GuruApp.instance.productProfile.noAdsCapIds.contains(intent.productId)) { +// final details = Details.define(DetailsReservedType.noAds, 1); +// return Manifest("no_ads", details: [details]); +// } +// return null; +// } +// +// static List builders = [buildNoBannerAndInterstitialAds]; +// } diff --git a/guru_app/lib/financial/manifest/manifest_manager.dart b/guru_app/lib/financial/manifest/manifest_manager.dart new file mode 100644 index 0000000..935344d --- /dev/null +++ b/guru_app/lib/financial/manifest/manifest_manager.dart @@ -0,0 +1,77 @@ +import 'dart:async'; + +import 'package:guru_app/financial/igc/igc_manager.dart'; +import 'package:guru_app/financial/product/product_model.dart'; +import 'manifest.dart'; + +/// Created by Haoyi on 2022/8/21 + +typedef DetailsDistributor = Future Function(Details, TransactionMethod, String scene); + +typedef ManifestBuilder = Future Function(TransactionIntent); + +class ManifestManager { + ManifestManager._() { + observableDeliveredManifest = deliveredManifestStream.stream.asBroadcastStream(); + } + + final StreamController deliveredManifestStream = StreamController(); + + static final ManifestManager instance = ManifestManager._(); + + late Stream observableDeliveredManifest; + + final Map distributors = { + DetailsReservedType.igc: _deliverIgcDetails + }; + + final List builders = []; + + static Future _deliverIgcDetails( + Details details, TransactionMethod method, String scene) async { + if (details.amount > 0) { + IgcManager.instance.accumulate(details.amount, method, scene: scene); + return true; + } + return false; + } + + void addDistributor(String type, DetailsDistributor distributor) { + distributors[type] = distributor; + } + + void addBuilder(ManifestBuilder builder) { + builders.add(builder); + } + + void addBuilders(List builders) { + this.builders.addAll(builders); + } + + Future deliver(Manifest manifest, TransactionMethod method) async { + bool result = false; + for (var details in manifest.details) { + result |= await distributors[details.type]?.call(details, method, manifest.scene) ?? false; + } + deliveredManifestStream.add(manifest); + return result; + } + + Future createManifest(TransactionIntent intent) async { + for (var builder in builders) { + final manifest = await builder(intent); + if (manifest != null) { + return manifest; + } + } + return Manifest.empty; + } + + Manifest createIgcManifest(int igc, {String? category, String scene = ""}) { + final details =
[]; + details.add(Details.define(DetailsReservedType.igc, igc)); + + final extras = {ExtraReservedField.scene: scene}; + return Manifest(category ?? DetailsReservedType.igc, extras: extras, details: details); + } +} diff --git a/guru_app/lib/financial/product/ids/product_ids.dart b/guru_app/lib/financial/product/ids/product_ids.dart new file mode 100644 index 0000000..f8d2459 --- /dev/null +++ b/guru_app/lib/financial/product/ids/product_ids.dart @@ -0,0 +1,145 @@ +// /// Created by Haoyi on 2021/7/1 +// +// part of "../product_model.dart"; +// +// class ProductProfile { +// final List oneOffChargeIapIds = []; +// final List subscriptionsIapIds = []; +// final List noAdsCapIds; +// +// final List igcIds = []; +// final List rewardIds = []; +// +// final List> _idsMap = +// List.generate(TransactionAttributes.count, (index) => {}); +// +// ProductProfile( +// {required List oneOffChargeIapIds, +// required List subscriptionsIapIds, +// List igcIds = const [], +// List rewardIds = const [], +// this.noAdsCapIds = const []}) { +// for (var productId in oneOffChargeIapIds) { +// _define(productId, TransactionMethod.iap); +// } +// for (var productId in subscriptionsIapIds) { +// _define(productId, TransactionMethod.iap); +// } +// for (var productId in igcIds) { +// _define(productId, TransactionMethod.igc); +// } +// for (var productId in rewardIds) { +// _define(productId, TransactionMethod.reward); +// } +// } +// +// bool hasIap() => oneOffChargeIapIds.isEmpty && subscriptionsIapIds.isEmpty; +// +// ProductId _define(ProductId productId, TransactionMethod method) { +// switch (method) { +// case TransactionMethod.iap: +// if (productId.isOneOffCharge) { +// oneOffChargeIapIds.add(productId); +// } else if (productId.isSubscription) { +// subscriptionsIapIds.add(productId); +// } +// break; +// case TransactionMethod.igc: +// igcIds.add(productId); +// break; +// case TransactionMethod.reward: +// rewardIds.add(productId); +// break; +// case TransactionMethod.none: +// break; +// } +// _idsMap[productId.attr][productId.sku] = productId; +// return productId; +// } +// +// ProductId findOrCreate(String sku, int attr, TransactionMethod method) { +// return _find(sku, attr) ?? _define(ProductId(android: sku, ios: sku, attr: attr), method); +// } +// +// ProductId? _find(String sku, int attr) { +// return _idsMap[attr][sku]; +// } +// +// ProductId? find({String? sku, int? attr}) { +// if (sku == null) { +// return null; +// } +// +// if (attr != null) { +// return _find(sku, attr); +// } else { +// return _find(sku, TransactionAttributes.possessive) ?? +// _find(sku, TransactionAttributes.subscriptions) ?? +// _find(sku, TransactionAttributes.consumable); +// } +// } +// } +// +// class IapProfile { +// final List oneOffChargeIapIds = []; +// final List subscriptionsIapIds = []; +// final List noAdsCapIds; +// final List> _idsMap = +// List.generate(TransactionAttributes.count, (index) => {}); +// +// IapProfile( +// {required List oneOffChargeIapIds, +// required List subscriptionsIapIds, +// this.noAdsCapIds = const []}) { +// for (var productId in oneOffChargeIapIds) { +// _define(productId); +// } +// for (var productId in subscriptionsIapIds) { +// _define(productId); +// } +// } +// +// bool hasIap() => oneOffChargeIapIds.isEmpty && subscriptionsIapIds.isEmpty; +// +// static final IapProfile invalid = +// IapProfile(oneOffChargeIapIds: [], subscriptionsIapIds: [], noAdsCapIds: []); +// +// ProductId _define(ProductId productId) { +// if (productId.isOneOffCharge) { +// oneOffChargeIapIds.add(productId); +// } else if (productId.isSubscription) { +// subscriptionsIapIds.add(productId); +// } else { +// return productId; +// } +// _idsMap[productId.attr][productId.sku] = productId; +// return productId; +// } +// +// ProductId findOrCreate(String sku, int attr) { +// return _find(sku, attr) ?? _define(ProductId(android: sku, ios: sku, attr: attr)); +// } +// +// ProductId? _find(String sku, int attr) { +// return _idsMap[attr][sku]; +// } +// +// ProductId? find({String? sku, int? attr}) { +// if (sku == null) { +// return null; +// } +// +// if (attr != null) { +// return _find(sku, attr); +// } else { +// return _find(sku, TransactionAttributes.possessive) ?? +// _find(sku, TransactionAttributes.subscriptions) ?? +// _find(sku, TransactionAttributes.consumable); +// } +// } +// } +// +// class ProductIds { +// static const ProductId invalid = +// ProductId(android: "", attr: TransactionAttributes.unknown, ios: ""); +// } diff --git a/guru_app/lib/financial/product/product_model.dart b/guru_app/lib/financial/product/product_model.dart new file mode 100644 index 0000000..560aebf --- /dev/null +++ b/guru_app/lib/financial/product/product_model.dart @@ -0,0 +1,259 @@ +import 'dart:io'; + +import 'package:guru_app/financial/data/db/order_database.dart'; +import 'package:guru_app/financial/manifest/manifest.dart'; +import 'package:guru_app/financial/manifest/manifest_manager.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:guru_utils/hash/hash.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; +import 'package:guru_app/financial/iap/iap_model.dart'; +import 'package:guru_app/financial/igc/igc_model.dart'; +import 'package:guru_app/financial/reward/reward_model.dart'; + +part 'product_profile.dart'; + +/// Created by Haoyi on 6/1/21 + +class OrderMethods { + static const free = 0; + static const gem = 1; // 宝石购买 + static const iap = 2; // IAP购买 + static const reward = 3; // 奖励收获 + static const limit = 4; + + static String toAbbrevText(int method) { + switch (method) { + case free: + return "free"; + case gem: + return "gem"; + case iap: + return "iap"; + case reward: + return "reward"; + case limit: + return "limit"; + default: + return ""; + } + } +} + +class OrderStates { + static const init = 0; + static const success = 1; + static const pending = -1; + static const error = -2; +} + +class TransactionAttributes { + static const unknown = 0; + + // Offer products for sale in your app for a one-off charge + @deprecated + static const possessive = 1; + static const asset = 1; + static const consumable = 2; + + static const Set oneOffChargeAttributes = {asset, consumable}; + + // Subscriptions are in-app content or services that are billed to users on a recurring basis + static const subscriptions = 10; + + static const count = 11; +} + +class TransactionCurrency { + static const free = "_FVC"; // Free Virtual Currency + static const igc = "_IGC"; // In-Game Virtual Currency + static const reward = "_RVC"; // Reward Virtual Currency +} + +// 这里的配置有别于 GP 上的配置,由于 GP 上对应的配置无法满足我们的需求,所以我们需要自己定义一套配置 +enum EligibilityCriteria { + // 这里对应的是 GP 上的 NEW CUSTOMER,但是新用户的判断是以APP的生命周期为标准, + // 没有购买过此商品组的新用户(默认选项) + newCustomerNeverHadSubscribedThisGroup, + + // 这里对应的是 GP 上的 NEW CUSTOMER,但是新用户的判断是以APP的生命周期为标准, + // 没有购买过此商品的新用户 + newCustomerNeverHadThisSubscription, + + // 这里对应的是 GP 上的 NEW CUSTOMER,但是新用户的判断是以APP的生命周期为标准, + // 没有购买过任何订阅商品的新用户 + newCustomerNeverHadAnySubscription, + + // 依赖 ProductId 进行筛选 + dependencyProductId +} + +class ProductId { + final String android; + final String ios; + final int attr; + + // android only + final String? basePlan; + final String? offerId; + final bool points; + + final ProductId? _originId; + + bool get isConsumable => attr == TransactionAttributes.consumable; + + bool get hasOffer => + Platform.isAndroid && basePlan?.isNotEmpty == true && offerId?.isNotEmpty == true; + + bool get hasBasePlan => Platform.isAndroid && basePlan?.isNotEmpty == true; + + static final _iapEventRegExp = RegExp(r'^.*\.[ia]\.'); + + ProductId get originId => _originId ?? this; + + @override + String toString() { + if (hasOffer) { + return 'ProductId{sku: $sku, basePlan: $basePlan, offerId: $offerId}'; + } + return 'ProductId{android: $android, ios: $ios, attr: $attr, points:$points}'; + } + + @deprecated + bool get isPossessive => attr == TransactionAttributes.asset; + + bool get isAsset => attr == TransactionAttributes.asset; + + bool get isPoints => points; + + bool get isOneOffCharge => + (attr == TransactionAttributes.consumable) || (attr == TransactionAttributes.asset); + + bool get isSubscription => attr == TransactionAttributes.subscriptions; + + String get iapEventName => sku.replaceFirst(_iapEventRegExp, "iap.").replaceAll(".", "_"); + + String get sku => Platform.isIOS ? ios : android; + + static const ProductId invalid = + ProductId.fromSku(sku: "", attr: TransactionAttributes.consumable); + + const ProductId( + {required this.android, + required this.ios, + required this.attr, + this.basePlan, + this.offerId, + this.points = false, + ProductId? originId}) + : _originId = originId; + + const ProductId.fromSku( + {required String sku, required this.attr, this.basePlan, this.offerId, this.points = false}) + : android = sku, + ios = sku, + _originId = null; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ProductId && + runtimeType == other.runtimeType && + sku == other.sku && + attr == other.attr && + basePlan == other.basePlan && + offerId == other.offerId && + points == points; + + @override + int get hashCode => hashObjects([sku, attr, basePlan ?? '', offerId ?? '', points]); + + bool isValid() { + return sku.isNotEmpty; + } + + TransactionIntent createIntent( + {required String scene, + int igcCost = 0, + bool sales = false, + double rate = 1.0, + EligibilityCriteria eligibilityCriteria = + EligibilityCriteria.newCustomerNeverHadSubscribedThisGroup}) { + return TransactionIntent(this, scene, + igcCost: igcCost, sales: sales, rate: rate, eligibilityCriteria: eligibilityCriteria); + } + + Future createRewardProduct(String scene) async { + final intent = createIntent(scene: scene); + final manifest = await ManifestManager.instance.createManifest(intent); + return RewardProduct(this, manifest); + } + + Future createIgcProduct(int igcCost, String scene) async { + final intent = createIntent(scene: scene, igcCost: igcCost); + final manifest = await ManifestManager.instance.createManifest(intent); + return IgcProduct(this, manifest, igcCost); + } +} + +abstract class Product { + ProductId get productId; + + Manifest get manifest; + + factory Product.iap(ProductId productId, ProductDetails details, Manifest manifest, + {ProductDetails? offerDetails}) = IapProduct; + + factory Product.igc(ProductId productId, Manifest manifest, int cost) = IgcProduct; + + factory Product.reward(ProductId productId, Manifest manifest) = RewardProduct; + +// +// factory Product.gems(ProductId productId, int price, Manifest manifest) = GemProduct; +// +// factory Product.reward(Reward reward) = RewardProduct; +// + OrderEntity createOrder(); +} + +class TransactionState { + static const init = 0; + static const success = 1; + static const pending = -1; + static const error = -2; + static const expired = -3; +} + +enum TransactionMethod { + iap, // IAP购买 + igc, // In-game currency 购买 + reward, // 奖励获得 + none +} + +String convertTransactionMethodName(TransactionMethod method) { + switch (method) { + case TransactionMethod.iap: + return "iap_buy"; + case TransactionMethod.igc: + return "igc"; + case TransactionMethod.reward: + return "reward"; + default: + return "none"; + } +} + +class TransactionIntent { + final ProductId productId; + final int igcCost; + final String scene; // 购买场景(最终会用到logEarnVirtualCurrency的item_category上) + final bool sales; // 是否为促销商品 + final double rate; // 默认1.0 指收益率,如:打折促销时设成1.2,最终的收益将是1.2倍 + final EligibilityCriteria eligibilityCriteria; + + TransactionIntent(this.productId, this.scene, + {this.igcCost = 0, + this.sales = false, + this.rate = 1.0, + this.eligibilityCriteria = EligibilityCriteria.newCustomerNeverHadSubscribedThisGroup}); +} diff --git a/guru_app/lib/financial/product/product_profile.dart b/guru_app/lib/financial/product/product_profile.dart new file mode 100644 index 0000000..24cb77e --- /dev/null +++ b/guru_app/lib/financial/product/product_profile.dart @@ -0,0 +1,168 @@ +/// Created by Haoyi on 2021/7/1 + +part of "product_model.dart"; + +class ProductProfile { + final Set oneOffChargeIapIds = {}; + final Set subscriptionsIapIds = {}; + final Set pointsIapIds = {}; + final Set noAdsCapIds; + + final Set iapIds = {}; + final Set igcIds = {}; + final Set rewardIds = {}; + + final Map groupMap; + + final List manifestBuilders = []; + + final Map> _offerIds = {}; + + final List> _idsMap = + List.generate(TransactionAttributes.count, (index) => {}); + + ProductProfile({required Set oneOffChargeIapIds, + required Set subscriptionsIapIds, + Set pointsIapIds = const {}, + Set igcIds = const {}, + Set rewardIds = const {}, + this.groupMap = const {}, + List manifestBuilders = const [], + this.noAdsCapIds = const {}}) { + for (var productId in oneOffChargeIapIds) { + _define(productId, TransactionMethod.iap); + } + for (var productId in subscriptionsIapIds) { + _define(productId, TransactionMethod.iap); + } + for (var productId in igcIds) { + _define(productId, TransactionMethod.igc); + } + for (var productId in rewardIds) { + _define(productId, TransactionMethod.reward); + } + this.pointsIapIds.addAll(pointsIapIds); + this.manifestBuilders.addAll(manifestBuilders); + } + + bool hasIap() => oneOffChargeIapIds.isNotEmpty; + + bool hasSubs() => subscriptionsIapIds.isNotEmpty; + + ProductId _define(ProductId productId, TransactionMethod method) { + ProductId definedProductId = productId; + switch (method) { + case TransactionMethod.iap: + if (productId.isOneOffCharge) { + oneOffChargeIapIds.add(productId); + } else if (productId.isSubscription) { + definedProductId = productId.originId; + subscriptionsIapIds.add(definedProductId); + if (productId.hasBasePlan) { + (_offerIds[productId.sku] ??= {}).add(productId); + } + } + iapIds.add(definedProductId); + break; + case TransactionMethod.igc: + igcIds.add(definedProductId); + break; + case TransactionMethod.reward: + rewardIds.add(definedProductId); + break; + case TransactionMethod.none: + break; + } + _idsMap[productId.attr][productId.sku] = definedProductId; + return productId; + } + + ProductId define(String sku, int attr, TransactionMethod method) { + final productId = _find(sku, attr) ?? ProductId.fromSku(sku: sku, attr: attr); + return _define(productId, method); + } + + ProductId? _find(String sku, int attr) { + return _idsMap[attr][sku]; + } + + ProductId? find({String? sku, int? attr}) { + if (sku == null || sku == "") { + return null; + } + + if (attr != null) { + return _find(sku, attr); + } else { + return _find(sku, TransactionAttributes.asset) ?? + _find(sku, TransactionAttributes.subscriptions) ?? + _find(sku, TransactionAttributes.consumable); + } + } + + Set offerProductIds(ProductId productId) { + return _offerIds[productId.sku] ?? {}; + } + + String? group(ProductId productId) { + return groupMap[productId.sku]; + } +} + +class IapProfile { + final List oneOffChargeIapIds = []; + final List subscriptionsIapIds = []; + final List noAdsCapIds; + final List> _idsMap = + List.generate(TransactionAttributes.count, (index) => {}); + + IapProfile({required List oneOffChargeIapIds, + required List subscriptionsIapIds, + this.noAdsCapIds = const []}) { + for (var productId in oneOffChargeIapIds) { + _define(productId); + } + for (var productId in subscriptionsIapIds) { + _define(productId.originId); + } + } + + bool hasIap() => oneOffChargeIapIds.isEmpty && subscriptionsIapIds.isEmpty; + + static final IapProfile invalid = + IapProfile(oneOffChargeIapIds: [], subscriptionsIapIds: [], noAdsCapIds: []); + + ProductId _define(ProductId productId) { + if (productId.isOneOffCharge) { + oneOffChargeIapIds.add(productId); + } else if (productId.isSubscription) { + subscriptionsIapIds.add(productId); + } else { + return productId; + } + _idsMap[productId.attr][productId.sku] = productId; + return productId; + } + + ProductId findOrCreate(String sku, int attr) { + return _find(sku, attr) ?? _define(ProductId(android: sku, ios: sku, attr: attr)); + } + + ProductId? _find(String sku, int attr) { + return _idsMap[attr][sku]; + } + + ProductId? find({String? sku, int? attr}) { + if (sku == null) { + return null; + } + + if (attr != null) { + return _find(sku, attr); + } else { + return _find(sku, TransactionAttributes.possessive) ?? + _find(sku, TransactionAttributes.subscriptions) ?? + _find(sku, TransactionAttributes.consumable); + } + } +} diff --git a/guru_app/lib/financial/product/product_store.dart b/guru_app/lib/financial/product/product_store.dart new file mode 100644 index 0000000..e54cac9 --- /dev/null +++ b/guru_app/lib/financial/product/product_store.dart @@ -0,0 +1,82 @@ +import 'package:guru_app/financial/product/product_model.dart'; + +/// Created by Haoyi on 6/1/21 + +class ProductStore { + final Map data = {}; + + ProductStore(); + + void putProduct(T item) { + data[item.productId] = item; + } + + void putAllProducts(List items) { + for (var item in items) { + putProduct(item); + } + } + + List getProducts(List ids) { + final result = []; + for (var id in ids) { + final item = getProduct(id); + if (item != null) { + result.add(item); + } + } + return result; + } + + T? getProduct(ProductId productId) { + return productId.isValid() ? data[productId] : null; + } + + bool existsProduct(ProductId productId) { + return productId.isValid() == true && data.containsKey(productId); + } + + bool existsProducts(List productIds) { + for (var productId in productIds) { + if (existsProduct(productId)) { + return true; + } + } + return false; + } + + T? getFirstProduct(List productIds) { + for (var productId in productIds) { + final product = getProduct(productId); + if (product != null) { + return product; + } + } + return null; + } + + // Map> filterUncertaintyProductIds(List productIds) { + // final uncertaintyIds = >{}; + // + // for (ProductId productId in productIds) { + // final id = productId.platformProductId; + // if (!data.containsKey(id)) { + // IapType iapType = IapType.Product; + // if (PurchaseUtils.isSubscriptionProductId(id)) { + // iapType = IapType.Subscription; + // } + // List ids = uncertaintyIds[iapType]; + // if (ids == null) { + // ids = []; + // uncertaintyIds[iapType] = ids; + // } + // ids.add(id); + // } + // } + // return uncertaintyIds; + // } + + ProductStore clone() { + return ProductStore()..data.addAll(data); + } +} diff --git a/guru_app/lib/financial/reward/reward_manager.dart b/guru_app/lib/financial/reward/reward_manager.dart new file mode 100644 index 0000000..85a5347 --- /dev/null +++ b/guru_app/lib/financial/reward/reward_manager.dart @@ -0,0 +1,69 @@ +import 'package:guru_app/database/guru_db.dart'; +import 'package:guru_app/financial/asset/assets_model.dart'; +import 'package:guru_app/financial/asset/assets_store.dart'; +import 'package:guru_app/financial/data/db/order_database.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_model.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:guru_utils/extensions/extensions.dart'; + +/// Created by Haoyi on 2023/2/13 + +class RewardManager { + static final RewardManager _instance = RewardManager._(); + + static RewardManager get instance => _instance; + + RewardManager._(); + + final BehaviorSubject> _assetsStoreSubject = + BehaviorSubject.seeded(AssetsStore.inactive()); + + AssetsStore get rewardedStore => _assetsStoreSubject.value; + + Stream> get observableAssetStore => _assetsStoreSubject.stream; + + Future init() async { + await reloadAssets(); + } + + Future reloadAssets() async { + final transactions = await GuruDB.instance.selectOrders( + method: TransactionMethod.reward, attrs: [TransactionAttributes.asset]); + final newAssetsStore = AssetsStore(); + for (var transaction in transactions) { + final productId = transaction.productId; + Log.v("init [Rewards] transaction:${transaction.sku} $productId"); + newAssetsStore.addAsset(Asset(productId, transaction)); + } + _assetsStoreSubject.addEx(newAssetsStore); + } + + Future buildRewardProduct(TransactionIntent intent) async { + final manifest = await ManifestManager.instance.createManifest(intent); + return RewardProduct(intent.productId, manifest); + } + + Future claim(RewardProduct product, {String from = ""}) async { + Log.v("rewarded"); + // 如果得到的奖励是可消耗的物品(金币,Joker等),这里将直接领取成功 + if (product.productId.isConsumable) { + ManifestManager.instance.deliver(product.manifest, TransactionMethod.reward); + return true; + } + + final order = product.createOrder(); + final result = await GuruDB.instance.upsertOrder(order: order).catchError((error, stacktrace) { + Log.v("refreshTransaction error!$error $stacktrace"); + return false; + }); + if (result) { + final newAssetsStore = rewardedStore.clone(); + newAssetsStore.addAsset(Asset(product.productId, order)); + _assetsStoreSubject.addEx(newAssetsStore); + } + ManifestManager.instance.deliver(product.manifest, TransactionMethod.reward); + return result; + } +} diff --git a/guru_app/lib/financial/reward/reward_model.dart b/guru_app/lib/financial/reward/reward_model.dart new file mode 100644 index 0000000..9c330c7 --- /dev/null +++ b/guru_app/lib/financial/reward/reward_model.dart @@ -0,0 +1,44 @@ +import 'package:guru_app/financial/asset/assets_model.dart'; +import 'package:guru_app/financial/data/db/order_database.dart'; +import 'package:guru_app/financial/manifest/manifest.dart'; +import 'package:guru_app/financial/product/product_model.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_utils/id/id_utils.dart'; + +/// Created by Haoyi on 2023/2/13 + +class RewardProduct implements Product { + @override + final ProductId productId; + + @override + final Manifest manifest; + + String get sku => productId.sku; + + RewardProduct(this.productId, this.manifest); + + bool isConsumable() { + return productId.isConsumable; + } + + @override + String toString() { + return 'IapProduct{productId: $productId}'; + } + + @override + OrderEntity createOrder() { + return OrderEntity( + orderId: IdUtils.uuidV4(), + sku: productId.sku, + state: TransactionState.success, + attr: productId.attr, + method: TransactionMethod.reward.index, + currency: TransactionCurrency.reward, + cost: 0, + category: manifest.category, + timestamp: DateTimeUtils.currentTimeInMillis(), + manifest: manifest); + } +} diff --git a/guru_app/lib/firebase/dxlinks/dxlink_manager.dart b/guru_app/lib/firebase/dxlinks/dxlink_manager.dart new file mode 100644 index 0000000..1965cfe --- /dev/null +++ b/guru_app/lib/firebase/dxlinks/dxlink_manager.dart @@ -0,0 +1,65 @@ +import 'package:firebase_dynamic_links/firebase_dynamic_links.dart'; +import 'package:flutter/services.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:guru_navigator/guru_navigator.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/router/router.dart'; +import 'package:guru_utils/uri/uri_utils.dart'; + +/// Created by Haoyi on 5/20/21 + +class DxLinkManager { + static final DxLinkManager instance = DxLinkManager._(); + + DxLinkManager._(); + + void initDynamicLinks() async { + FirebaseDynamicLinks.instance.onLink.listen((PendingDynamicLinkData? dynamicLink) async { + final Uri? deepLink = dynamicLink?.link; + Log.d("### onDynamicLink $deepLink"); + if (deepLink != null) { + _openLink(deepLink); + } + }, onError: (e) async { + Log.w('onLinkError ${e.message} ${e.stacktrace}', error: e); + }); + + final PendingDynamicLinkData? data = + await FirebaseDynamicLinks.instance.getInitialLink().catchError((error, stacktrace) { + Log.d("getInitialLink error:$error $stacktrace"); + }); + final Uri? deepLink = data?.link; + Log.d("initDynamicLinks: $deepLink"); + if (deepLink != null) { + Future.delayed(const Duration(seconds: 2), () => _openLink(deepLink)); + } + } + + Future _handleDeeplink(MethodCall call) async { + Log.d("call.method: ${call.method} arguments:${call.arguments}"); + switch (call.method) { + case "navigate": + final uri = Uri.parse(call.arguments["uri"]); + if ((uri.authority.isNotEmpty != true) || + uri.toString().contains(GuruApp.instance.appSpec.details.authority)) { + _openLink(uri); + } else { + UriUtils.launchURL(uri); + } + } + return true; + } + + void initDeeplink() async { + GuruNavigator.init(_handleDeeplink); + } + + void init() async { + initDeeplink(); + initDynamicLinks(); + } + + void _openLink(Uri uri) { + RouteCenter.instance.dispatchUri(uri); + } +} diff --git a/guru_app/lib/firebase/firebase.dart b/guru_app/lib/firebase/firebase.dart new file mode 100644 index 0000000..4e7cd2a --- /dev/null +++ b/guru_app/lib/firebase/firebase.dart @@ -0,0 +1,7 @@ +library guru_firebase; + +/// Created by Haoyi on 2022/8/31 +export 'remoteconfig/remote_config_manager.dart'; +export 'messaging/remote_messaging_manager.dart'; +export 'package:firebase_messaging/firebase_messaging.dart'; + diff --git a/guru_app/lib/firebase/firestore/account/account_extension.dart b/guru_app/lib/firebase/firestore/account/account_extension.dart new file mode 100644 index 0000000..8ac701c --- /dev/null +++ b/guru_app/lib/firebase/firestore/account/account_extension.dart @@ -0,0 +1,31 @@ +/// Created by Haoyi on 2021/7/28 + +part of '../firestore_manager.dart'; + +extension AccountExtension on FirestoreManager { + String get userCollection { + if (kReleaseMode) { + return "users"; + } else { + return "test_users"; + } + } + + Future modifyProfile(Map modifyJson) async { + final uid = AccountDataStore.instance.uid; + if (uid == null || modifyJson.isEmpty) { + Log.i("modifyProfile error! uid is null!"); + return null; + } + modifyJson.remove(AccountProfile.dirtyField); + // if (Settings.instance.debugMode.get() != true) { + await FirebaseFirestore.instance + .collection(userCollection) + .doc(uid) + .set(modifyJson, SetOptions(merge: true /*mergeFields: modifyJson.keys.toList()*/)); + // } + modifyJson[AccountProfile.dirtyField] = false; + return AccountDataStore.instance.accountProfile?.merge(modifyJson) ?? + AccountProfile.fromJson(modifyJson); + } +} diff --git a/guru_app/lib/firebase/firestore/firestore_manager.dart b/guru_app/lib/firebase/firestore/firestore_manager.dart new file mode 100644 index 0000000..2a89af0 --- /dev/null +++ b/guru_app/lib/firebase/firestore/firestore_manager.dart @@ -0,0 +1,15 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/foundation.dart'; +import 'package:guru_app/account/account_data_store.dart'; +import 'package:guru_app/account/model/account_profile.dart'; +import 'package:guru_utils/log/log.dart'; + +/// Created by Haoyi on 2022/9/1 + +part 'account/account_extension.dart'; + +class FirestoreManager { + static final FirestoreManager instance = FirestoreManager._(); + + FirestoreManager._(); +} diff --git a/guru_app/lib/firebase/messaging/remote_messaging_manager.dart b/guru_app/lib/firebase/messaging/remote_messaging_manager.dart new file mode 100644 index 0000000..7924d21 --- /dev/null +++ b/guru_app/lib/firebase/messaging/remote_messaging_manager.dart @@ -0,0 +1,453 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui'; + +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/services.dart'; +import 'package:guru_app/account/account_manager.dart'; +import 'package:guru_app/analytics/guru_analytics.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:guru_app/lifecycle/lifecycle_model.dart'; +import 'package:guru_app/property/app_property.dart'; +import 'package:guru_app/property/property_keys.dart'; +import 'package:guru_utils/controller/lifecycle_controller.dart'; +import 'package:guru_utils/lifecycle/lifecycle_manager.dart'; +import 'package:guru_utils/router/router.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/math/math_utils.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:json_annotation/json_annotation.dart'; + +/// Created by Haoyi on 5/14/21 +// +Future _backgroundMessageHandler(RemoteMessage message) async { + Log.d("_backgroundMessageHandler:${message.data} ${message.data["uri"]}"); + return; +} + +enum RationaleResult { skip, allow } + +enum PromptTrigger { + @JsonValue(0) + rationale, // 依赖Android原生的shouldShowRequestRationale返回值来展示对应的Rationale页面 + @JsonValue(1) + request // 依赖请求的次数来展示对应的Rationale页面 +} + +class RemoteMessagingManager { + static RemoteMessagingManager instance = RemoteMessagingManager._(); + + late FirebaseMessaging _firebaseMessaging; + + final BehaviorSubject fcmToken = BehaviorSubject.seeded(null); + + Stream get observableFCMToken => fcmToken.stream; + + RemoteMessagingManager._(); + + int _retryFetchTokenCount = 0; + + final _statusMap = { + AuthorizationStatus.authorized: "granted", + AuthorizationStatus.denied: "denied", + AuthorizationStatus.provisional: "provisional", + AuthorizationStatus.notDetermined: "not_determined" + }; + + String? _getUriLastSegment(String? uri) { + try { + return Uri.parse(uri ?? "").pathSegments.last; + } catch (error) { + return null; + } + } + + void fetchToken({Completer? completer}) async { + Log.d("Fetch FCMToken!!"); + String? token; + try { + token = await _firebaseMessaging.getToken(); + if (token != null) { + fcmToken.addEx(token); + // RuntimeProperty.instance.setString("firebase_push_token", token); + Log.d("### FCMToken :$token"); + } + } catch (error, stacktrace) { + Log.d("fetchToken error!", error: error, stackTrace: stacktrace); + } + if (token == null || token == '') { + final intervalSeconds = + (MathUtils.fibonacci(_retryFetchTokenCount) * 8).clamp(8, 600); + Future.delayed(Duration(seconds: intervalSeconds), () { + fetchToken(); + }); + _retryFetchTokenCount++; + } else { + _retryFetchTokenCount = 0; + completer?.complete(Future.value(token)); + } + } + + Future getToken() async { + final result = fcmToken.value ?? (await _firebaseMessaging.getToken()); + if (result != null && fcmToken.value == null) { + fcmToken.addEx(result); + } + return result; + } + + void init() async { + _firebaseMessaging = FirebaseMessaging.instance; + final granted = await checkNotificationPermission(); + if (!granted) { + Future.delayed(const Duration(seconds: 8), () async { + if (GuruApp + .instance.appSpec.deployment.autoRequestNotificationPermission) { + Log.d("guru_app auto request notification permissions!"); + requestNotificationPermission(); + } else { + Log.d("guru_app check notification permissions!"); + final shouldShowRequestRationale = + await Permission.notification.shouldShowRequestRationale; + Log.d( + "guru_app post request notification permission event! shouldShowRequestRationale:$shouldShowRequestRationale"); + LifecycleManager.instance.postEvent( + RequestNotificationPermissionEvent( + rationale: shouldShowRequestRationale)); + } + }); + } + + FirebaseMessaging.instance.getInitialMessage().then((message) { + if (message != null) { + final uri = message.data["uri"]; + if (uri != null && uri is String && uri.isNotEmpty) { + Log.d("getInitialMessage:${message.data} $uri"); + RouteCenter.instance.dispatchUri(Uri.parse(uri)); + } + } + }); + FirebaseMessaging.onMessage.listen((message) { + Log.d("onMessage:${message.data}"); + final data = message.data; + + // final notification = message.notification; + // if (Platform.isAndroid && notification != null) { + // NotificationChannel.showNotification( + // NotificationChannel.pushType, {"title": notification.title, "body": notification.body, "cmd": data["cmd"], "uri": data["uri"]}); + // } + GuruAnalytics.instance.logEventEx("push_receive", + itemCategory: data["cmd"], itemName: _getUriLastSegment(data["uri"])); + }); + + FirebaseMessaging.onMessageOpenedApp.listen((message) { + final uri = message.data["uri"]; + if (uri != null && uri is String && uri.isNotEmpty) { + Log.d("onMessageOpenApp:${message.data} ${message.data["uri"]}"); + RouteCenter.instance.dispatchUri(Uri.parse(message.data["uri"])); + } + }); + _firebaseMessaging.onTokenRefresh.listen((event) { + Log.d("onTokenRefresh $event"); + AccountManager.instance.refreshFcmToken(); + // Injector.provide().refreshFcmToken(); + }); + fetchToken(); + } + + Future checkNotificationPermission() async { + final _map = { + AuthorizationStatus.authorized: "granted", + AuthorizationStatus.denied: "denied", + AuthorizationStatus.provisional: "provisional", + AuthorizationStatus.notDetermined: "not_determined" + }; + final notificationSettings = + await _firebaseMessaging.getNotificationSettings(); + final property = _map[notificationSettings.authorizationStatus]; + if (property != null) { + GuruAnalytics.instance.setUserProperty("noti_perm", property); + } else { + GuruAnalytics.instance.setUserProperty("noti_perm", "not_determined"); + } + return notificationSettings.authorizationStatus == + AuthorizationStatus.authorized; + } + + Future getNotificationAuthorizationStatus() async { + final notificationSettings = + await _firebaseMessaging.getNotificationSettings(); + return notificationSettings.authorizationStatus; + } + + Future isShouldShowRequestRationale() async { + if (GuruApp + .instance.appSpec.deployment.notificationPermissionPromptTrigger == + PromptTrigger.rationale) { + return await Permission.notification.shouldShowRequestRationale; + } + + if (await Permission.notification.isGranted) { + return false; + } + final permanentlyDenied = await Permission.notification.isPermanentlyDenied; + if (permanentlyDenied) { + return false; + } + int deniedTimes = await AppProperty.getInstance() + .getInt(PropertyKeys.deniedNotificationPermissionTimes, defValue: 0); + if (deniedTimes >= 2) { + return false; + } + + final requestTimes = await AppProperty.getInstance() + .getInt(PropertyKeys.requestNotificationPermissionTimes, defValue: 0); + return requestTimes >= 1; + } + + Future _requestNotificationPermissionForAndroid( + {String style = "default", + String scene = "", + Completer Function()? showRationale}) async { + final PromptTrigger promptTrigger = + GuruApp.instance.appSpec.deployment.notificationPermissionPromptTrigger; + + if (await Permission.notification.isGranted) { + GuruAnalytics.instance.setUserProperty("noti_perm", "granted"); + return true; + } else { + final permanentlyDenied = + await Permission.notification.isPermanentlyDenied; + if (permanentlyDenied) { + GuruAnalytics.instance.setUserProperty("noti_perm", "denied"); + return false; + } + int deniedTimes = await AppProperty.getInstance() + .getInt(PropertyKeys.deniedNotificationPermissionTimes, defValue: 0); + if (deniedTimes >= 2) { + GuruAnalytics.instance.setUserProperty("noti_perm", "denied"); + return false; + } + + final promptTriggerValue = + promptTrigger == PromptTrigger.rationale ? "a" : "b"; + + final requestTimes = await AppProperty.getInstance().increaseAndGet( + PropertyKeys.requestNotificationPermissionTimes, + defValue: 0); + + final trackingNotificationPermissionPass = GuruApp + .instance.appSpec.deployment.trackingNotificationPermissionPass && + requestTimes < + (GuruApp.instance.appSpec.deployment + .trackingNotificationPermissionPassLimitTimes); + + if (trackingNotificationPermissionPass) { + GuruAnalytics.instance.logEventEx("noti_perm_req_$requestTimes", + itemCategory: style, + itemName: scene, + parameters: { + "request_times": requestTimes, + "denied_times": deniedTimes, + "prompt_trigger": promptTriggerValue + }); + } + + final shouldShowRequestRationale = + await Permission.notification.shouldShowRequestRationale || + (promptTrigger == PromptTrigger.request && requestTimes > 1); + + Log.d( + "_requestNotificationPermission requestTimes:$requestTimes deniedTimes:$deniedTimes trackingNotificationPermissionPass:$trackingNotificationPermissionPass promptTrigger:$promptTrigger shouldShowRequestRationale:$shouldShowRequestRationale "); + + if (shouldShowRequestRationale && showRationale != null) { + GuruAnalytics.instance.logEventEx("noti_perm_rationale_imp", + itemCategory: style, itemName: scene); + RationaleResult rationaleResult = RationaleResult.skip; + try { + final completer = showRationale(); + rationaleResult = await completer.future; + } catch (error, stacktrace) { + Log.d("showRationale error!", error: error, stackTrace: stacktrace); + } + GuruAnalytics.instance.logEventEx("noti_perm_rationale_result", + itemCategory: style, + itemName: scene, + parameters: { + "result": + rationaleResult == RationaleResult.allow ? "allow" : "skip", + }); + if (rationaleResult == RationaleResult.skip) { + return false; + } + } + + final showTimes = await AppProperty.getInstance().increaseAndGet( + PropertyKeys.showNotificationPermissionTimes, + defValue: 0); + + GuruAnalytics.instance.logEventEx("noti_perm_imp", + itemCategory: style, + itemName: scene, + parameters: { + "show_times": showTimes, + "request_times": requestTimes, + "denied_times": deniedTimes, + "prompt_trigger": promptTriggerValue + }); + final requestSettings = await _firebaseMessaging.requestPermission(); + final result = + _statusMap[requestSettings.authorizationStatus] ?? "not_determined"; + await GuruAnalytics.instance.setUserProperty("noti_perm", result); + + if (requestSettings.authorizationStatus != + AuthorizationStatus.authorized) { + final shouldShowRequestRationale2 = + await Permission.notification.shouldShowRequestRationale; + if (deniedTimes == 0 && shouldShowRequestRationale2) { + deniedTimes = 1; + await AppProperty.getInstance() + .setInt(PropertyKeys.deniedNotificationPermissionTimes, 1); + } else if (deniedTimes == 1 && + shouldShowRequestRationale != shouldShowRequestRationale2) { + deniedTimes = 2; + await AppProperty.getInstance() + .setInt(PropertyKeys.deniedNotificationPermissionTimes, 2); + } + } else { + if (trackingNotificationPermissionPass) { + GuruAnalytics.instance.logEventEx("noti_perm_pass_$requestTimes", + itemCategory: style, + itemName: scene, + parameters: { + "show_times": showTimes, + "request_times": requestTimes, + "denied_times": deniedTimes, + "prompt_trigger": promptTriggerValue + }); + } + } + + GuruAnalytics.instance.logEventEx("noti_perm_result", + itemCategory: style, + itemName: scene, + parameters: { + "result": result, + "show_times": showTimes, + "request_times": requestTimes, + "denied_times": deniedTimes, + "prompt_trigger": promptTriggerValue + }); + + Log.d( + "notificationSettings.authorizationStatus:${requestSettings.authorizationStatus} showTimes:$requestTimes deniedTimes:$deniedTimes promptTrigger: $promptTrigger"); + return requestSettings.authorizationStatus == + AuthorizationStatus.authorized; + } + } + + Future _requestNotificationPermissionForIOS( + {String style = "default", String scene = ""}) async { + final status = await getNotificationAuthorizationStatus(); + switch (status) { + case AuthorizationStatus.authorized: + GuruAnalytics.instance.setUserProperty("noti_perm", "granted"); + return true; + case AuthorizationStatus.provisional: + GuruAnalytics.instance.setUserProperty("noti_perm", "provisional"); + return true; + case AuthorizationStatus.denied: + GuruAnalytics.instance.setUserProperty("noti_perm", "denied"); + return false; + default: + break; + } + + final trackingNotificationPermissionPass = + GuruApp.instance.appSpec.deployment.trackingNotificationPermissionPass; + + int deniedTimes = await AppProperty.getInstance() + .getInt(PropertyKeys.deniedNotificationPermissionTimes, defValue: 0); + + final requestTimes = await AppProperty.getInstance().increaseAndGet( + PropertyKeys.requestNotificationPermissionTimes, + defValue: 0); + final showTimes = await AppProperty.getInstance().increaseAndGet( + PropertyKeys.showNotificationPermissionTimes, + defValue: 0); + + if (trackingNotificationPermissionPass) { + GuruAnalytics.instance.logEventEx("noti_perm_req_$requestTimes", + itemCategory: style, + itemName: scene, + parameters: { + "request_times": requestTimes, + "denied_times": deniedTimes, + "prompt_trigger": "a" + }); + } + + final requestSettings = await _firebaseMessaging.requestPermission(); + final result = + _statusMap[requestSettings.authorizationStatus] ?? "not_determined"; + await GuruAnalytics.instance.setUserProperty("noti_perm", result); + + if (requestSettings.authorizationStatus != AuthorizationStatus.authorized) { + deniedTimes += 1; + } else { + if (trackingNotificationPermissionPass) { + GuruAnalytics.instance.logEventEx("noti_perm_pass_$requestTimes", + itemCategory: style, + itemName: scene, + parameters: { + "show_times": showTimes, + "request_times": requestTimes, + "denied_times": deniedTimes, + "prompt_trigger": "a" + }); + } + } + + GuruAnalytics.instance.logEventEx("noti_perm_result", + itemCategory: style, + itemName: scene, + parameters: { + "result": result, + "show_times": showTimes, + "request_times": requestTimes, + "denied_times": deniedTimes, + "prompt_trigger": "a" + }); + Log.d( + "notificationSettings.authorizationStatus:${requestSettings.authorizationStatus} showTimes:$requestTimes deniedTimes:$deniedTimes"); + return requestSettings.authorizationStatus == + AuthorizationStatus.authorized; + } + + Future requestNotificationPermission( + {String style = "default", + String scene = "", + Completer Function()? showRationale}) async { + if (Platform.isAndroid) { + return _requestNotificationPermissionForAndroid( + style: style, scene: scene, showRationale: showRationale); + } else if (Platform.isIOS) { + return _requestNotificationPermissionForIOS(style: style, scene: scene); + } + return false; + } + + void saveTokenToClipboard() { + final token = fcmToken.value; + if (token != null) { + Clipboard.setData(ClipboardData(text: token)); + Log.d("saveTokenToClipboard:$token"); + } + } + + void dispose() { + fcmToken.close(); + } +} diff --git a/guru_app/lib/firebase/remoteconfig/remote_config_interface.dart b/guru_app/lib/firebase/remoteconfig/remote_config_interface.dart new file mode 100644 index 0000000..1e37007 --- /dev/null +++ b/guru_app/lib/firebase/remoteconfig/remote_config_interface.dart @@ -0,0 +1,112 @@ +/// Created by Haoyi on 2022/2/26 + +part of "remote_config_manager.dart"; + +extension RemoteConfigInterface on RemoteConfigManager { + bool isIOSReview() { + if (Platform.isIOS) { + return getBool(RemoteConfigReservedConstants.iosReviewVersion, defaultValue: true) ?? true; + } else { + return false; + } + } + + CdnConfig getCdnConfig() { + final cdnConfigStr = getString(RemoteConfigReservedConstants.cdnConfig); + if (cdnConfigStr != null && cdnConfigStr.isNotEmpty) { + return CdnConfig.fromJson( + jsonDecode(cdnConfigStr), + defaultStoragePrefix: GuruApp.instance.details.storagePrefix, + defaultCdnPrefix: GuruApp.instance.details.defaultCdnPrefix, + ); + } + return CdnConfig.fromJson( + {}, + defaultStoragePrefix: GuruApp.instance.details.storagePrefix, + defaultCdnPrefix: GuruApp.instance.details.defaultCdnPrefix, + ); + } + + TaichiConfig? getTaichiConfig() { + final taichiStr = + RemoteConfigManager.instance.getString(RemoteConfigReservedConstants.taichiConfig); + if (taichiStr != null && taichiStr.isNotEmpty) { + return TaichiConfig.fromJson(jsonDecode(taichiStr)); + } + return null; + } + + AdInterstitialConfig getIadsConfig() { + final iadsConfigStr = + RemoteConfigManager.instance.getString(RemoteConfigReservedConstants.iadsConfig); + if (iadsConfigStr != null && iadsConfigStr.isNotEmpty) { + return AdInterstitialConfig.fromJson(jsonDecode(iadsConfigStr)); + } + return AdInterstitialConfig.fromJson({}); + } + + AdRewardedConfig getRadsConfig() { + final radsConfigStr = + RemoteConfigManager.instance.getString(RemoteConfigReservedConstants.radsConfig); + if (radsConfigStr != null && radsConfigStr.isNotEmpty) { + return AdRewardedConfig.fromJson(jsonDecode(radsConfigStr)); + } + return AdRewardedConfig.fromJson({}); + } + + AdBannerConfig getBadsConfig() { + final badsConfigStr = + RemoteConfigManager.instance.getString(RemoteConfigReservedConstants.badsConfig); + if (badsConfigStr != null && badsConfigStr.isNotEmpty) { + return AdBannerConfig.fromJson(jsonDecode(badsConfigStr)); + } + return AdBannerConfig.fromJson({}); + } + + StrategyAdsConfig getStrategyAdsConfig() { + final sadsConfigStr = + RemoteConfigManager.instance.getString(RemoteConfigReservedConstants.sadsConfig); + if (sadsConfigStr != null && sadsConfigStr.isNotEmpty) { + return StrategyAdsConfig.fromJson(jsonDecode(sadsConfigStr)); + } + return StrategyAdsConfig.fromJson({}); + } + + IOSAttConfig getIOSAttConfig() { + final iosAttConfigString = + RemoteConfigManager.instance.getString(RemoteConfigReservedConstants.iosAttConfig); + + if (iosAttConfigString != null && iosAttConfigString.isNotEmpty) { + return IOSAttConfig.fromJson(jsonDecode(iosAttConfigString)); + } + return IOSAttConfig.fromJson({}); + } + + CommonAdsConfig getCommonAdsConfig() { + final commonAdsConfigString = + RemoteConfigManager.instance.getString(RemoteConfigReservedConstants.commonAdsConfig); + + if (commonAdsConfigString != null && commonAdsConfigString.isNotEmpty) { + return CommonAdsConfig.fromJson(jsonDecode(commonAdsConfigString)); + } + return CommonAdsConfig.fromJson({}); + } + + AnalyticsConfig getAnalyticsConfig() { + final analyticsConfigStr = + RemoteConfigUtils.instance.getString(RemoteConfigReservedConstants.analyticsConfig); + if (analyticsConfigStr != null && analyticsConfigStr.isNotEmpty) { + return AnalyticsConfig.fromJson(jsonDecode(analyticsConfigStr)); + } + return AnalyticsConfig.fromJson({}); + } + + RemoteDeployment getRemoteDeployment() { + final deploymentConfigStr = + RemoteConfigUtils.instance.getString(RemoteConfigReservedConstants.deploymentConfig); + if (deploymentConfigStr != null && deploymentConfigStr.isNotEmpty) { + return RemoteDeployment.fromJson(jsonDecode(deploymentConfigStr)); + } + return RemoteDeployment.fromJson({}); + } +} diff --git a/guru_app/lib/firebase/remoteconfig/remote_config_manager.dart b/guru_app/lib/firebase/remoteconfig/remote_config_manager.dart new file mode 100644 index 0000000..f4d2db9 --- /dev/null +++ b/guru_app/lib/firebase/remoteconfig/remote_config_manager.dart @@ -0,0 +1,209 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:firebase_remote_config/firebase_remote_config.dart'; +import 'package:guru_app/ads/core/ads_config.dart'; +import 'package:guru_app/analytics/data/analytics_model.dart'; +import 'package:guru_app/firebase/remoteconfig/reserved_remote_config_models.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:guru_utils/http/http_model.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:guru_utils/remote/remote_config.dart'; + +part "remote_config_interface.dart"; + +/// Created by Haoyi on 2020/4/22 +/// +part "remote_config_reserved_constants.dart"; + +class RemoteConfigManager extends IRemoteConfig { + final BehaviorSubject _subject = + BehaviorSubject.seeded(null); + static RemoteConfigManager? _instance; + + static RemoteConfigManager _getInstance() { + _instance ??= RemoteConfigManager._internal(); + return _instance!; + } + + factory RemoteConfigManager() => _getInstance(); + + static RemoteConfigManager get instance => _getInstance(); + + static final RegExp _invalidABKey = RegExp('[^a-zA-Z0-9_-]'); + + RemoteConfigManager._internal(); + + Future init(Map defaultConfigs) async { + final remoteConfig = FirebaseRemoteConfig.instance; + await remoteConfig.setConfigSettings(RemoteConfigSettings( + fetchTimeout: const Duration(seconds: 15), + minimumFetchInterval: const Duration(hours: 2), + )); + + _subject.add(remoteConfig); + + try { + await remoteConfig.setDefaults(defaultConfigs); + await remoteConfig.activate(); + } catch (exception) { + Log.d( + "Unable to fetch remote config. Cached or default values will be used!", + error: exception); + } finally { + _subject.add(remoteConfig); + } + } + + Future fetchAndActivate() async { + final remoteConfig = FirebaseRemoteConfig.instance; + try { + await remoteConfig.fetchAndActivate(); + } catch (exception) { + Log.d( + "Unable to fetch remote config. Cached or default values will be used!", + error: exception); + } finally { + _subject.add(remoteConfig); + } + } + + Future dumpString() async { + final config = FirebaseRemoteConfig.instance; + final data = config.getAll(); + String result = ""; + for (var entry in data.entries) { + result += "[${entry.key}] ==> (${entry.value.asString()})\n"; + } + return result; + } + + static String valueSourceToString(ValueSource source) { + switch (source) { + case ValueSource.valueRemote: + return "Remote"; + case ValueSource.valueStatic: + return "Static"; + default: + return "Default"; + } + } + + Map allData() { + final config = FirebaseRemoteConfig.instance; + final data = config.getAll(); + final result = { + for (var entry in data.entries) + "${entry.key} [${valueSourceToString(entry.value.source)}]": + entry.value.asString() + }; + result["last_fetch_remote_config_time"] = config.lastFetchTime.toString(); + result["last_fetch_remote_config_status"] = + config.lastFetchStatus.toString(); + return result; + } + + Future forceFetch({bool debug = false}) async { + final remoteConfig = FirebaseRemoteConfig.instance; + try { + await remoteConfig.setConfigSettings(RemoteConfigSettings( + fetchTimeout: const Duration(seconds: 15), + minimumFetchInterval: const Duration(seconds: 0), + )); + await remoteConfig.fetchAndActivate(); + } catch (exception) { + Log.d( + "Unable to fetch remote config. Cached or default values will be used $exception", + error: exception); + if (debug) { + rethrow; + } + } finally { + _subject.add(remoteConfig); + await remoteConfig.setConfigSettings(RemoteConfigSettings( + fetchTimeout: const Duration(seconds: 15), + minimumFetchInterval: const Duration(hours: 2), + )); + } + } + + Map getABProperties() { + final config = FirebaseRemoteConfig.instance; + final data = config.getAll(); + final result = {}; + final invalidABKeys = {}; + dynamic cause; + for (var entry in data.entries) { + final valueStr = entry.value.asString(); + if (valueStr.contains("guru_ab_")) { + try { + final jsonValue = json.decode(valueStr); + if (jsonValue is Map) { + for (var jsonEntry in jsonValue.entries) { + if (jsonEntry.key.contains("guru_ab_")) { + String abName = jsonEntry.key.replaceFirst("guru_ab_", ""); + if (abName.contains(_invalidABKey)) { + Log.w("abName($abName) length is invalid! $abName"); + invalidABKeys.add(abName); + } else { + if (abName.length > 20) { + invalidABKeys.add(abName); + abName = abName.substring(0, 20); + } + result["ab_$abName"] = jsonEntry.value.toString(); + Log.i("abName:ab_$abName value:${jsonEntry.value}"); + } + } + } + } + } catch (error, stacktrace) { + Log.w("decode json error! $error"); + cause = error; + } + } + } + if (invalidABKeys.isNotEmpty) { + GuruAnalytics.instance.logException( + InvalidABPropertyKeysException(invalidABKeys, cause: cause)); + } + return result; + } + + @override + bool? getBool(String name, {bool? defaultValue}) => + _subject.value?.getBool(name) ?? defaultValue; + + @override + String? getString(String name, {String? defaultValue}) => + _subject.value?.getString(name) ?? + defaultValue ?? + RemoteConfigReservedConstants.getDefaultConfigString(name); + + @override + double? getDouble(String name, {double? defaultValue}) => + _subject.value?.getDouble(name) ?? defaultValue; + + @override + int? getInt(String name, {int? defaultValue}) => + _subject.value?.getInt(name) ?? defaultValue; + + Stream observeConfig() => + _subject.stream.map((config) => config ?? FirebaseRemoteConfig.instance); + + @override + Stream observeBool(String name, {bool? defaultValue}) => + observeConfig().map((config) => config.getBool(name)); + + @override + Stream observeString(String name, {String? defaultValue}) => + observeConfig().map((config) => config.getString(name)); + + @override + Stream observeDouble(String name, {double? defaultValue}) => + observeConfig().map((config) => config.getDouble(name)); + + @override + Stream observeInt(String name, {int? defaultValue}) => + observeConfig().map((config) => config.getInt(name)); +} diff --git a/guru_app/lib/firebase/remoteconfig/remote_config_reserved_constants.dart b/guru_app/lib/firebase/remoteconfig/remote_config_reserved_constants.dart new file mode 100644 index 0000000..97f94f4 --- /dev/null +++ b/guru_app/lib/firebase/remoteconfig/remote_config_reserved_constants.dart @@ -0,0 +1,46 @@ +/// Created by Haoyi on 2022/2/26 + +part of "remote_config_manager.dart"; + +extension RemoteConfigReservedConstants on RemoteConfigManager { + static const fbEventMapping = "fb_event_mapping"; + static const iosReviewVersion = "ios_review_version"; + static const taichiConfig = "taichi_config"; + + // ads + static const iadsConfig = "iads_config"; // Interstitial Ads Config + static const badsConfig = "bads_config"; // Banner Ads Config + static const radsConfig = "rads_config"; // Rewarded Ads Config + static const oadsConfig = "oads_config"; // Open Ads Config + static const sadsConfig = "sads_config"; // Strategy Ads Config + static const iosAttConfig = "ios_att_config"; // iOS ATT Config + static const commonAdsConfig = "common_ads_config"; // Common Ads Config + + // rater + static const appRater = "app_rater"; + + static const cdnConfig = "cdn_config"; + + static const analyticsConfig = "analytics_config"; + + static const deploymentConfig = "deployment_config"; + + static const Set _reservedRemoteConfigNames = { + fbEventMapping, + iosReviewVersion, + taichiConfig, + iadsConfig, + badsConfig, + radsConfig, + oadsConfig, + iosAttConfig, + appRater, + cdnConfig, + analyticsConfig, + deploymentConfig + }; + + static String? getDefaultConfigString(String key) { + return GuruApp.instance.defaultRemoteConfig[key]; + } +} diff --git a/guru_app/lib/firebase/remoteconfig/reserved_remote_config_models.dart b/guru_app/lib/firebase/remoteconfig/reserved_remote_config_models.dart new file mode 100644 index 0000000..5b458a4 --- /dev/null +++ b/guru_app/lib/firebase/remoteconfig/reserved_remote_config_models.dart @@ -0,0 +1,131 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:guru_app/ads/core/ads_config.dart'; +export 'package:guru_app/ads/core/ads_config.dart'; +/// Created by Haoyi on 2022/8/25 +/// +part 'reserved_remote_config_models.g.dart'; + +// @JsonSerializable() +// class TaichiConfig { +// @JsonKey(name: "enable", defaultValue: false) +// final bool enable; +// +// @JsonKey(name: "threshold", defaultValue: "") +// final String threshold; +// +// @JsonKey(name: "abnormal_threshold", defaultValue: 0.1) +// final double abnormalThreshold; +// +// TaichiConfig( +// {this.enable = false, this.threshold = "", this.abnormalThreshold = 0.1}); +// +// factory TaichiConfig.fromJson(Map json) => +// _$TaichiConfigFromJson(json); +// +// @override +// String toString() { +// return 'TaichiConfig{enable: $enable, threshold: $threshold}'; +// } +// +// Map toJson() => _$TaichiConfigToJson(this); +// } + +@JsonSerializable() +class ImpressionData { + @JsonKey(name: "ad_platform", defaultValue: "MAX") + final String platform; + + @JsonKey(name: "id", defaultValue: "") + final String id; + + @JsonKey(name: "adunit_id", defaultValue: "") + final String unitId; + + @JsonKey(name: "adunit_name", defaultValue: "") + final String unitName; + + @JsonKey(name: "adunit_format", defaultValue: "") + final String unitFormat; + + @JsonKey(name: "adgroup_id", defaultValue: "") + final String groupId; + + @JsonKey(name: "adgroup_name", defaultValue: "") + final String groupName; + + @JsonKey(name: "adgroup_type", defaultValue: "") + final String groupType; + + @JsonKey(name: "currency", defaultValue: "") + final String currency; + + @JsonKey(name: "country", defaultValue: "") + final String country; + + @JsonKey(name: "app_version", defaultValue: "") + final String appVersion; + + @JsonKey(name: "adgroup_priority", defaultValue: 0) + final int groupPriority; + + @JsonKey(name: "publisher_revenue", defaultValue: -1) + final double publisherRevenue; + + @JsonKey(name: "network_name", defaultValue: "") + final String networkName; + + @JsonKey(name: "network_placement_id", defaultValue: "") + final String networkPlacementId; + + @JsonKey(name: "precision", defaultValue: "") + final String precision; + + @JsonKey(ignore: true) + late Map payload; + + ImpressionData derive({double? newPublisherRevenue}) { + final newPayload = Map.from(payload); + newPayload["publisher_revenue"] = newPublisherRevenue ?? publisherRevenue; + return ImpressionData.fromJson(newPayload); + } + + @override + String toString() { + return 'ImpressionData{platform: $platform, id: $id, unitId: $unitId, unitName: $unitName, unitFormat: $unitFormat, groupId: $groupId, groupName: $groupName, groupType: $groupType, currency: $currency, country: $country, appVersion: $appVersion, groupPriority: $groupPriority, publisherRevenue: $publisherRevenue, networkName: $networkName, networkPlacementId: $networkPlacementId, precision: $precision}'; + } + + ImpressionData( + {required this.platform, + required this.id, + required this.unitId, + required this.unitName, + required this.unitFormat, + required this.groupId, + required this.groupName, + required this.groupType, + required this.currency, + required this.country, + required this.appVersion, + required this.groupPriority, + required this.publisherRevenue, + required this.networkName, + required this.networkPlacementId, + required this.precision}); + + factory ImpressionData.fromJson(Map json) => + _$ImpressionDataFromJson(json)..payload = json; + + Map toJson() => _$ImpressionDataToJson(this); +} + +class InvalidABPropertyKeysException implements Exception { + final Set invalidKeys; + final dynamic cause; + + InvalidABPropertyKeysException(this.invalidKeys, {this.cause}); + + @override + String toString() { + return "InvalidABPropertyKeysException: $invalidKeys cause:$cause"; + } +} diff --git a/guru_app/lib/firebase/remoteconfig/reserved_remote_config_models.g.dart b/guru_app/lib/firebase/remoteconfig/reserved_remote_config_models.g.dart new file mode 100644 index 0000000..74ef2d4 --- /dev/null +++ b/guru_app/lib/firebase/remoteconfig/reserved_remote_config_models.g.dart @@ -0,0 +1,47 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'reserved_remote_config_models.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ImpressionData _$ImpressionDataFromJson(Map json) => + ImpressionData( + platform: json['ad_platform'] as String? ?? 'MAX', + id: json['id'] as String? ?? '', + unitId: json['adunit_id'] as String? ?? '', + unitName: json['adunit_name'] as String? ?? '', + unitFormat: json['adunit_format'] as String? ?? '', + groupId: json['adgroup_id'] as String? ?? '', + groupName: json['adgroup_name'] as String? ?? '', + groupType: json['adgroup_type'] as String? ?? '', + currency: json['currency'] as String? ?? '', + country: json['country'] as String? ?? '', + appVersion: json['app_version'] as String? ?? '', + groupPriority: json['adgroup_priority'] as int? ?? 0, + publisherRevenue: (json['publisher_revenue'] as num?)?.toDouble() ?? -1, + networkName: json['network_name'] as String? ?? '', + networkPlacementId: json['network_placement_id'] as String? ?? '', + precision: json['precision'] as String? ?? '', + ); + +Map _$ImpressionDataToJson(ImpressionData instance) => + { + 'ad_platform': instance.platform, + 'id': instance.id, + 'adunit_id': instance.unitId, + 'adunit_name': instance.unitName, + 'adunit_format': instance.unitFormat, + 'adgroup_id': instance.groupId, + 'adgroup_name': instance.groupName, + 'adgroup_type': instance.groupType, + 'currency': instance.currency, + 'country': instance.country, + 'app_version': instance.appVersion, + 'adgroup_priority': instance.groupPriority, + 'publisher_revenue': instance.publisherRevenue, + 'network_name': instance.networkName, + 'network_placement_id': instance.networkPlacementId, + 'precision': instance.precision, + }; diff --git a/guru_app/lib/generated/intl/messages_all.dart b/guru_app/lib/generated/intl/messages_all.dart new file mode 100644 index 0000000..203415c --- /dev/null +++ b/guru_app/lib/generated/intl/messages_all.dart @@ -0,0 +1,63 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that looks up messages for specific locales by +// delegating to the appropriate library. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:implementation_imports, file_names, unnecessary_new +// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering +// ignore_for_file:argument_type_not_assignable, invalid_assignment +// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases +// ignore_for_file:comment_references + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; +import 'package:intl/src/intl_helpers.dart'; + +import 'messages_en.dart' as messages_en; + +typedef Future LibraryLoader(); +Map _deferredLibraries = { + 'en': () => new SynchronousFuture(null), +}; + +MessageLookupByLibrary? _findExact(String localeName) { + switch (localeName) { + case 'en': + return messages_en.messages; + default: + return null; + } +} + +/// User programs should call this before using [localeName] for messages. +Future initializeMessages(String localeName) { + var availableLocale = Intl.verifiedLocale( + localeName, (locale) => _deferredLibraries[locale] != null, + onFailure: (_) => null); + if (availableLocale == null) { + return new SynchronousFuture(false); + } + var lib = _deferredLibraries[availableLocale]; + lib == null ? new SynchronousFuture(false) : lib(); + initializeInternalMessageLookup(() => new CompositeMessageLookup()); + messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); + return new SynchronousFuture(true); +} + +bool _messagesExistFor(String locale) { + try { + return _findExact(locale) != null; + } catch (e) { + return false; + } +} + +MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { + var actualLocale = + Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); + if (actualLocale == null) return null; + return _findExact(actualLocale); +} diff --git a/guru_app/lib/generated/intl/messages_en.dart b/guru_app/lib/generated/intl/messages_en.dart new file mode 100644 index 0000000..a029099 --- /dev/null +++ b/guru_app/lib/generated/intl/messages_en.dart @@ -0,0 +1,25 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that provides messages for a en locale. All the +// messages from the main program should be duplicated here with the same +// function name. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new +// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering +// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases +// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes +// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; + +final messages = new MessageLookup(); + +typedef String MessageIfAbsent(String messageStr, List args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'en'; + + final messages = _notInlinedMessages(_notInlinedMessages); + static Map _notInlinedMessages(_) => {}; +} diff --git a/guru_app/lib/generated/l10n.dart b/guru_app/lib/generated/l10n.dart new file mode 100644 index 0000000..7d72d31 --- /dev/null +++ b/guru_app/lib/generated/l10n.dart @@ -0,0 +1,78 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'intl/messages_all.dart'; + +// ************************************************************************** +// Generator: Flutter Intl IDE plugin +// Made by Localizely +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars +// ignore_for_file: join_return_with_assignment, prefer_final_in_for_each +// ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes + +class S { + S(); + + static S? _current; + + static S get current { + assert(_current != null, + 'No instance of S was loaded. Try to initialize the S delegate before accessing S.current.'); + return _current!; + } + + static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); + + static Future load(Locale locale) { + final name = (locale.countryCode?.isEmpty ?? false) + ? locale.languageCode + : locale.toString(); + final localeName = Intl.canonicalizedLocale(name); + return initializeMessages(localeName).then((_) { + Intl.defaultLocale = localeName; + final instance = S(); + S._current = instance; + + return instance; + }); + } + + static S of(BuildContext context) { + final instance = S.maybeOf(context); + assert(instance != null, + 'No instance of S present in the widget tree. Did you add S.delegate in localizationsDelegates?'); + return instance!; + } + + static S? maybeOf(BuildContext context) { + return Localizations.of(context, S); + } +} + +class AppLocalizationDelegate extends LocalizationsDelegate { + const AppLocalizationDelegate(); + + List get supportedLocales { + return const [ + Locale.fromSubtags(languageCode: 'en'), + ]; + } + + @override + bool isSupported(Locale locale) => _isSupported(locale); + @override + Future load(Locale locale) => S.load(locale); + @override + bool shouldReload(AppLocalizationDelegate old) => false; + + bool _isSupported(Locale locale) { + for (var supportedLocale in supportedLocales) { + if (supportedLocale.languageCode == locale.languageCode) { + return true; + } + } + return false; + } +} diff --git a/guru_app/lib/guru_app.dart b/guru_app/lib/guru_app.dart new file mode 100644 index 0000000..dd0bcf7 --- /dev/null +++ b/guru_app/lib/guru_app.dart @@ -0,0 +1,344 @@ +import 'dart:io'; +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/ads/ads_manager.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_applovin_flutter/guru_applovin_flutter.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/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: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'; +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'; + + +/// Created by Haoyi on 2022/8/25 + +abstract class AppSpec { + String get appName; + + String get flavor; + + AppDetails get details; + + AdsProfile get adsProfile; + + ProductProfile get productProfile; + + AdjustProfile get adjustProfile; + + Deployment get deployment; + + Map get defaultRemoteConfig; +} + +class NotImplementationAppSpecCreatorException implements Exception { + NotImplementationAppSpecCreatorException(); + + @override + String toString() { + return 'NotImplementationAppSpecCreatorException'; + } +} + +class AppEnv { + final AppSpec spec; + final RootPackage package; + final BackgroundMessageHandler? backgroundMessageHandler; + final ToastDelegate? toastDelegate; + + AppEnv( + {required this.spec, + required this.package, + this.backgroundMessageHandler, + this.toastDelegate}); +} + +extension _GuruPackageExtension on GuruPackage { + Iterable _mergeSupportedLocales() { + final Set locales = supportedLocales.toSet(); + for (var child in children) { + locales.addAll(child._mergeSupportedLocales()); + } + return locales; + } + + Iterable> _mergeLocalizationsDelegates() { + final Set> 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(); + } + } +} + +class GuruApp { + static late GuruApp _instance; + + static GuruApp get instance => _instance; + + final RootPackage rootPackage; + + final AppSpec appSpec; + + 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 get defaultRemoteConfig => appSpec.defaultRemoteConfig; + + Set get conversionEvents => appSpec.deployment.conversionEvents; + + GuruApp._({required this.appSpec, required this.rootPackage, ToastDelegate? toastDelegate}) { + GuruUtils.toastDelegate = toastDelegate; + AdsOverlay.bind(showBanner: GuruPopup.instance.showAdsBanner); + } + + Iterable get supportedLocales => rootPackage._mergeSupportedLocales(); + + Iterable> get localizationsDelegates => + rootPackage._mergeLocalizationsDelegates(); + + bool? _check; + + 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 _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); + 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.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; + try { + _instance = GuruApp._( + appSpec: appEnv.spec, rootPackage: appEnv.package, toastDelegate: appEnv.toastDelegate); + 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); + + final remoteDeployment = RemoteConfigManager.instance.getRemoteDeployment(); + 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 offerProductIds(ProductId productId) { + return productProfile.offerProductIds(productId); + } +} + +extension GuruRemoteConfigExt on GuruApp { + String getDefaultRemoteConfig(String key, {String defaultValue = ""}) { + return defaultRemoteConfig[key] ?? defaultValue; + } +} diff --git a/guru_app/lib/hook/hook_manager.dart b/guru_app/lib/hook/hook_manager.dart new file mode 100644 index 0000000..c08ad6f --- /dev/null +++ b/guru_app/lib/hook/hook_manager.dart @@ -0,0 +1,56 @@ +import 'package:guru_utils/log/log.dart'; + +/// Created by Haoyi on 2022/8/15 + +// enum HookScene { watchRewardAds, audit } + +class HookScene { + final String name; + + const HookScene.define(this.name); + + static const watchRewardAds = HookScene.define("watchRewardAds"); +} + +typedef HookCallback = void Function(dynamic); + +class HookManager { + final Map> hooks = {}; + + HookManager._(); + + static final HookManager instance = HookManager._(); + + void addHook(HookScene scene, HookCallback callback) { + List? callbacks = hooks[scene]; + if (callbacks == null) { + callbacks = []; + hooks[scene] = callbacks; + } else { + callbacks.remove(callback); + } + callbacks.add(callback); + } + + void removeHook(HookScene scene, HookCallback callback) { + List? callbacks = hooks[scene]; + if (callbacks != null) { + callbacks.remove(callback); + } + } + + void dispatch(HookScene scene, dynamic params) { + final callbacks = hooks[scene]?.toList() ?? []; + for (var callback in callbacks) { + try { + callback(params); + } catch (error, stacktrace) { + Log.w("_dispatch hook[$scene] params:$params error! $error!", stackTrace: stacktrace); + } + } + } + + void watchRewardAds() { + dispatch(HookScene.watchRewardAds, null); + } +} diff --git a/guru_app/lib/l10n/intl_en.arb b/guru_app/lib/l10n/intl_en.arb new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/guru_app/lib/l10n/intl_en.arb @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/guru_app/lib/lifecycle/lifecycle_model.dart b/guru_app/lib/lifecycle/lifecycle_model.dart new file mode 100644 index 0000000..90b24e8 --- /dev/null +++ b/guru_app/lib/lifecycle/lifecycle_model.dart @@ -0,0 +1,12 @@ +import 'package:guru_utils/controller/controller.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_applifecycle_flutter/guru_applifecycle.dart'; + +/// Created by Haoyi on 2022/7/18 + +class RequestNotificationPermissionEvent extends LifecycleEvent { + final bool rationale; + + const RequestNotificationPermissionEvent({this.rationale = false}) : super(); +} diff --git a/guru_app/lib/property/app_property.dart b/guru_app/lib/property/app_property.dart new file mode 100644 index 0000000..e875fd6 --- /dev/null +++ b/guru_app/lib/property/app_property.dart @@ -0,0 +1,29 @@ +/// Created by Haoyi on 2022/10/5 +import 'dart:convert'; + +import 'package:guru_app/account/model/account.dart'; +import 'package:guru_app/account/model/account_profile.dart'; +import 'package:guru_app/account/model/user.dart'; +import 'package:guru_app/api/data/orders/orders_model.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:guru_app/property/property_keys.dart'; +import 'package:guru_utils/property/property_model.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_utils/device/device_info.dart'; +import 'package:guru_app/property/app_property.dart'; +import 'package:guru_utils/id/id_utils.dart'; +import 'package:guru_utils/core/ext.dart'; + +export 'package:guru_utils/property/app_property.dart'; + +part 'property_tags.dart'; + +part 'modules/account_property_extension.dart'; + +part 'modules/default_property_extension.dart'; + +part 'modules/iap_property_extension.dart'; + +part 'modules/analytics_property_extension.dart'; + +part 'modules/igc_property_extension.dart'; \ No newline at end of file diff --git a/guru_app/lib/property/modules/account_property_extension.dart b/guru_app/lib/property/modules/account_property_extension.dart new file mode 100644 index 0000000..9bf0e81 --- /dev/null +++ b/guru_app/lib/property/modules/account_property_extension.dart @@ -0,0 +1,81 @@ +/// Created by Haoyi on 6/3/21 +part of "../app_property.dart"; + +extension AccountPropertyExtension on AppProperty { + void setAccountSaasUser(SaasUser saasUser) { + final data = jsonEncode(saasUser); + setString(PropertyKeys.accountSaasUser, data); + } + + void setAccountDevice(DeviceInfo deviceInfo) { + final data = jsonEncode(deviceInfo); + setString(PropertyKeys.accountDevice, data); + } + + void setAccountProfile(AccountProfile accountProfile) { + final data = jsonEncode(accountProfile); + setString(PropertyKeys.accountProfile, data); + } + + // refer updateLocalProfile + void setDirtyAccountProfile(AccountProfile accountProfile) { + final data = jsonEncode(accountProfile.copyWith(dirty: true)); + setString(PropertyKeys.accountProfile, data); + } + + Future getAccountDevice() async { + final deviceString = await getString(PropertyKeys.accountDevice, defValue: ''); + if (deviceString == "") { + return null; + } + final map = jsonDecode(deviceString); + return DeviceInfo.fromJson(map); + } + + Future loadAccount() async { + final accountBundle = await loadValuesByTag(PropertyTags.account).catchError((error) { + Log.v("loadValuesByTag is empty, $error"); + return PropertyBundle.empty(); + }); + SaasUser? saasUser; + DeviceInfo? device; + AccountProfile? accountProfile; + + final saasUserString = accountBundle.getString(PropertyKeys.accountSaasUser); + if (DartExt.isNotBlank(saasUserString)) { + final map = jsonDecode(saasUserString!); + saasUser = SaasUser.fromJson(map); + } + + final accountDeviceString = accountBundle.getString(PropertyKeys.accountDevice); + if (DartExt.isNotBlank(accountDeviceString)) { + final map = jsonDecode(accountDeviceString!); + device = DeviceInfo.fromJson(map); + } + + final accountProfileString = accountBundle.getString(PropertyKeys.accountProfile); + if (DartExt.isNotBlank(accountProfileString)) { + final map = jsonDecode(accountProfileString!); + accountProfile = AccountProfile.fromJson(map); + } + + return Account.restore(saasUser: saasUser, device: device, accountProfile: accountProfile); + } + + Future getLatestReportDeviceTimestamp() async { + return await getInt(PropertyKeys.latestReportDeviceTimestamp, defValue: 0); + } + + void setLatestReportDeviceTimestamp(int timestamp) async { + setInt(PropertyKeys.latestReportDeviceTimestamp, timestamp); + } + + Future getAnonymousSecretKey() async { + String? secret = await getString(PropertyKeys.anonymousSecretKey, defValue: ""); + if (secret == '') { + secret = IdUtils.uuidV4(); + await setString(PropertyKeys.anonymousSecretKey, secret); + } + return secret; + } +} diff --git a/guru_app/lib/property/modules/analytics_property_extension.dart b/guru_app/lib/property/modules/analytics_property_extension.dart new file mode 100644 index 0000000..41b2ac1 --- /dev/null +++ b/guru_app/lib/property/modules/analytics_property_extension.dart @@ -0,0 +1,41 @@ +/// Created by Haoyi on 2022/12/22 + +part of "../app_property.dart"; + +extension AnalyticsPropertyExtension on AppProperty { + Future setAdId(String adId) async { + if (adId.isNotEmpty) { + await setString(PropertyKeys.analyticsAdId, adId); + } + } + + Future setFirebaseId(String firebaseId) async { + if (firebaseId.isNotEmpty) { + await setString(PropertyKeys.analyticsFirebaseId, firebaseId); + } + } + + Future setAdjustId(String adjustId) async { + if (adjustId.isNotEmpty) { + await setString(PropertyKeys.analyticsAdjustId, adjustId); + } + } + + Future setAnalyticsDeviceId(String deviceId) async { + if (deviceId.isNotEmpty) { + await setString(PropertyKeys.analyticsDeviceId, deviceId); + } + } + + Future setUserId(String userId) async { + if (userId.isNotEmpty) { + await setString(PropertyKeys.analyticsUserId, userId); + } + } + + Future setIdfa(String idfa) async { + if (idfa.isNotEmpty) { + await setString(PropertyKeys.analyticsIdfa, idfa); + } + } +} diff --git a/guru_app/lib/property/modules/default_property_extension.dart b/guru_app/lib/property/modules/default_property_extension.dart new file mode 100644 index 0000000..ca306f0 --- /dev/null +++ b/guru_app/lib/property/modules/default_property_extension.dart @@ -0,0 +1,40 @@ +part of "../app_property.dart"; + +/// Created by @Haoyi on 5/14/21 + +extension DefaultPropertyExtension on AppProperty { + Future getDeviceId() async { + return getOrCreateString(PropertyKeys.deviceId, IdUtils.uuidV4()); + } + + Future getFirstInstallTime() async { + return await AppProperty.getInstance() + .getOrCreateInt(PropertyKeys.firstInstallTime, DateTimeUtils.currentTimeInMillis()); + } + + Future getLatestLtDate() async { + return await AppProperty.getInstance() + .getOrCreateInt(PropertyKeys.latestLtDate, DateTimeUtils.yyyyMMddUtcNum); + } + + Future setLatestLtDate(int dateNum) async { + await AppProperty.getInstance().setInt(PropertyKeys.latestLtDate, dateNum); + } + + Future getLtDays() async { + return await AppProperty.getInstance().getInt(PropertyKeys.ltDays, defValue: 0); + } + + Future setLtDays(int days) async { + await AppProperty.getInstance().setInt(PropertyKeys.ltDays, days); + } + + Future getLatestAnalyticsStrategy() async { + return await AppProperty.getInstance() + .getString(PropertyKeys.latestAnalyticsStrategy, defValue: ""); + } + + Future setLatestAnalyticsStrategy(String strategy) async { + await AppProperty.getInstance().setString(PropertyKeys.latestAnalyticsStrategy, strategy); + } +} diff --git a/guru_app/lib/property/modules/iap_property_extension.dart b/guru_app/lib/property/modules/iap_property_extension.dart new file mode 100644 index 0000000..54c6322 --- /dev/null +++ b/guru_app/lib/property/modules/iap_property_extension.dart @@ -0,0 +1,21 @@ +/// Created by Haoyi on 2022/11/30 +part of "../app_property.dart"; + +extension IapPropertyExtension on AppProperty { + Future saveFailedIapOrders(OrdersReport order) async { + await setString(PropertyKeys.buildReportFailedIapOrdersKey(), json.encode(order)); + } + + Future loadAllFailedIapOrders() async { + try { + return await loadValuesByTag(PropertyTags.failedOrders); + } catch (e) { + Log.e("error:$e"); + } + return PropertyBundle.empty(); + } + + Future removeReportSuccessOrder(PropertyKey key) async { + remove(key); + } +} diff --git a/guru_app/lib/property/modules/igc_property_extension.dart b/guru_app/lib/property/modules/igc_property_extension.dart new file mode 100644 index 0000000..048d17c --- /dev/null +++ b/guru_app/lib/property/modules/igc_property_extension.dart @@ -0,0 +1,75 @@ +/// Created by Haoyi on 2021/7/1 + +part of "../app_property.dart"; + +extension IgcPropertyExtension on AppProperty { + Future isFirstUseGemsFeature() async { + final gems = await getInt(PropertyKeys.currentIgcBalance, defValue: -1); + return gems == -1; + } + + Future _calculateIgcBalance(int coin) async { + final bundle = await loadValuesByTag(PropertyTags.igc); + int currentCoinBalance = bundle.getInt(PropertyKeys.currentIgcBalance) ?? 0; + final cipher = bundle.getInt(PropertyKeys.currentIgcBalanceValidation) ?? + GuruApp.instance.appSpec.deployment.igcBalanceSecret; + final cipherCoinBalance = cipher ^ GuruApp.instance.appSpec.deployment.igcBalanceSecret; + + if (cipherCoinBalance != currentCoinBalance) { + currentCoinBalance = cipherCoinBalance; + } + final newBalance = max(currentCoinBalance + coin, 0); + final updateBundle = PropertyBundle(); + updateBundle.setInt(PropertyKeys.currentIgcBalance, newBalance); + updateBundle.setInt(PropertyKeys.currentIgcBalanceValidation, + newBalance ^ GuruApp.instance.appSpec.deployment.igcBalanceSecret); + setProperties(updateBundle); + return newBalance; + } + + Future consumeIgc(int igc) async { + return await _calculateIgcBalance(-igc); + } + + Future accumulateIgc(int igc) async { + return await _calculateIgcBalance(igc); + } + + Future clearAllIgc() async { + return await removeAllWithTag(PropertyTags.igc); + } + + Future increaseAndGetIapCount() async { + final count = await getInt(PropertyKeys.iapCount, defValue: 0); + await setInt(PropertyKeys.iapCount, count + 1); + return count + 1; + } + + Future getIapCount() async { + return await getInt(PropertyKeys.iapCount, defValue: 0); + } + + Future isPaidUser() async { + return (await getIapCount()) > 0; + } + + Future getIapIgc() async { + return await getInt(PropertyKeys.iapIgc, defValue: 0); + } + + Future accumulateIapIgc(int igc) async { + final accumulatedIgc = await getInt(PropertyKeys.iapIgc, defValue: 0); + await setInt(PropertyKeys.iapIgc, accumulatedIgc + igc); + return accumulatedIgc + igc; + } + + Future getNoIapIgc() async { + return await getInt(PropertyKeys.noIapIgc, defValue: 0); + } + + Future accumulateNoIapIgc(int igc) async { + final accumulatedIgc = await getInt(PropertyKeys.noIapIgc, defValue: 0); + await setInt(PropertyKeys.noIapIgc, accumulatedIgc + igc); + return accumulatedIgc + igc; + } +} diff --git a/guru_app/lib/property/property_keys.dart b/guru_app/lib/property/property_keys.dart new file mode 100644 index 0000000..79bf542 --- /dev/null +++ b/guru_app/lib/property/property_keys.dart @@ -0,0 +1,117 @@ +import 'package:guru_app/guru_app.dart'; +import 'package:guru_app/property/app_property.dart'; +import 'package:guru_utils/id/id_utils.dart'; +import 'package:guru_utils/property/property_model.dart'; +import 'package:guru_utils/settings/settings.dart'; + +/// Created by Haoyi on 2022/8/25 + +class PropertyKeys { + static const PropertyKey version = UtilsSettingsKeys.version; + static const PropertyKey buildNumber = UtilsSettingsKeys.buildNumber; + static const PropertyKey firstInstallTime = UtilsSettingsKeys.firstInstallTime; + static const PropertyKey firstInstallVersion = UtilsSettingsKeys.firstInstallVersion; + static const PropertyKey prevInstallVersion = UtilsSettingsKeys.prevInstallVersion; + static const PropertyKey latestInstallVersion = UtilsSettingsKeys.latestInstallVersion; + static const PropertyKey previousInstalledVersion = UtilsSettingsKeys.previousInstalledVersion; + static const PropertyKey appInstanceId = UtilsSettingsKeys.appInstanceId; + static const PropertyKey soundEffect = UtilsSettingsKeys.soundEffect; + static const PropertyKey vibration = UtilsSettingsKeys.vibration; + static const PropertyKey deviceId = UtilsSettingsKeys.deviceId; + static const PropertyKey debugMode = UtilsSettingsKeys.debugMode; + static const PropertyKey latestLtDate = UtilsSettingsKeys.latestLtDate; + static const PropertyKey ltDays = UtilsSettingsKeys.ltDays; + + static const PropertyKey accountSaasUser = + PropertyKey.general("account_saas_user", tag: PropertyTags.account); + static const PropertyKey accountDevice = + PropertyKey.general("account_device", tag: PropertyTags.account); + static const PropertyKey accountProfile = + PropertyKey.general("account_profile", tag: PropertyTags.account); + static const PropertyKey latestReportDeviceTimestamp = + PropertyKey.general("latest_report_device_timestamp", tag: PropertyTags.account); + static const PropertyKey anonymousSecretKey = + PropertyKey.general("anonymous_secret_key", tag: PropertyTags.account); + + static const PropertyKey isNoAds = PropertyKey.setting("no_ads", tag: PropertyTags.ads); + + static const PropertyKey totalRevenue = + PropertyKey.general("total_revenue", tag: PropertyTags.financial); + + static const PropertyKey userRewardedCount = + PropertyKey.general("user_rewarded_count", tag: PropertyTags.ads); + + static const PropertyKey fakeInterstitialAds = UtilsPropertyKeys.fakeInterstitialAds; + + static const PropertyKey fakeRewardedAds = UtilsPropertyKeys.fakeRewardedAds; + + static const PropertyKey iapCount = PropertyKey.general("iap_count", tag: PropertyTags.iap); + static const PropertyKey iapIgc = PropertyKey.general("iap_igc", tag: PropertyTags.iap); + static const PropertyKey noIapIgc = PropertyKey.general("no_iap_igc", tag: PropertyTags.iap); + + static const PropertyKey admobConsentTestDeviceId = + PropertyKey.general("admob_consent_test_device_id", tag: PropertyTags.ads); + static const PropertyKey admobConsentDebugGeography = + PropertyKey.general("admob_consent_debug_geography", tag: PropertyTags.ads); + + static const PropertyKey latestAnalyticsStrategy = + PropertyKey.general("latest_analytics_strategy", tag: PropertyTags.analytics); + + static const PropertyKey subscriptionCount = + PropertyKey.general("subs_count", tag: PropertyTags.iap); + + static PropertyKey buildGroupSubscriptionCount(String group) { + return PropertyKey.general("subs_${group}_count", tag: PropertyTags.iap); + } + + static PropertyKey buildSubscriptionCount(ProductId productId) { + String key = "subs_${productId.iapEventName}_"; + if (productId.basePlan != null) { + key += "${productId.basePlan}_"; + } + + if (productId.offerId != null) { + key += "${productId.offerId}_"; + } + return PropertyKey.general("${key}count", tag: PropertyTags.iap); + } + + static PropertyKey buildReportFailedIapOrdersKey({String? overrideKey}) { + return PropertyKey.general(overrideKey ?? "RFIO_${IdUtils.uuidV4()}", + tag: PropertyTags.failedOrders); + } + + static const PropertyKey bestLevel = + PropertyKey.setting("best_level", tag: PropertyTags.analytics); + + static const PropertyKey analyticsAdId = + PropertyKey.general("analytics_ad_id", tag: PropertyTags.analytics); + static const PropertyKey analyticsFirebaseId = + PropertyKey.general("analytics_firebase_id", tag: PropertyTags.analytics); + static const PropertyKey analyticsAdjustId = + PropertyKey.general("analytics_adjust_id", tag: PropertyTags.analytics); + static const PropertyKey analyticsUserId = + PropertyKey.general("analytics_user_id", tag: PropertyTags.analytics); + static const PropertyKey analyticsDeviceId = + PropertyKey.general("analytics_device_id", tag: PropertyTags.analytics); + static const PropertyKey analyticsIdfa = + PropertyKey.general("analytics_idfa", tag: PropertyTags.analytics); + + static const PropertyKey currentIgcBalance = + PropertyKey.general("current_balance", tag: PropertyTags.igc); + static const PropertyKey currentIgcBalanceValidation = + PropertyKey.general("16%98d1x9sr0a@d20xrng", tag: PropertyTags.igc); + + static PropertyKey buildABTestProperty(String key) { + return PropertyKey.general("abtest_$key", tag: PropertyTags.guruAB); + } + + static PropertyKey requestNotificationPermissionTimes = + const PropertyKey.general("request_notification_permission_times"); + + static PropertyKey showNotificationPermissionTimes = + const PropertyKey.general("show_notification_permission_times"); + + static PropertyKey deniedNotificationPermissionTimes = + const PropertyKey.general("denied_notification_permission_times"); +} diff --git a/guru_app/lib/property/property_tags.dart b/guru_app/lib/property/property_tags.dart new file mode 100644 index 0000000..ca125c4 --- /dev/null +++ b/guru_app/lib/property/property_tags.dart @@ -0,0 +1,22 @@ +part of "app_property.dart"; + +class PropertyTags { + static const String promoCode = "promo_code"; + static const String account = "account"; + static const String rank = "rank"; + static const String iapSub = "iap_sub"; + static const String receipt = "receipt"; + static const String dailyRewards = "daily_rewards"; + static const String dailyQuests = "daily_quests"; + static const String failedOrders = "failed_orders"; + static const String strategyAds = "StrategyAds"; + static const String guruAB = "GuruAB"; + + static const String iap = UtilsPropertyTags.iap; + static const String ads = UtilsPropertyTags.ads; + static const String timer = UtilsPropertyTags.timer; + static const String igc = UtilsPropertyTags.igc; + static const String financial = UtilsPropertyTags.financial; + static const String settings = UtilsPropertyTags.settings; + static const String analytics = UtilsPropertyTags.analytics; +} diff --git a/guru_app/lib/property/runtime_property.dart b/guru_app/lib/property/runtime_property.dart new file mode 100644 index 0000000..bf74071 --- /dev/null +++ b/guru_app/lib/property/runtime_property.dart @@ -0,0 +1,2 @@ +/// Created by Haoyi on 2022/10/5 +export 'package:guru_utils/property/runtime_property.dart'; diff --git a/guru_app/lib/property/settings/global_settings.dart b/guru_app/lib/property/settings/global_settings.dart new file mode 100644 index 0000000..e1a5be2 --- /dev/null +++ b/guru_app/lib/property/settings/global_settings.dart @@ -0,0 +1,9 @@ +part of 'guru_settings.dart'; + +/// Created by Haoyi on 2022/8/25 + +mixin GlobalSettings { + final SettingBoolData isNoAds = SettingBoolData(PropertyKeys.isNoAds, defaultValue: false); + + final SettingIntData bestLevel = SettingIntData(PropertyKeys.bestLevel, defaultValue: 1); +} diff --git a/guru_app/lib/property/settings/guru_settings.dart b/guru_app/lib/property/settings/guru_settings.dart new file mode 100644 index 0000000..7090bd3 --- /dev/null +++ b/guru_app/lib/property/settings/guru_settings.dart @@ -0,0 +1,27 @@ +import 'package:guru_app/property/app_property.dart'; +import 'package:guru_app/property/property_keys.dart'; +import 'package:guru_utils/audio/audio_effector.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/property/runtime_property.dart'; +import 'package:guru_utils/settings/settings.dart'; +import 'package:guru_utils/vibration/vibrate_model.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +export 'package:guru_utils/settings/settings.dart'; + +/// Created by Haoyi on 2022/9/1 +/// +part 'global_settings.dart'; + +class GuruSettings extends Settings with GlobalSettings { + static final GuruSettings instance = GuruSettings._(); + + GuruSettings._(); + + @override + Future refresh() async { + final bundle = await super.refresh(); + return bundle; + } +} diff --git a/guru_app/lib/test/test_guru_app_creator.dart b/guru_app/lib/test/test_guru_app_creator.dart new file mode 100644 index 0000000..f7f1cb5 --- /dev/null +++ b/guru_app/lib/test/test_guru_app_creator.dart @@ -0,0 +1,16 @@ +import 'dart:math'; + +import 'package:guru_app/financial/data/db/order_database.dart'; +import 'package:guru_app/financial/manifest/manifest.dart'; +import 'package:guru_app/guru_app.dart'; + +/// Created by Haoyi on 2023/1/20 + +part 'test_guru_app_creator.g.dart'; + +@guruSpecCreator +AppSpec createSampleAppSpec(String flavor) { + return _GuruSpecFactory.create(flavor); +} + + diff --git a/guru_app/lib/test/test_guru_app_creator.g.dart b/guru_app/lib/test/test_guru_app_creator.g.dart new file mode 100644 index 0000000..b114e79 --- /dev/null +++ b/guru_app/lib/test/test_guru_app_creator.g.dart @@ -0,0 +1,946 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'test_guru_app_creator.dart'; + +// ************************************************************************** +// GuruSpecGenerator +// ************************************************************************** + +class _Guru_testRemoteConfigConstants { + static const iadsConfig = 'iads_config'; + + static const radsConfig = 'rads_config'; + + static const badsConfig = 'bads_config'; + + static const analyticsConfig = 'analytics_config'; + + static const Map _defaultConfigs = { + iadsConfig: + '{"free_s":600,"win_count":4,"scene":"game_start","sp_scene":"new_block:120;reset_scs:120","retry_min_s":10,"retry_max_s":600,"amazon_enable":false,"imp_gap_s":120}', + radsConfig: '{"win_count":3}', + badsConfig: '{"free_s":180,"win_count":1}', + analyticsConfig: '{"cap":"firebase|facebook|guru", "init_delay_s": 10}', + }; + + static String getDefaultConfigString(String key) => _defaultConfigs[key]; +} + +class _Guru_testAppSpec extends AppSpec { + _Guru_testAppSpec._(); + + static final _instance = _Guru_testAppSpec._(); + + @override + final appName = 'GuruApp'; + + @override + final flavor = 'guru_test'; + + @override + final details = AppDetails( + saasAppId: 'guruapp', + authority: 'demo.gurugame.fun', + storagePrefix: + 'https://firebasestorage.googleapis.com/v0/b/example.appspot.com/o', + defaultCdnPrefix: 'https://cdn1.example.gurugame.fun', + androidGooglePlayUrl: + 'https://play.google.com/store/apps/details?id=app_package_id', + policyUrl: 'https://solitaire.fungame.studio/policy.html', + termsUrl: 'https://solitaire.fungame.studio/termsofservice.html', + emailUrl: 'demo@gurugame.fun', + packageName: 'guru.app.demo', + bundleId: 'guru.app.demo', + facebookAppId: '123456789', + ); + + @override + final deployment = Deployment( + propertyCacheSize: 512, + enableDithering: false, + disableRewardsAds: true, + enableAnalyticsStatistic: true, + autoRestoreIap: false, + igcBalanceSecret: 2654404609, + initIgc: 500, + autoRequestNotificationPermission: false, + logFileSizeLimit: 10485760, + logFileCount: 7, + persistentLogLevel: 1, + iosValidateReceiptPassword: 'aa998877665544332211bb00cc', + conversionEvents: { + 'first_rads_rewarded', + 'level_end_success_1', + 'level_end_success_6', + 'level_end_success_10', + 'level_end_success_12', + 'level_end_success_15', + 'level_up', + 'level_up_1', + 'level_up_3', + 'level_up_5', + 'level_up_7', + 'level_up_10', + 'level_up_12', + 'level_up_15', + 'tch_ad_rev_roas_001', + 'tutorial_complete', + }, + apiConnectTimeout: 15000, + apiReceiveTimeout: 15000, + iosSandboxSubsRenewalSpeed: 2, + adsCompliantInitialization: false, + notificationPermissionPromptTrigger: PromptTrigger.rationale, + trackingNotificationPermissionPass: false, + trackingNotificationPermissionPassLimitTimes: 10, + allowInterstitialAsAlternativeReward: false, + showInternalAdsWhenBannerUnavailable: true, + ); + + @override + final adsProfile = AdsProfile( + bannerId: + const AdUnitId(android: 'xxxxxxxxxxxxxxxx', ios: 'xxxxxxxxxxxxxxxx'), + interstitialId: + const AdUnitId(android: 'xxxxxxxxxxxxxxxx', ios: 'xxxxxxxxxxxxxxxx'), + rewardsId: + const AdUnitId(android: 'xxxxxxxxxxxxxxxx', ios: 'xxxxxxxxxxxxxxxx'), + amazonAppId: const AdAppId( + android: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + ios: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'), + amazonBannerSlotId: const AdSlotId( + android: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + ios: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'), + amazonInterstitialSlotId: const AdSlotId( + android: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + ios: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'), + amazonRewardedSlotId: const AdSlotId( + android: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + ios: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'), + pubmaticAppStoreUrl: Platform.isAndroid + ? 'https://play.google.com/store/apps/details?id=app_package_id' + : ''); + + @override + final productProfile = ProductProfile( + oneOffChargeIapIds: _Guru_testProducts.oneOffChargeIapIds, + subscriptionsIapIds: _Guru_testProducts.subscriptionsIapIds, + pointsIapIds: _Guru_testProducts.pointsIapIds, + rewardIds: _Guru_testProducts.rewardIds, + igcIds: _Guru_testProducts.igcIds, + groupMap: _Guru_testProducts.groupMap, + manifestBuilders: _Guru_testProducts.manifestBuilders, + noAdsCapIds: _Guru_testProducts.noAdsCapIds, + ); + + @override + final adjustProfile = AdjustProfile( + appToken: Platform.isAndroid ? 'testapptoken' : 'testapptoken', + eventNameMapping: Platform.isAndroid + ? { + "level_start": (_) => AdjustEvent("hq0xzz"), + "iap_purchase": (params) => + AdjustProfile.createAdjustEvent("yzy3uh", params), + "sub_purchase": (params) => + AdjustProfile.createAdjustEvent("yzy3uh", params), + "level_end": (_) => AdjustEvent("so63k4"), + "tutorial_complete": (_) => AdjustEvent("95fu7q"), + } + : { + "level_start": (_) => AdjustEvent("b8khry"), + "iap_purchase": (params) => + AdjustProfile.createAdjustEvent("z0gje7", params), + "sub_purchase": (params) => + AdjustProfile.createAdjustEvent("z0gje7", params), + "level_end": (_) => AdjustEvent("1p8z5t"), + "tutorial_complete": (_) => AdjustEvent("1p8z5t"), + }); + + @override + final defaultRemoteConfig = _Guru_testRemoteConfigConstants._defaultConfigs; +} + +class _Guru_testProducts { + static final themeRegExp = RegExp(r"^theme_(.*)$"); + + static final propRegExp = RegExp(r"^theme_(.*)_(.*)$"); + + static final themeMulRegExp = RegExp(r"^theme_(.*)_(.*)$"); + + static const noAds = ProductId( + android: 'so.a.iap.noads.699', + ios: 'so.i.iap.noads.699', + attr: TransactionAttributes.asset, + points: false); + + static const noAdsCoinBundle = ProductId( + android: 'so.a.iap.noads.coin.799', + ios: 'so.i.iap.noads.coin.799', + attr: TransactionAttributes.asset, + points: false); + + static const noAds2 = ProductId( + android: 'so.a.iap.noads.699', + ios: 'so.i.iap.noads.699', + attr: TransactionAttributes.possessive, + points: false); + + static const coin200 = ProductId( + android: 'so.a.iapc.coin.200', + ios: 'so.i.iapc.coin.200', + attr: TransactionAttributes.consumable, + points: true); + + static const stagePack = ProductId( + android: 'so.a.iap.stage.1', + ios: 'so.i.iap.stage.1', + attr: TransactionAttributes.consumable, + points: false); + + static const premiumWeeklyFreetrial = ProductId( + android: 'm2.a.sub.premium', + ios: 'm2.i.sub.premium.p1w', + attr: TransactionAttributes.subscriptions, + basePlan: 'weekly', + offerId: 'freetrial', + originId: premiumWeek); + + static const premiumWeeklyDiscount = ProductId( + android: 'm2.a.sub.premium', + ios: 'm2.i.sub.premium.p1w', + attr: TransactionAttributes.subscriptions, + basePlan: 'weekly', + offerId: 'discount', + originId: premiumWeek); + + static final premiumWeekOfferIds = { + premiumWeeklyFreetrial, + premiumWeeklyDiscount + }; + + static const premiumWeek = ProductId( + android: 'm2.a.sub.premium', + ios: 'm2.i.sub.premium.p1w', + basePlan: 'weekly', + attr: TransactionAttributes.subscriptions, + points: false); + + static const premiumYearlyFreetrial = ProductId( + android: 'm2.a.sub.premium', + ios: 'm2.i.sub.premium.p1y', + attr: TransactionAttributes.subscriptions, + basePlan: 'yearly', + offerId: 'freetrial', + originId: premiumYear); + + static const premiumYearlyDiscount = ProductId( + android: 'm2.a.sub.premium', + ios: 'm2.i.sub.premium.p1y', + attr: TransactionAttributes.subscriptions, + basePlan: 'yearly', + offerId: 'discount', + originId: premiumYear); + + static final premiumYearOfferIds = { + premiumYearlyFreetrial, + premiumYearlyDiscount + }; + + static const premiumYear = ProductId( + android: 'm2.a.sub.premium', + ios: 'm2.i.sub.premium.p1y', + basePlan: 'yearly', + attr: TransactionAttributes.subscriptions, + points: false); + + static final noAdsCapIds = { + noAdsCoinBundle, + noAds2, + premiumWeek, + premiumWeeklyFreetrial, + premiumWeeklyDiscount, + premiumYear, + premiumYearlyFreetrial, + premiumYearlyDiscount + }; + + static final premiumGroup = { + premiumWeek, + premiumWeeklyFreetrial, + premiumWeeklyDiscount, + premiumYear, + premiumYearlyFreetrial, + premiumYearlyDiscount + }; + + static final groupMap = { + premiumWeek.sku: 'premium', + premiumWeeklyFreetrial.sku: 'premium', + premiumWeeklyDiscount.sku: 'premium', + premiumYear.sku: 'premium', + premiumYearlyFreetrial.sku: 'premium', + premiumYearlyDiscount.sku: 'premium' + }; + + static final oneOffChargeIapIds = { + noAds, + noAdsCoinBundle, + noAds2, + coin200, + stagePack + }; + + static final subscriptionsIapIds = { + premiumWeek, + premiumWeeklyFreetrial, + premiumWeeklyDiscount, + premiumYear, + premiumYearlyFreetrial, + premiumYearlyDiscount + }; + + static final pointsIapIds = {coin200}; + + static final rewardIds = {noAds}; + + static final igcIds = {noAds}; + + static const manifestBuilders = [ + buildNoAdsManifest, + buildNoAdsCoinBundleManifest, + buildThemeManifest, + buildPropManifest, + buildNoAds2Manifest, + buildCoin200Manifest, + buildStagePackManifest, + buildPremiumWeekManifest, + buildPremiumYearManifest, + buildThemeMulManifest + ]; + + static Future buildNoAdsManifest(TransactionIntent intent) async { + if (intent.productId != noAds) { + return null; + } + final extras = { + ExtraReservedField.scene: intent.scene, + ExtraReservedField.rate: intent.rate, + ExtraReservedField.sales: intent.sales, + }; + return Manifest('no_ads', extras: extras); + } + + static bool isOwnNoAds(OrderEntity entity) { + if (entity.state == TransactionState.success && + entity.category == 'no_ads') { + return noAds.sku == entity.sku; + } + return false; + } + + static Future buildNoAdsCoinBundleManifest( + TransactionIntent intent) async { + if (intent.productId != noAdsCoinBundle) { + return null; + } + final extras = { + ExtraReservedField.scene: intent.scene, + ExtraReservedField.rate: intent.rate, + ExtraReservedField.sales: intent.sales, + }; + final details =
[]; + details.add(Details.define( + 'igc', intent.sales ? max(500, (intent.rate * 500).toInt()) : 500)); + details.add(Details.define( + 'cup', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1)); + details.add(Details.define( + 'frag', intent.sales ? max(20, (intent.rate * 20).toInt()) : 20)); + return Manifest('no_ads', extras: extras, details: details); + } + + static bool isOwnNoAdsCoinBundle(OrderEntity entity) { + if (entity.state == TransactionState.success && + entity.category == 'no_ads_coin_bundle') { + return noAdsCoinBundle.sku == entity.sku; + } + return false; + } + + static ProductId theme( + String themeId, + TransactionMethod method, + ) => + GuruApp.instance.defineProductId( + 'theme_${themeId}', TransactionAttributes.possessive, method); + + static Future buildThemeManifest(TransactionIntent intent) async { + final matches = themeRegExp.allMatches(intent.productId.sku); + if (matches.isEmpty) { + return null; + } + final extras = { + ExtraReservedField.scene: intent.scene, + ExtraReservedField.rate: intent.rate, + ExtraReservedField.sales: intent.sales, + 'theme_id': matches.first.group(1)!, + }; + return Manifest('theme_${matches.first.group(1)!}', extras: extras); + } + + static bool isOwnTheme( + OrderEntity entity, + String themeId, + ) { + if (entity.state == TransactionState.success && + entity.category == 'theme') { + final match = themeRegExp.firstMatch(entity.sku); + return match?.group(1) == themeId; + } + return false; + } + + static ProductId prop( + String propId, + String pcId, + TransactionMethod method, + ) => + GuruApp.instance.defineProductId( + 'theme_${propId}_${pcId}', TransactionAttributes.possessive, method); + + static Future buildPropManifest(TransactionIntent intent) async { + final matches = propRegExp.allMatches(intent.productId.sku); + if (matches.isEmpty) { + return null; + } + final extras = { + ExtraReservedField.scene: intent.scene, + ExtraReservedField.rate: intent.rate, + ExtraReservedField.sales: intent.sales, + }; + final details =
[]; + details.add(Details.define( + 'prop', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1) + ..setString('prop_id', matches.first.group(1)!)); + details.add(Details.define( + 'pc', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1) + ..setString('prop_id', matches.first.group(2)!)); + return Manifest('prop', extras: extras, details: details); + } + + static bool isOwnProp( + OrderEntity entity, + String propId, + String pcId, + ) { + if (entity.state == TransactionState.success && entity.category == 'prop') { + final match = propRegExp.firstMatch(entity.sku); + return match?.group(1) == propId && match?.group(2) == pcId; + } + return false; + } + + static Future buildNoAds2Manifest(TransactionIntent intent) async { + if (intent.productId != noAds2) { + return null; + } + final extras = { + ExtraReservedField.scene: intent.scene, + ExtraReservedField.rate: intent.rate, + ExtraReservedField.sales: intent.sales, + }; + final details =
[]; + details.add(Details.define('no_ads', 1)); + return Manifest('no_ads', extras: extras, details: details); + } + + static bool isOwnNoAds2(OrderEntity entity) { + if (entity.state == TransactionState.success && + entity.category == 'no_ads2') { + return noAds2.sku == entity.sku; + } + return false; + } + + static Future buildCoin200Manifest( + TransactionIntent intent) async { + if (intent.productId != coin200) { + return null; + } + final extras = { + ExtraReservedField.scene: intent.scene, + ExtraReservedField.rate: intent.rate, + ExtraReservedField.sales: intent.sales, + }; + final details =
[]; + details.add(Details.define( + 'coin', intent.sales ? max(200, (intent.rate * 200).toInt()) : 200)); + return Manifest('coin', extras: extras, details: details); + } + + static Future buildStagePackManifest( + TransactionIntent intent) async { + if (intent.productId != stagePack) { + return null; + } + final extras = { + ExtraReservedField.scene: intent.scene, + ExtraReservedField.rate: intent.rate, + ExtraReservedField.sales: intent.sales, + }; + final details =
[]; + details.add(Details.define( + 'stage', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1) + ..setInt('stage', 1)); + return Manifest('stage_1', extras: extras, details: details); + } + + static Future buildPremiumWeekManifest( + TransactionIntent intent) async { + if (!premiumWeekOfferIds.contains(intent.productId)) { + return null; + } + final extras = { + ExtraReservedField.scene: intent.scene, + ExtraReservedField.rate: intent.rate, + ExtraReservedField.sales: intent.sales, + }; + if (Platform.isAndroid && intent.productId.hasOffer) { + extras[ExtraReservedField.basePlanId] = intent.productId.basePlan; + extras[ExtraReservedField.offerId] = intent.productId.offerId; + } + final details =
[]; + details.add(Details.define( + 'igc', intent.sales ? max(8000, (intent.rate * 8000).toInt()) : 8000)); + return Manifest('sub', extras: extras, details: details); + } + + static Future buildPremiumYearManifest( + TransactionIntent intent) async { + if (!premiumYearOfferIds.contains(intent.productId)) { + return null; + } + final extras = { + ExtraReservedField.scene: intent.scene, + ExtraReservedField.rate: intent.rate, + ExtraReservedField.sales: intent.sales, + }; + if (Platform.isAndroid && intent.productId.hasOffer) { + extras[ExtraReservedField.basePlanId] = intent.productId.basePlan; + extras[ExtraReservedField.offerId] = intent.productId.offerId; + } + final details =
[]; + details.add(Details.define('igc', + intent.sales ? max(16000, (intent.rate * 16000).toInt()) : 16000)); + return Manifest('sub', extras: extras, details: details); + } + + static ProductId themeMul( + String category, + String themeId, + ) => + GuruApp.instance.defineProductId('theme_${category}_${themeId}', + TransactionAttributes.possessive, TransactionMethod.igc); + + static Future buildThemeMulManifest( + TransactionIntent intent) async { + final matches = themeMulRegExp.allMatches(intent.productId.sku); + if (matches.isEmpty) { + return null; + } + final extras = { + ExtraReservedField.scene: intent.scene, + ExtraReservedField.rate: intent.rate, + ExtraReservedField.sales: intent.sales, + 'theme_id': matches.first.group(2)!, + 'cate': matches.first.group(1)!, + }; + return Manifest('${matches.first.group(1)!}', extras: extras); + } + + static bool isOwnThemeMul( + OrderEntity entity, + String category, + String themeId, + ) { + if (entity.state == TransactionState.success && + entity.category == 'theme_mul') { + final match = themeMulRegExp.firstMatch(entity.sku); + return match?.group(1) == category && match?.group(2) == themeId; + } + return false; + } + + static Set get iapIds => + {...oneOffChargeIapIds, ...subscriptionsIapIds}; +} + +class _SpiderRemoteConfigConstants { + static const iadsConfig = 'iads_config'; + + static const radsConfig = 'rads_config'; + + static const badsConfig = 'bads_config'; + + static const Map _defaultConfigs = { + iadsConfig: + '{"free_s":600,"win_count":4,"scene":"game_start|new_block|p2g|p2h|reset_keep|reset_scs|ads_break|double|nap","sp_scene":"new_block:120;reset_scs:120","retry_min_s":10,"retry_max_s":600,"amazon_enable":false,"imp_gap_s":120}', + radsConfig: '{"win_count":3}', + badsConfig: '{"free_s":180,"win_count":1}', + }; + + static String getDefaultConfigString(String key) => _defaultConfigs[key]; +} + +class _SpiderAppSpec extends AppSpec { + _SpiderAppSpec._(); + + static final _instance = _SpiderAppSpec._(); + + @override + final appName = 'Spider'; + + @override + final flavor = 'Spider'; + + @override + final details = AppDetails( + saasAppId: 'spider', + authority: 'solitaire.fungame.studio', + storagePrefix: + 'https://firebasestorage.googleapis.com/v0/b/solitaire-66fbf.appspot.com/o', + defaultCdnPrefix: 'https://cdn1.solitaire.fungame.studio', + androidGooglePlayUrl: + 'https://play.google.com/store/apps/details?id=solitaire.patience.card.games.klondike.free', + policyUrl: 'https://solitaire.fungame.studio/policy.html', + termsUrl: 'https://solitaire.fungame.studio/termsofservice.html', + emailUrl: 'card@fungame.studio', + packageName: 'guru.app.demo', + bundleId: 'guru.app.demo', + facebookAppId: '987654321', + ); + + @override + final deployment = Deployment( + propertyCacheSize: 512, + enableDithering: false, + disableRewardsAds: true, + ); + + @override + final adsProfile = AdsProfile( + bannerId: + const AdUnitId(android: 'a1dc70299fd5d487', ios: '97da0e2028ba80b7'), + interstitialId: + const AdUnitId(android: '25b7c47878fcbf6a', ios: '4e7ba2c4921ecdfb'), + rewardsId: + const AdUnitId(android: '3cd13a4e5c388e7b', ios: '2a65c75c3ed690b2'), + amazonAppId: const AdAppId( + android: '22296b56-f6b3-4bee-9fd1-0cd6d5cc69bc', + ios: '9fdfd4c0-3f34-4bd4-b9b4-1f649ff50a2a'), + amazonBannerSlotId: const AdSlotId( + android: '3c10ec33-a2bf-44be-ac9f-707853e63ff2', + ios: '7cb36f8a-2953-4f02-a1cb-ec3dfdf33878'), + amazonInterstitialSlotId: const AdSlotId( + android: 'b7fac191-5986-4144-9fdb-691556b2e092', + ios: '82d23cfa-2b5d-4501-bfc3-1cd2b688ed41'), + pubmaticAppStoreUrl: Platform.isAndroid + ? 'https://play.google.com/store/apps/details?id=solitaire.patience.card.games.klondike.free' + : ''); + + @override + final productProfile = ProductProfile( + oneOffChargeIapIds: _SpiderProducts.oneOffChargeIapIds, + subscriptionsIapIds: _SpiderProducts.subscriptionsIapIds, + pointsIapIds: _SpiderProducts.pointsIapIds, + rewardIds: _SpiderProducts.rewardIds, + igcIds: _SpiderProducts.igcIds, + groupMap: _SpiderProducts.groupMap, + manifestBuilders: _SpiderProducts.manifestBuilders, + noAdsCapIds: _SpiderProducts.noAdsCapIds, + ); + + @override + final adjustProfile = AdjustProfile( + appToken: Platform.isAndroid ? 'fwbn7l32vpc0' : 'xxakw3rgxnnk', + eventNameMapping: Platform.isAndroid + ? { + "level_start": (_) => AdjustEvent("hq0xzz"), + "in_app_purchase": (params) => + AdjustProfile.createAdjustEvent("yzy3uh", params), + "level_end": (_) => AdjustEvent("so63k4"), + "tutorial_complete": (_) => AdjustEvent("95fu7q"), + } + : { + "level_start": (_) => AdjustEvent("b8khry"), + "in_app_purchase": (params) => + AdjustProfile.createAdjustEvent("z0gje7", params), + "level_end": (_) => AdjustEvent("1p8z5t"), + "tutorial_complete": (_) => AdjustEvent("1p8z5t"), + }); + + @override + final defaultRemoteConfig = _SpiderRemoteConfigConstants._defaultConfigs; +} + +class _SpiderProducts { + static final themeRegExp = RegExp(r"^theme_(.*)$"); + + static const noAds = ProductId( + android: 'so.a.iap.noads.699', + ios: 'so.i.iap.noads.699', + attr: TransactionAttributes.possessive, + points: false); + + static const coin200 = ProductId( + android: 'so.a.iapc.coin.200', + ios: 'so.i.iapc.coin.200', + attr: TransactionAttributes.consumable, + points: false); + + static final noAdsCapIds = {noAds}; + + static final groupMap = {}; + + static final oneOffChargeIapIds = {}; + + static final subscriptionsIapIds = {}; + + static final pointsIapIds = {}; + + static final rewardIds = {}; + + static final igcIds = {}; + + static const manifestBuilders = [buildThemeManifest]; + + static ProductId theme( + String themeId, + TransactionMethod method, + ) => + GuruApp.instance.defineProductId( + 'theme_${themeId}', TransactionAttributes.possessive, method); + + static Future buildThemeManifest(TransactionIntent intent) async { + final matches = themeRegExp.allMatches(intent.productId.sku); + if (matches.isEmpty) { + return null; + } + final extras = { + ExtraReservedField.scene: intent.scene, + ExtraReservedField.rate: intent.rate, + ExtraReservedField.sales: intent.sales, + 'theme_id': matches.first.group(1)!, + }; + final details =
[]; + details.add(Details.define( + 'theme', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1)); + return Manifest('${matches.first.group(1)!}', + extras: extras, details: details); + } + + static bool isOwnTheme( + OrderEntity entity, + String themeId, + ) { + if (entity.state == TransactionState.success && + entity.category == 'theme') { + final match = themeRegExp.firstMatch(entity.sku); + return match?.group(1) == themeId; + } + return false; + } + + static Set get iapIds => + {...oneOffChargeIapIds, ...subscriptionsIapIds}; +} + +class RemoteConfigConstants { + static const iadsConfig = 'iads_config'; + + static const radsConfig = 'rads_config'; + + static const badsConfig = 'bads_config'; + + static const analyticsConfig = 'analytics_config'; +} + +class ProductIds { + static ProductId get noAds { + if (GuruApp.instance.flavor == 'guru_test') { + return _Guru_testProducts.noAds; + } + if (GuruApp.instance.flavor == 'Spider') { + return _SpiderProducts.noAds; + } + return ProductId.invalid; + } + + static ProductId get noAdsCoinBundle { + if (GuruApp.instance.flavor == 'guru_test') { + return _Guru_testProducts.noAdsCoinBundle; + } + return ProductId.invalid; + } + + static ProductId get noAds2 { + if (GuruApp.instance.flavor == 'guru_test') { + return _Guru_testProducts.noAds2; + } + return ProductId.invalid; + } + + static ProductId get coin200 { + if (GuruApp.instance.flavor == 'guru_test') { + return _Guru_testProducts.coin200; + } + if (GuruApp.instance.flavor == 'Spider') { + return _SpiderProducts.coin200; + } + return ProductId.invalid; + } + + static ProductId get stagePack { + if (GuruApp.instance.flavor == 'guru_test') { + return _Guru_testProducts.stagePack; + } + return ProductId.invalid; + } + + static ProductId get premiumWeeklyFreetrial { + if (GuruApp.instance.flavor == 'guru_test') { + return _Guru_testProducts.premiumWeeklyFreetrial; + } + return ProductId.invalid; + } + + static ProductId get premiumWeeklyDiscount { + if (GuruApp.instance.flavor == 'guru_test') { + return _Guru_testProducts.premiumWeeklyDiscount; + } + return ProductId.invalid; + } + + static ProductId get premiumWeek { + if (GuruApp.instance.flavor == 'guru_test') { + return _Guru_testProducts.premiumWeek; + } + return ProductId.invalid; + } + + static ProductId get premiumYearlyFreetrial { + if (GuruApp.instance.flavor == 'guru_test') { + return _Guru_testProducts.premiumYearlyFreetrial; + } + return ProductId.invalid; + } + + static ProductId get premiumYearlyDiscount { + if (GuruApp.instance.flavor == 'guru_test') { + return _Guru_testProducts.premiumYearlyDiscount; + } + return ProductId.invalid; + } + + static ProductId get premiumYear { + if (GuruApp.instance.flavor == 'guru_test') { + return _Guru_testProducts.premiumYear; + } + return ProductId.invalid; + } + + static ProductId theme( + String themeId, + TransactionMethod method, + ) { + if (GuruApp.instance.flavor == 'guru_test') { + return _Guru_testProducts.theme(themeId, method); + } + if (GuruApp.instance.flavor == 'Spider') { + return _SpiderProducts.theme(themeId, method); + } + return ProductId.invalid; + } + + static ProductId prop( + String propId, + String pcId, + TransactionMethod method, + ) { + if (GuruApp.instance.flavor == 'guru_test') { + return _Guru_testProducts.prop(propId, pcId, method); + } + return ProductId.invalid; + } + + static ProductId themeMul( + String category, + String themeId, + ) { + if (GuruApp.instance.flavor == 'guru_test') { + return _Guru_testProducts.themeMul(category, themeId); + } + return ProductId.invalid; + } + + static Set get noAdsCapIds { + if (GuruApp.instance.flavor == 'guru_test') { + return _Guru_testProducts.noAdsCapIds; + } + if (GuruApp.instance.flavor == 'Spider') { + return _SpiderProducts.noAdsCapIds; + } + return {}; + } + + static Set get iapIds => GuruApp.instance.productProfile.iapIds; + + static Set get oneOffChargeIapIds => + GuruApp.instance.productProfile.oneOffChargeIapIds; + + static Set get subscriptionsIapIds => + GuruApp.instance.productProfile.subscriptionsIapIds; + + static Set get pointsIapIds => + GuruApp.instance.productProfile.pointsIapIds; + + static Set get rewardIds => + GuruApp.instance.productProfile.rewardIds; +} + +class ProductCategory { + static const noAds = 'no_ads'; + + static const prop = 'prop'; + + static const coin = 'coin'; + + static const stage1 = 'stage_1'; + + static const sub = 'sub'; + + static theme(String themeId) { + "theme_${themeId}"; + } + + static themeMul(String category) { + "${category}"; + } +} + +class _GuruSpecFactory { + static AppSpec create(String flavor) { + if (flavor == 'guru_test') { + return _Guru_testAppSpec._instance; + } + if (flavor == 'Spider') { + return _SpiderAppSpec._instance; + } + throw NotImplementationAppSpecCreatorException(); + } +} + +class Flavors { + static const guruTest = 'guru_test'; + + static const spider = 'Spider'; +} diff --git a/guru_app/lib/utils/guru_file_utils_extension.dart b/guru_app/lib/utils/guru_file_utils_extension.dart new file mode 100644 index 0000000..3a039b8 --- /dev/null +++ b/guru_app/lib/utils/guru_file_utils_extension.dart @@ -0,0 +1,19 @@ +import 'dart:io'; + +import 'package:guru_utils/file/file_utils.dart'; +export 'package:guru_utils/file/file_utils.dart'; + +extension GuruFileUtilsExtension on FileUtils { + String get guruPath => "guru"; + + String get configPath => "config"; + + Future getGuruConfigDir(String name, {bool recursive = true}) async { + return await getAppDir(subDirs: [guruPath, configPath, name], recursive: recursive); + } + + Future getGuruConfigFile(String dirName, String name) async { + final dir = await getGuruConfigDir(dirName); + return File("${dir.path}/$name"); + } +} diff --git a/guru_app/packages/guru_assistant/.gitignore b/guru_app/packages/guru_assistant/.gitignore new file mode 100644 index 0000000..2ee8465 --- /dev/null +++ b/guru_app/packages/guru_assistant/.gitignore @@ -0,0 +1,120 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +.vscode +.gradle +.idea +/local.properties +.DS_Store +/build +.metadata +ios/Flutter/flutter_export_environment.sh + +**/.settings +**/.project +**/.classpath + + +# build id +build_id.properties + +# IntelliJ related + +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +/build/ +flutter_* +.flutter-plugins-dependencies + +# goCli related +go-cli/.packages +go-cli/pubspec.lock + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/gradlew +**/android/gradlew.bat +**/android/fastlane/report.xml +**/android/fastlane/README.md + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* +**/ios/build/* +**/ios/fastlane/README.md +**/ios/fastlane/report.xml + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages + +# Crashlytics +debugSymbols/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# i18n +**/android/res/values/strings_en.arb +lib/generated/i18n.dart + +# output +output/ios/adhok/* +output/ios/appstore/* +ios/fastlane/report.xml +android/fastlane/report.xml + +**/android/fastlane/metadata/android/**/images/** +**/ios/fastlane/screenshots/** \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/CHANGELOG.md b/guru_app/packages/guru_assistant/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/guru_app/packages/guru_assistant/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/guru_app/packages/guru_assistant/LICENSE b/guru_app/packages/guru_assistant/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/guru_app/packages/guru_assistant/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/guru_app/packages/guru_assistant/README.md b/guru_app/packages/guru_assistant/README.md new file mode 100644 index 0000000..02fe8ec --- /dev/null +++ b/guru_app/packages/guru_assistant/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/guru_app/packages/guru_assistant/analysis_options.yaml b/guru_app/packages/guru_assistant/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/guru_app/packages/guru_assistant/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/guru_app/packages/guru_assistant/example/.gitignore b/guru_app/packages/guru_assistant/example/.gitignore new file mode 100644 index 0000000..2ee8465 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/.gitignore @@ -0,0 +1,120 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +.vscode +.gradle +.idea +/local.properties +.DS_Store +/build +.metadata +ios/Flutter/flutter_export_environment.sh + +**/.settings +**/.project +**/.classpath + + +# build id +build_id.properties + +# IntelliJ related + +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +/build/ +flutter_* +.flutter-plugins-dependencies + +# goCli related +go-cli/.packages +go-cli/pubspec.lock + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/gradlew +**/android/gradlew.bat +**/android/fastlane/report.xml +**/android/fastlane/README.md + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* +**/ios/build/* +**/ios/fastlane/README.md +**/ios/fastlane/report.xml + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages + +# Crashlytics +debugSymbols/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# i18n +**/android/res/values/strings_en.arb +lib/generated/i18n.dart + +# output +output/ios/adhok/* +output/ios/appstore/* +ios/fastlane/report.xml +android/fastlane/report.xml + +**/android/fastlane/metadata/android/**/images/** +**/ios/fastlane/screenshots/** \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/example/README.md b/guru_app/packages/guru_assistant/example/README.md new file mode 100644 index 0000000..2b3fce4 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/README.md @@ -0,0 +1,16 @@ +# example + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/guru_app/packages/guru_assistant/example/analysis_options.yaml b/guru_app/packages/guru_assistant/example/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/guru_app/packages/guru_assistant/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/guru_app/packages/guru_assistant/example/android/.gitignore b/guru_app/packages/guru_assistant/example/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/guru_app/packages/guru_assistant/example/android/app/build.gradle b/guru_app/packages/guru_assistant/example/android/app/build.gradle new file mode 100644 index 0000000..d76b118 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/android/app/build.gradle @@ -0,0 +1,81 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 33 + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + + compileSdkVersion 33 + + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "guru.app.example.example" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. + minSdkVersion 23 + targetSdkVersion 33 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + multiDexEnabled true + ndk { + abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64' + } + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.android.installreferrer:installreferrer:2.2' + implementation "androidx.annotation:annotation:1.3.0" + implementation 'androidx.multidex:multidex:2.0.1' +} diff --git a/guru_app/packages/guru_assistant/example/android/app/src/debug/AndroidManifest.xml b/guru_app/packages/guru_assistant/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..3bf6195 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/guru_app/packages/guru_assistant/example/android/app/src/main/AndroidManifest.xml b/guru_app/packages/guru_assistant/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..dc16b78 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/guru_app/packages/guru_assistant/example/android/app/src/main/kotlin/guru/app/example/example/MainActivity.kt b/guru_app/packages/guru_assistant/example/android/app/src/main/kotlin/guru/app/example/example/MainActivity.kt new file mode 100644 index 0000000..77f15b0 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/android/app/src/main/kotlin/guru/app/example/example/MainActivity.kt @@ -0,0 +1,6 @@ +package guru.app.example.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/guru_app/packages/guru_assistant/example/android/app/src/main/res/drawable-v21/launch_background.xml b/guru_app/packages/guru_assistant/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/guru_app/packages/guru_assistant/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/guru_app/packages/guru_assistant/example/android/app/src/main/res/drawable/launch_background.xml b/guru_app/packages/guru_assistant/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/guru_app/packages/guru_assistant/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/guru_app/packages/guru_assistant/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/guru_app/packages/guru_assistant/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/guru_app/packages/guru_assistant/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/guru_app/packages/guru_assistant/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/guru_app/packages/guru_assistant/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/guru_app/packages/guru_assistant/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/guru_app/packages/guru_assistant/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/guru_app/packages/guru_assistant/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/guru_app/packages/guru_assistant/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/guru_app/packages/guru_assistant/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/guru_app/packages/guru_assistant/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/guru_app/packages/guru_assistant/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/guru_app/packages/guru_assistant/example/android/app/src/main/res/values-night/styles.xml b/guru_app/packages/guru_assistant/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/guru_app/packages/guru_assistant/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/guru_app/packages/guru_assistant/example/android/app/src/main/res/values/styles.xml b/guru_app/packages/guru_assistant/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/guru_app/packages/guru_assistant/example/android/app/src/profile/AndroidManifest.xml b/guru_app/packages/guru_assistant/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..3bf6195 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/guru_app/packages/guru_assistant/example/android/build.gradle b/guru_app/packages/guru_assistant/example/android/build.gradle new file mode 100644 index 0000000..8e1805c --- /dev/null +++ b/guru_app/packages/guru_assistant/example/android/build.gradle @@ -0,0 +1,39 @@ +buildscript { + ext.kotlin_version = '1.7.10' + repositories { +// maven { url 'http://localhost:8081/repository/maven-public/' } + google() + mavenCentral() + mavenLocal() + maven { url 'https://artifacts.applovin.com/android' } + } + + dependencies { +// classpath 'com.android.tools.build:gradle:4.2.1' + classpath 'com.android.tools.build:gradle:7.1.2' + classpath 'com.google.gms:google-services:4.3.15' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "com.applovin.quality:AppLovinQualityServiceGradlePlugin:+" + } +} + +allprojects { + repositories { + mavenLocal() + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/example/android/gradle.properties b/guru_app/packages/guru_assistant/example/android/gradle.properties new file mode 100644 index 0000000..999813a --- /dev/null +++ b/guru_app/packages/guru_assistant/example/android/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.jvmargs=-Xmx4G -XX:MaxPermSize=512m +android.useAndroidX=true +android.enableJetifier=true +android.enableR8=true +android.enableDexingArtifactTransform=false diff --git a/guru_app/packages/guru_assistant/example/android/gradle/wrapper/gradle-wrapper.properties b/guru_app/packages/guru_assistant/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..cb24abd --- /dev/null +++ b/guru_app/packages/guru_assistant/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/guru_app/packages/guru_assistant/example/android/settings.gradle b/guru_app/packages/guru_assistant/example/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/guru_app/packages/guru_assistant/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-Black.ttf b/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-Black.ttf new file mode 100644 index 0000000..f226c5d Binary files /dev/null and b/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-Black.ttf differ diff --git a/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-Bold.ttf b/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-Bold.ttf new file mode 100644 index 0000000..b31f604 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-Bold.ttf differ diff --git a/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-ExtraBold.ttf b/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-ExtraBold.ttf new file mode 100644 index 0000000..9faff73 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-ExtraBold.ttf differ diff --git a/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-ExtraLight.ttf b/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-ExtraLight.ttf new file mode 100644 index 0000000..b3b44ab Binary files /dev/null and b/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-ExtraLight.ttf differ diff --git a/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-Light.ttf b/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-Light.ttf new file mode 100644 index 0000000..1e2c96b Binary files /dev/null and b/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-Light.ttf differ diff --git a/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-Medium.ttf b/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-Medium.ttf new file mode 100644 index 0000000..1e09523 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-Medium.ttf differ diff --git a/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-Regular.ttf b/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-Regular.ttf new file mode 100644 index 0000000..890b507 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-Regular.ttf differ diff --git a/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-SemiBold.ttf b/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-SemiBold.ttf new file mode 100644 index 0000000..afb3658 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-SemiBold.ttf differ diff --git a/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-Thin.ttf b/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-Thin.ttf new file mode 100644 index 0000000..4b0f584 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/assets/fonts/EncodeSansExpanded-Thin.ttf differ diff --git a/guru_app/packages/guru_assistant/example/assets/images/ic_ads.png b/guru_app/packages/guru_assistant/example/assets/images/ic_ads.png new file mode 100644 index 0000000..7adbff7 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/assets/images/ic_ads.png differ diff --git a/guru_app/packages/guru_assistant/example/assets/images/ic_arrow_right.png b/guru_app/packages/guru_assistant/example/assets/images/ic_arrow_right.png new file mode 100644 index 0000000..3244ab6 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/assets/images/ic_arrow_right.png differ diff --git a/guru_app/packages/guru_assistant/example/assets/images/ic_check.png b/guru_app/packages/guru_assistant/example/assets/images/ic_check.png new file mode 100644 index 0000000..6491f55 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/assets/images/ic_check.png differ diff --git a/guru_app/packages/guru_assistant/example/assets/images/ic_close.png b/guru_app/packages/guru_assistant/example/assets/images/ic_close.png new file mode 100644 index 0000000..8c96036 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/assets/images/ic_close.png differ diff --git a/guru_app/packages/guru_assistant/example/assets/images/ic_coin.png b/guru_app/packages/guru_assistant/example/assets/images/ic_coin.png new file mode 100644 index 0000000..a6edaee Binary files /dev/null and b/guru_app/packages/guru_assistant/example/assets/images/ic_coin.png differ diff --git a/guru_app/packages/guru_assistant/example/assets/images/ic_illustration.png b/guru_app/packages/guru_assistant/example/assets/images/ic_illustration.png new file mode 100644 index 0000000..5764aa7 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/assets/images/ic_illustration.png differ diff --git a/guru_app/packages/guru_assistant/example/assets/images/remove_ads.png b/guru_app/packages/guru_assistant/example/assets/images/remove_ads.png new file mode 100644 index 0000000..73b7fa6 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/assets/images/remove_ads.png differ diff --git a/guru_app/packages/guru_assistant/example/ios/.gitignore b/guru_app/packages/guru_assistant/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/guru_app/packages/guru_assistant/example/ios/Flutter/AppFrameworkInfo.plist b/guru_app/packages/guru_assistant/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..9625e10 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/guru_app/packages/guru_assistant/example/ios/Flutter/Debug.xcconfig b/guru_app/packages/guru_assistant/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/guru_app/packages/guru_assistant/example/ios/Flutter/Release.xcconfig b/guru_app/packages/guru_assistant/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/guru_app/packages/guru_assistant/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/guru_app/packages/guru_assistant/example/ios/Podfile b/guru_app/packages/guru_assistant/example/ios/Podfile new file mode 100644 index 0000000..88359b2 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/guru_app/packages/guru_assistant/example/ios/Runner.xcodeproj/project.pbxproj b/guru_app/packages/guru_assistant/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..31e8f09 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,484 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 39253T242A; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = guru.app.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 39253T242A; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = guru.app.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 39253T242A; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = guru.app.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/guru_app/packages/guru_assistant/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/guru_app/packages/guru_assistant/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/guru_app/packages/guru_assistant/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/guru_app/packages/guru_assistant/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/guru_app/packages/guru_assistant/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/guru_app/packages/guru_assistant/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/guru_app/packages/guru_assistant/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/guru_app/packages/guru_assistant/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/guru_app/packages/guru_assistant/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/guru_app/packages/guru_assistant/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c87d15a --- /dev/null +++ b/guru_app/packages/guru_assistant/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_app/packages/guru_assistant/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/guru_app/packages/guru_assistant/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/guru_app/packages/guru_assistant/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/guru_app/packages/guru_assistant/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/guru_app/packages/guru_assistant/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/guru_app/packages/guru_assistant/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/guru_app/packages/guru_assistant/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/AppDelegate.swift b/guru_app/packages/guru_assistant/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..28c6bf0 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..f091b6b Binary files /dev/null and b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..d0ef06e Binary files /dev/null and b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..c8f9ed8 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..75b2d16 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c4df70d Binary files /dev/null and b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/guru_app/packages/guru_assistant/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/guru_app/packages/guru_assistant/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Base.lproj/Main.storyboard b/guru_app/packages/guru_assistant/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Info.plist b/guru_app/packages/guru_assistant/example/ios/Runner/Info.plist new file mode 100644 index 0000000..7f55346 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/ios/Runner/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/guru_app/packages/guru_assistant/example/ios/Runner/Runner-Bridging-Header.h b/guru_app/packages/guru_assistant/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/guru_app/packages/guru_assistant/example/lib/data/app_details.dart b/guru_app/packages/guru_assistant/example/lib/data/app_details.dart new file mode 100644 index 0000000..0cbb2d5 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/data/app_details.dart @@ -0,0 +1,6 @@ + +class AppDetails { + static const version = "3.0"; + + static const databaseVersion = 2; +} diff --git a/guru_app/packages/guru_assistant/example/lib/data/constants.dart b/guru_app/packages/guru_assistant/example/lib/data/constants.dart new file mode 100644 index 0000000..9e4199c --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/data/constants.dart @@ -0,0 +1,53 @@ +import 'dart:io'; + +class Constants { + static const String POLICY_URL = "https://m2blocks.gurugame.fun/policy.html"; + static const String TERMS_URL = "https://m2blocks.gurugame.fun/termsofservice.html"; + + static const String EMAIL_ADDRESS = "m2blocks@fungame.studio"; + + static const String AndroidAppStoreUrl = + "https://play.google.com/store/apps/details?id=merge.blocks.drop.number.puzzle.games"; + static const String IosAppStoreUrl = "https://apps.apple.com/app/id1564391515"; + + static const String InviteTranslationUrl = "https://docs.google.com/forms/d/e/1FAIpQLScFplG0dKS4XcJ_MoXbpD9-qfGFdVSIiNu-hx16psmz0GWryA/viewform?usp=sf_link"; + static const String saasAppId = "m2-block"; + static const String saasApiDevHost = "https://dev.saas.castbox.fm"; + static const String saasApiHost = "https://saas.castbox.fm"; + static const String saasApiTestHost = "mock_host"; + + static const String authority = "m2blocks.fungame.studio"; + + static const String storagePrefix = + "https://firebasestorage.googleapis.com/v0/b/m2-block.appspot.com/o"; + + static const String defaultCdnPrefix = "https://cdn2.m2blocks.fungame.studio"; + + // static const String facebookCpmCalibrationUrl = "$saasApiHost/third-party-data/api/v1/facebook/query/cpm"; + + static const String serverTimeBaseUrl = "https://saas.castbox.fm/tool/api/v1/system/time"; + + static const String storageSeparator = "%2F"; + + static const String gameStoragePath = "game"; + + static const String avatarStoragePath = "avatars"; + + static const int fatalThreshold = 5; + + static String buildAvatarConfigUrl(String configName) { + return "$storagePrefix/$avatarStoragePath$storageSeparator$configName.json?alt=media"; + } + + static String buildAvatarUrl(String configName, {String suffix = "png"}) { + return "$storagePrefix/$avatarStoragePath$storageSeparator$configName.$suffix?alt=media"; + } + + static String get storeUrl { + if (Platform.isIOS) { + return Constants.IosAppStoreUrl; + } else { + return Constants.AndroidAppStoreUrl; + } + } +} diff --git a/guru_app/packages/guru_assistant/example/lib/data/database/debug_db.dart b/guru_app/packages/guru_assistant/example/lib/data/database/debug_db.dart new file mode 100644 index 0000000..30beb8d --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/data/database/debug_db.dart @@ -0,0 +1,28 @@ +import 'package:guru_utils/property/storage/db/property_database.dart'; +import 'package:guru_utils/property/storage/property_storage.dart'; +import 'package:guru_utils/database/database.dart'; + + +final List _creatorV1 = [PropertyEntity.createTable]; + +abstract class _DebugDB extends AppDatabase with PropertyStorage {} + +class DebugDB extends _DebugDB with PropertyDatabase { + static final DebugDB instance = DebugDB._(); + + DebugDB._() { + setDatabase(this); + } + + @override + String get dbName => "debug"; + + @override + List get migrations => []; + + @override + List get tableCreators => _creatorV1; + + @override + int get version => 1; +} diff --git a/guru_app/packages/guru_assistant/example/lib/data/initializer.dart b/guru_app/packages/guru_assistant/example/lib/data/initializer.dart new file mode 100644 index 0000000..ce28b20 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/data/initializer.dart @@ -0,0 +1,88 @@ +import 'package:design/design.dart'; +import 'package:example/data/constants.dart'; +import 'package:guru_app/financial/data/db/order_database.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:guru_app/property/settings/guru_settings.dart'; +import 'package:guru_utils/packages/guru_package.dart'; +import 'package:injectable/injectable.dart'; +import 'package:guru_utils/router/router.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; + +import '../route/router.dart'; +import 'settings/debug_settings.dart'; + +part 'initializer.g.dart'; + +@guruSpecCreator +AppSpec createAppSpec(String flavor) { + return _GuruSpecFactory.create(flavor); +} + +class _RootPackage extends RootPackage { + bool migrate = false; + + @override + Future initialize() async { + // GuruAnalytics.disableAnalytics(); + Log.isDebug = kDebugMode; + + await DebugSettings.instance.refresh(); + + Initializer.initialPath = AppPages.initialPath; + + RouteCenter.initialize(routeMatchers: [ + RouteMatcher( + checker: (route) => + (route.host == GuruApp.instance.appSpec.details.authority) && + (route.path == Initializer.initialPath), + dispatcher: (uri) async { + // 忽略分享回流链接为主页的情况 + return; + }), + ]); + } + + + @override + // TODO: implement localizationsDelegates + Iterable get localizationsDelegates => const [ + // Built-in localization of basic text for Material widgets + GlobalMaterialLocalizations.delegate, + // Built-in localization for text direction LTR/RTL + GlobalWidgetsLocalizations.delegate, + // Built-in localization of basic text for Cupertino widgets + GlobalCupertinoLocalizations.delegate, + ]; + + @override + // TODO: implement supportedLocales + Iterable get supportedLocales => throw UnimplementedError(); +} + +@singleton +class Initializer { + // final TransactionService transactionService; + // final AccountService accountService; + + // final GuruAppCompat guruAppCompat; + + static String initialPath = "/"; + + bool initialized = false; + + Initializer() { + _initialize(); + } + + static _initialize() {} + + static AppEnv _buildAppEnv({String flavor = ""}) { + final rootPackage = _RootPackage(); + + return AppEnv(spec: createAppSpec(flavor), package: rootPackage); + } + + static Future ensureInitialized() async { + await GuruApp.initialize(appEnv: _buildAppEnv(flavor: Flavors.classic)); + } +} diff --git a/guru_app/packages/guru_assistant/example/lib/data/initializer.g.dart b/guru_app/packages/guru_assistant/example/lib/data/initializer.g.dart new file mode 100644 index 0000000..b74255d --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/data/initializer.g.dart @@ -0,0 +1,33 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'initializer.dart'; + +// ************************************************************************** +// GuruSpecGenerator +// ************************************************************************** + +class RemoteConfigConstants {} + +class ProductIds { + static Set get noAdsCapIds { + return {}; + } + + static Set get iapIds => GuruApp.instance.productProfile.iapIds; + static Set get oneOffChargeIapIds => + GuruApp.instance.productProfile.oneOffChargeIapIds; + static Set get subscriptionsIapIds => + GuruApp.instance.productProfile.subscriptionsIapIds; + static Set get rewardIds => + GuruApp.instance.productProfile.rewardIds; +} + +class ProductCategory {} + +class _GuruSpecFactory { + static AppSpec create(String flavor) { + throw NotImplementationAppSpecCreatorException(); + } +} + +class Flavors {} diff --git a/guru_app/packages/guru_assistant/example/lib/data/settings/ads_debug_settings.dart b/guru_app/packages/guru_assistant/example/lib/data/settings/ads_debug_settings.dart new file mode 100644 index 0000000..4e174f5 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/data/settings/ads_debug_settings.dart @@ -0,0 +1,50 @@ +import 'package:guru_utils/property/property.dart'; +import 'package:guru_utils/property/property_model.dart'; +import 'package:guru_utils/settings/settings.dart'; +// import 'package:guru_applovin_flutter/gdpr/gdpr_models.dart'; + + +class AdsDebugSettingsKeys { + static const defaultGroup = "demo"; + + static const PropertyKey forceGdpr = PropertyKey.setting("force_gdpr", group: defaultGroup); + + static const PropertyKey forceGdprType = + PropertyKey.setting("force_gdpr_type", tag: AdsDebugPropertyTags.consent, group: defaultGroup); + + static const PropertyKey consentDebugGeography = PropertyKey.setting( + "admob_consent_debug_geography", + tag: AdsDebugPropertyTags.consent, + group: defaultGroup); + + static const PropertyKey latestShownFundingChoicesTime = PropertyKey.setting( + "latest_shown_funding_choices_time", + tag: AdsDebugPropertyTags.consent, + group: defaultGroup); + + static const PropertyKey admobConsentTestDeviceId = PropertyKey.setting( + "admob_consent_test_device_id", + tag: AdsDebugPropertyTags.consent, + group: defaultGroup); + + static const PropertyKey forcePubmatic = + PropertyKey.setting("force_pubmatic", tag: AdsDebugPropertyTags.ads, group: defaultGroup); + +} + +class AdsDebugPropertyTags { + static const String consent = "consent"; + static const String ads = "ads"; +} + +class AdsDebugSettings extends Settings { + static AdsDebugSettings instance = AdsDebugSettings._(); + + final SettingData fakeInterstitialAds = SettingBoolData(UtilsPropertyKeys.fakeInterstitialAds, defaultValue: false); + + final SettingData fakeRewardedAds = SettingBoolData(UtilsPropertyKeys.fakeRewardedAds, defaultValue: false); + + final SettingData forcePubmatic = SettingBoolData(AdsDebugSettingsKeys.forcePubmatic, defaultValue: false); + + AdsDebugSettings._(); +} diff --git a/guru_app/packages/guru_assistant/example/lib/data/settings/assistant_settings.dart b/guru_app/packages/guru_assistant/example/lib/data/settings/assistant_settings.dart new file mode 100644 index 0000000..c811cdb --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/data/settings/assistant_settings.dart @@ -0,0 +1,17 @@ +import 'package:guru_utils/property/property.dart'; +import 'package:guru_utils/settings/settings.dart'; + +/// Created by Haoyi on 2023/6/5 + +class AssistantSettingsKeys { + static const defaultGroup = "demo"; + static const PropertyKey checkedTileIdx = + PropertyKey.setting("checked_tile_idx", group: defaultGroup); + static const PropertyKey switchTileValue = PropertyKey.setting("switch_tile_value", group: defaultGroup); +} + +class AssistantSettings extends Settings { + static AssistantSettings instance = AssistantSettings._(); + + AssistantSettings._(); +} diff --git a/guru_app/packages/guru_assistant/example/lib/data/settings/debug_settings.dart b/guru_app/packages/guru_assistant/example/lib/data/settings/debug_settings.dart new file mode 100644 index 0000000..df72451 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/data/settings/debug_settings.dart @@ -0,0 +1,17 @@ +import 'package:guru_utils/property/property.dart'; +import 'package:guru_utils/settings/settings.dart'; + +/// Created by Haoyi on 2023/6/5 + +class DebugSettingsKeys { + static const defaultGroup = "demo"; + static const PropertyKey checkedTileIdx = + PropertyKey.setting("checked_tile_idx", group: defaultGroup); + static const PropertyKey switchTileValue = PropertyKey.setting("switch_tile_value", group: defaultGroup); +} + +class DebugSettings extends Settings { + static DebugSettings instance = DebugSettings._(); + + DebugSettings._(); +} diff --git a/guru_app/packages/guru_assistant/example/lib/generated/assets.dart b/guru_app/packages/guru_assistant/example/lib/generated/assets.dart new file mode 100644 index 0000000..9abd032 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/generated/assets.dart @@ -0,0 +1,22 @@ +///This file is automatically generated. DO NOT EDIT, all your changes would be lost. +class Assets { + Assets._(); + + static const String fontsEncodeSansExpandedBlack = 'assets/fonts/EncodeSansExpanded-Black.ttf'; + static const String fontsEncodeSansExpandedBold = 'assets/fonts/EncodeSansExpanded-Bold.ttf'; + static const String fontsEncodeSansExpandedExtraBold = 'assets/fonts/EncodeSansExpanded-ExtraBold.ttf'; + static const String fontsEncodeSansExpandedExtraLight = 'assets/fonts/EncodeSansExpanded-ExtraLight.ttf'; + static const String fontsEncodeSansExpandedLight = 'assets/fonts/EncodeSansExpanded-Light.ttf'; + static const String fontsEncodeSansExpandedMedium = 'assets/fonts/EncodeSansExpanded-Medium.ttf'; + static const String fontsEncodeSansExpandedRegular = 'assets/fonts/EncodeSansExpanded-Regular.ttf'; + static const String fontsEncodeSansExpandedSemiBold = 'assets/fonts/EncodeSansExpanded-SemiBold.ttf'; + static const String fontsEncodeSansExpandedThin = 'assets/fonts/EncodeSansExpanded-Thin.ttf'; + static const String imagesIcAds = 'assets/images/ic_ads.png'; + static const String imagesIcArrowRight = 'assets/images/ic_arrow_right.png'; + static const String imagesIcCheck = 'assets/images/ic_check.png'; + static const String imagesIcClose = 'assets/images/ic_close.png'; + static const String imagesIcCoin = 'assets/images/ic_coin.png'; + static const String imagesIcIllustration = 'assets/images/ic_illustration.png'; + static const String imagesRemoveAds = 'assets/images/remove_ads.png'; + +} diff --git a/guru_app/packages/guru_assistant/example/lib/main.dart b/guru_app/packages/guru_assistant/example/lib/main.dart new file mode 100644 index 0000000..f3f5d8c --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/main.dart @@ -0,0 +1,64 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:example/theme/example_theme.dart'; +import 'package:get/get.dart'; +import 'package:guru_widgets/theme/guru_theme.dart'; +import 'package:guru_utils/router/router.dart'; + +import 'route/router.dart'; + + +void main() async { + runZonedGuarded( + () => runApp(GuruTheme( + guruTheme: ExampleThemes.defaultGuruTheme, + child: GetMaterialApp( + title: "GURU DEBUG", + theme: ExampleThemes.defaultLightTheme, + initialRoute: AppPages.initialPath, + routingCallback: RoutingObserver.listener, + localizationsDelegates: const [ + // Built-in localization of basic text for Material widgets + GlobalMaterialLocalizations.delegate, + // Built-in localization for text direction LTR/RTL + GlobalWidgetsLocalizations.delegate, + // Built-in localization of basic text for Cupertino widgets + GlobalCupertinoLocalizations.delegate, + ], + localeResolutionCallback: (locale, supportedLocales) { + // Check if the current device locale is supported + //supportedLocales.firstWhere((supportedLocale) => supportedLocale.languageCode == locale.languageCode, orElse: () => supportedLocales.first), + Locale? matchLocale; + if (locale != null) { + for (var supportedLocale in supportedLocales) { + if (supportedLocale.languageCode == locale.languageCode && + supportedLocale.countryCode == locale.countryCode) { + matchLocale = supportedLocale; + break; + } + } + + if (matchLocale == null) { + for (var supportedLocale in supportedLocales) { + if (supportedLocale.languageCode == locale.languageCode) { + matchLocale = supportedLocale; + break; + } + } + } + } + matchLocale = matchLocale ?? const Locale("en", "US"); + Get.locale = matchLocale; + return matchLocale; + }, + getPages: AppPages.routes, + ), + )), + _recordError); +} + +Future _recordError(dynamic exception, StackTrace stack) async { + debugPrint("record error! ===> $exception $stack"); +} \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/ads_debug_binding.dart b/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/ads_debug_binding.dart new file mode 100644 index 0000000..748df53 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/ads_debug_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'ads_debug_controller.dart'; + +class AdsDebugBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => AdsDebugController()); + } +} diff --git a/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/ads_debug_controller.dart b/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/ads_debug_controller.dart new file mode 100644 index 0000000..a92723e --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/ads_debug_controller.dart @@ -0,0 +1,21 @@ +import 'package:get/get.dart'; +import 'package:guru_utils/controller/lifecycle_controller.dart'; +import 'package:guru_utils/property/app_property.dart'; +import 'package:rxdart/rxdart.dart'; + +import '../../../data/settings/ads_debug_settings.dart'; +import 'ads_debug_model.dart'; + +class AdsDebugController extends LifecycleController { + final AdsDebugModel model = AdsDebugModel(); + + @override + void onReady() async { + super.onReady(); + } + + @override + void onClose() { + super.onClose(); + } +} diff --git a/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/ads_debug_model.dart b/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/ads_debug_model.dart new file mode 100644 index 0000000..2829a2b --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/ads_debug_model.dart @@ -0,0 +1,10 @@ +class AdsDebugModel { + AdsDebugModel() { + ///Initialize variables + } +} + +class GdprPopupType { + static const guru = "guru"; + static const admob = "admob"; +} diff --git a/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/ads_debug_view.dart b/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/ads_debug_view.dart new file mode 100644 index 0000000..96ad746 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/ads_debug_view.dart @@ -0,0 +1,265 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:example/route/router.dart'; +import 'package:guru_utils/router/router.dart'; +import 'package:guru_assistant/ui/ads_debug_page.dart' as Assistant; + +import 'ads_debug_controller.dart'; + +class AdsDebugPage extends GetWidget { + const AdsDebugPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final controller = Get.find(); + final model = Get.find().model; + + // SettingItem _buildGdprGroup() { + // if (controller.forceGdprTypeSubject.value == 0) { + // return GroupSetting( + // title: "AdMob GDPR", + // items: [ + // EntranceSetting(title: "Admob Consent Debug Geography", onTap: () async { + // final consentDebugGeographyIndex = controller.consentDebugGeographySubject.value; + // await GuruPopup.instance.showCustomDialog(child: RadioGroupWidget(consentDebugGeographyIndex, AdsDebugController.geographyName.map((t) => Text( + // t, + // style: const TextStyle(fontSize: 16, color: Colors.black), + // )) + // .toList(), onChanged: (idx) { + // controller.setForceGdprType(idx); + // })); + // }), + // EntranceSetting(title: "Admob Consent Test Device Id", onTap: () async { + + // }), + // EntranceSetting(title: "Modify Latest Shown Funding Choices Ts", summary: "", onTap: () async { + // final dt = controller.latestShownFundingChoicesTime.value; + // final selectedDate = await showDatePicker( + // context: Get.context!, + // initialDate: dt?.isAfter(DateTime(2022, 6, 10)) == true + // ? dt ?? DateTime.now() + // : DateTime.now(), + // firstDate: DateTime(2022, 6, 10), + // lastDate: DateTime.now().add(const Duration(days: 365))); + + // final selectedTime = await showTimePicker( + // context: Get.context!, + // initialTime: + // dt != null ? TimeOfDay(hour: dt.hour, minute: dt.minute) : TimeOfDay.now()); + + // final newDateTime = DateTime(selectedDate?.year ?? dt.year, selectedDate?.month ?? dt.month, + // selectedDate?.day ?? dt.day, selectedTime?.hour ?? dt.hour, selectedTime?.minute ?? dt.minute); + + // controller.setLatestShownFundingChoicesTimestamp(newDateTime); + // }) + // ] + // ); + // } else { + // return GroupSetting( + // title: "GURU GDPR", + // items: [ + // // SwitchSetting( + // // enabledTitle: "Force Show Guru Gdpr", + // // settingData: AdsDebugSettings.instance.forceGdpr, + // // onChanged: (value) { + // // AdsDebugSettings.instance.forceGdpr.set(value); + // // }), + // ] + // ); + // } + // } + + return Assistant.AdsDebugPage(onEnterAdsTest: () { + RouteCenter.instance.openPath(Routes.testAds.path()); + }); + // GuruSettingsPage( + // items: [ + // GroupSetting( + // title: "MAX SUPPORT", + // items: [ + // EntranceSetting(title: "MAX Mediation Debugger", onTap: () async { + // GuruApplovinFlutter.instance.openDebugger(); + // }), + // EntranceSetting(title: "GURU Mediation Debugger", onTap: () async { + // RouteCenter.instance.openPath(Routes.testAds.path()); + // }), + // SwitchSetting( + // enabledTitle: "Fake Interstitial Ads", + // settingData: AdsDebugSettings.instance.fakeInterstitialAds, + // onChanged: (value) { + // AdsDebugSettings.instance.fakeInterstitialAds.set(value); + // }), + // SwitchSetting( + // enabledTitle: "Fake Rewarded Ads", + // settingData: AdsDebugSettings.instance.fakeRewardedAds, + // onChanged: (value) { + // AdsDebugSettings.instance.fakeRewardedAds.set(value); + // }), + // ] + // ), + // GroupSetting( + // title: "GDPR", + // items: [ + // EntranceSetting(title: "Reset Gdpr", onTap: () async { + // controller.resetGdpr(); + // }), + // EntranceSetting(title: "GDPR TYPE", onTap: () async { + // final forceGdprTypeIndex = controller.forceGdprTypeSubject.value; + // await GuruPopup.instance.showCustomDialog(child: RadioGroupWidget(forceGdprTypeIndex, AdsDebugController.gdprType.map((t) => Text( + // t, + // style: const TextStyle(fontSize: 16, color: Colors.black), + // )) + // .toList(), onChanged: (i) { + // controller.setForceGdprType(i); + // })); + // }) + // ] + // ), + // _buildGdprGroup(), + // GroupSetting( + // title: "PUBMATIC", + // items: [ + // SwitchSetting( + // enabledTitle: "Force Pubmatic", + // settingData: AdsDebugSettings.instance.forcePubmatic, + // onChanged: (value) { + // AdsDebugSettings.instance.forcePubmatic.set(value); + // }), + // EntranceSetting(title: "Reset Pubmatic", onTap: () async { + // controller.resetPubmatic(); + // }) + // ] + // ), + // GroupSetting( + // title: "DATA", + // items: [ + // EntranceSetting(title: "Ads Remote Config", onTap: () async { + // GuruPopup.instance.showKeyValueDialog(AdsManager.instance.adsConfig.dump().entries.toList()); + // }), + // EntranceSetting(title: "Ads Properties", onTap: () async { + + // }) + // ] + // ), + // GroupSetting( + // title: "IDS", + // items: [ + // EntranceSetting(title: "Banner AdUnitId", summary: profile.bannerId.id, onTap: () async { + // Clipboard.setData(ClipboardData(text: profile.bannerId.id)); + // Log.d("saveTokenToClipboard:${profile.bannerId.id}"); + // GuruPopup.instance.showCommonToast("Copied Banner AdUnitId to your clipboard"); + // }), + // EntranceSetting(title: "Interstitial AdUnitId", summary: profile.interstitialId.id, onTap: () async { + // Clipboard.setData(ClipboardData(text: profile.interstitialId.id)); + // Log.d("saveTokenToClipboard:${profile.interstitialId.id}"); + // GuruPopup.instance.showCommonToast("Copied Interstitial AdUnitId to your clipboard"); + // }), + // EntranceSetting(title: "Rewarded AdUnitId", summary: profile.rewardsId.id, onTap: () async { + // Clipboard.setData(ClipboardData(text: profile.rewardsId.id)); + // Log.d("saveTokenToClipboard:${profile.rewardsId.id}"); + // GuruPopup.instance.showCommonToast("Copied Rewarded AdUnitId to your clipboard"); + // }) + // ] + // ) + // ] + // ); + } +} + + + + + +class RadioGroupWidget extends StatefulWidget { + final MainAxisAlignment mainAxisAlignment; + final MainAxisSize mainAxisSize; + final CrossAxisAlignment crossAxisAlignment; + + final TextDirection? textDirection; + final Axis direction; + + final int initialIndex; + final List items; + final void Function(int)? onChanged; + + RadioGroupWidget(this.initialIndex, this.items, + {this.mainAxisAlignment = MainAxisAlignment.start, + this.mainAxisSize = MainAxisSize.min, + this.crossAxisAlignment = CrossAxisAlignment.start, + this.direction = Axis.vertical, + this.textDirection, + this.onChanged}); + + @override + State createState() { + return RadioGroupState(); + } +} + +class RadioGroupState extends State { + int groupValue = 0; + + final List _items = []; + + void _refresh() { + _items.clear(); + + for (int index = 0; index < widget.items.length; ++index) { + _items.add(Row( + mainAxisSize: MainAxisSize.min, + children: [ + Radio( + value: index, + groupValue: groupValue, + onChanged: (v) { + setState(() { + groupValue = v ?? 0; + }); + widget.onChanged?.call(v ?? 0); + }, + ), + InkWell( + onTap: () { + setState(() { + groupValue = index; + }); + widget.onChanged?.call(index); + }, + child: widget.items[index], + ) + ], + )); + } + } + + @override + void initState() { + super.initState(); + groupValue = widget.initialIndex; + } + + @override + void didUpdateWidget(covariant RadioGroupWidget oldWidget) { + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + _refresh(); + if (widget.direction == Axis.vertical) { + return Column( + mainAxisAlignment: widget.mainAxisAlignment, + mainAxisSize: widget.mainAxisSize, + crossAxisAlignment: widget.crossAxisAlignment, + children: _items, + ); + } else { + return Row( + mainAxisAlignment: widget.mainAxisAlignment, + mainAxisSize: widget.mainAxisSize, + crossAxisAlignment: widget.crossAxisAlignment, + children: _items, + ); + } + } +} \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/test/test_ads_binding.dart b/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/test/test_ads_binding.dart new file mode 100644 index 0000000..20af6a7 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/test/test_ads_binding.dart @@ -0,0 +1,10 @@ +import 'package:example/pages/debug/ads/test/test_ads_controller.dart'; +import 'package:get/get.dart'; + + +class TestAdsBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => TestAdsController()); + } +} diff --git a/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/test/test_ads_controller.dart b/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/test/test_ads_controller.dart new file mode 100644 index 0000000..cecd0da --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/test/test_ads_controller.dart @@ -0,0 +1,18 @@ +import 'package:example/pages/debug/ads/test/test_ads_model.dart'; +import 'package:guru_utils/controller/aware/controller_aware.dart'; +import 'package:guru_utils/controller/ads_controller.dart'; + + +class TestAdsController extends AdsController with RewardedAware, BannerAware, InterstitialAware { + final TestAdsModel model = TestAdsModel(); + + @override + void onReady() { + super.onReady(); + } + + @override + void onClose() { + super.onClose(); + } +} diff --git a/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/test/test_ads_model.dart b/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/test/test_ads_model.dart new file mode 100644 index 0000000..c939aa1 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/test/test_ads_model.dart @@ -0,0 +1,5 @@ +class TestAdsModel { + TestAdsModel() { + ///Initialize variables + } +} diff --git a/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/test/test_ads_view.dart b/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/test/test_ads_view.dart new file mode 100644 index 0000000..086f16a --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/debug/ads/test/test_ads_view.dart @@ -0,0 +1,48 @@ +import 'package:example/pages/debug/ads/test/test_ads_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:guru_widgets/pages/settings/guru_settings_page.dart'; + + + +class TestAdsPage extends GetWidget { + const TestAdsPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final controller = Get.find(); + final model = Get.find().model; + + return GuruSettingsPage( + items: [ + GroupSetting( + title: "TEST BANNER ADS", + items: [ + EntranceSetting(title: "Pubmatic", onTap: () async { + controller.showBanner(); + }), + ] + ), + GroupSetting( + title: "TEST INTERSTITIAL ADS", + items: [ + EntranceSetting(title: "Pubmatic", onTap: () async { + controller.showInterstitialAd(scene: "test"); + }), + EntranceSetting(title: "Guru INTER2INTER", onTap: () async { + + }) + ] + ), + GroupSetting( + title: "TEST REWARDS ADS", + items: [ + EntranceSetting(title: "Pubmatic", onTap: () async { + controller.showRewardedAd(scene: "test"); + }) + ] + ) + ] + ); + } +} \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/example/lib/pages/debug/data/data_debug_binding.dart b/guru_app/packages/guru_assistant/example/lib/pages/debug/data/data_debug_binding.dart new file mode 100644 index 0000000..722e246 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/debug/data/data_debug_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'data_debug_controller.dart'; + +class DataDebugBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => DataDebugController()); + } +} diff --git a/guru_app/packages/guru_assistant/example/lib/pages/debug/data/data_debug_controller.dart b/guru_app/packages/guru_assistant/example/lib/pages/debug/data/data_debug_controller.dart new file mode 100644 index 0000000..79aca38 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/debug/data/data_debug_controller.dart @@ -0,0 +1,17 @@ +import 'package:guru_utils/controller/lifecycle_controller.dart'; + +import 'data_debug_model.dart'; + +class DataDebugController extends LifecycleController { + final DataDebugModel model = DataDebugModel(); + + @override + void onReady() async { + super.onReady(); + } + + @override + void onClose() { + super.onClose(); + } +} diff --git a/guru_app/packages/guru_assistant/example/lib/pages/debug/data/data_debug_model.dart b/guru_app/packages/guru_assistant/example/lib/pages/debug/data/data_debug_model.dart new file mode 100644 index 0000000..426fa94 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/debug/data/data_debug_model.dart @@ -0,0 +1,5 @@ +class DataDebugModel { + DataDebugModel() { + ///Initialize variables + } +} diff --git a/guru_app/packages/guru_assistant/example/lib/pages/debug/data/data_debug_view.dart b/guru_app/packages/guru_assistant/example/lib/pages/debug/data/data_debug_view.dart new file mode 100644 index 0000000..392cc20 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/debug/data/data_debug_view.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:guru_assistant/ui/ads_debug_page.dart' as Assistant; + +import 'data_debug_controller.dart'; + +class DataDebugPage extends GetWidget { + const DataDebugPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final controller = Get.find(); + final model = Get.find().model; + + return Assistant.AdsDebugPage(); + } +} \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/example/lib/pages/debug/debug_binding.dart b/guru_app/packages/guru_assistant/example/lib/pages/debug/debug_binding.dart new file mode 100644 index 0000000..c74862e --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/debug/debug_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'debug_controller.dart'; + +class DebugBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => DebugController()); + } +} diff --git a/guru_app/packages/guru_assistant/example/lib/pages/debug/debug_controller.dart b/guru_app/packages/guru_assistant/example/lib/pages/debug/debug_controller.dart new file mode 100644 index 0000000..94eb8a4 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/debug/debug_controller.dart @@ -0,0 +1,18 @@ +import 'package:get/get.dart'; +import 'package:guru_utils/controller/lifecycle_controller.dart'; + +import 'debug_model.dart'; + +class DebugController extends LifecycleController { + final DebugModel model = DebugModel(); + + @override + void onReady() { + super.onReady(); + } + + @override + void onClose() { + super.onClose(); + } +} diff --git a/guru_app/packages/guru_assistant/example/lib/pages/debug/debug_model.dart b/guru_app/packages/guru_assistant/example/lib/pages/debug/debug_model.dart new file mode 100644 index 0000000..aabc77e --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/debug/debug_model.dart @@ -0,0 +1,5 @@ +class DebugModel { + DebugModel() { + ///Initialize variables + } +} diff --git a/guru_app/packages/guru_assistant/example/lib/pages/debug/debug_view.dart b/guru_app/packages/guru_assistant/example/lib/pages/debug/debug_view.dart new file mode 100644 index 0000000..8523e97 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/debug/debug_view.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:guru_assistant/ui/guru_debug_page.dart'; +import 'package:guru_utils/router/router.dart'; + +import '../../route/router.dart'; +import 'debug_controller.dart'; + +class DebugPage extends GetWidget { + const DebugPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final controller = Get.find(); + final model = Get.find().model; + + return GuruDebugPage( + onEnterAdsDebug: () { + RouteCenter.instance.openPath(Routes.adsDebug.path()); + }, + onEnterDataViewer: () { + RouteCenter.instance.openPath(Routes.dataDebug.path()); + } + ); + } +} \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/example/lib/pages/root/roor_model.dart b/guru_app/packages/guru_assistant/example/lib/pages/root/roor_model.dart new file mode 100644 index 0000000..9768168 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/root/roor_model.dart @@ -0,0 +1,5 @@ +class RootModel { + RootModel() { + ///Initialize variables + } +} diff --git a/guru_app/packages/guru_assistant/example/lib/pages/root/root_binding.dart b/guru_app/packages/guru_assistant/example/lib/pages/root/root_binding.dart new file mode 100644 index 0000000..ed0cedc --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/root/root_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'root_controller.dart'; + +class RootBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => RootController()); + } +} diff --git a/guru_app/packages/guru_assistant/example/lib/pages/root/root_controller.dart b/guru_app/packages/guru_assistant/example/lib/pages/root/root_controller.dart new file mode 100644 index 0000000..f967a90 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/root/root_controller.dart @@ -0,0 +1,18 @@ +import 'package:get/get.dart'; +import 'package:guru_utils/controller/lifecycle_controller.dart'; + +import 'roor_model.dart'; + +class RootController extends LifecycleController { + final RootModel model = RootModel(); + + @override + void onReady() { + super.onReady(); + } + + @override + void onClose() { + super.onClose(); + } +} diff --git a/guru_app/packages/guru_assistant/example/lib/pages/root/root_view.dart b/guru_app/packages/guru_assistant/example/lib/pages/root/root_view.dart new file mode 100644 index 0000000..4a6773c --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/root/root_view.dart @@ -0,0 +1,33 @@ +import 'package:example/route/router.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:guru_widgets/tile/guru_list_tile.dart'; +import 'package:guru_utils/router/router.dart'; + +import 'root_controller.dart'; + +class RootPage extends GetWidget { + const RootPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final controller = Get.find(); + final model = Get.find().model; + + return Scaffold( + backgroundColor: const Color(0xFF121212), + appBar: AppBar( + title: const Text('Guru Assistant'), + backgroundColor: Colors.transparent, + elevation: 0, + centerTitle: true, + ), + body: Align(alignment: Alignment.topCenter, child: GuruListTile( + title: 'Settings', + onTap: () { + RouteCenter.instance.openPath(Routes.settings.path()); + }, + )), + ); + } +} diff --git a/guru_app/packages/guru_assistant/example/lib/pages/settings/settings_binding.dart b/guru_app/packages/guru_assistant/example/lib/pages/settings/settings_binding.dart new file mode 100644 index 0000000..7570a4c --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/settings/settings_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'settings_controller.dart'; + +class SettingsBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => SettingsController()); + } +} diff --git a/guru_app/packages/guru_assistant/example/lib/pages/settings/settings_controller.dart b/guru_app/packages/guru_assistant/example/lib/pages/settings/settings_controller.dart new file mode 100644 index 0000000..723fa17 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/settings/settings_controller.dart @@ -0,0 +1,48 @@ +import 'package:get/get.dart'; +import 'package:guru_utils/controller/lifecycle_controller.dart'; +import 'package:guru_utils/audio/audio_effector.dart'; +import 'package:guru_utils/vibration/vibrate_utils.dart'; +import 'package:guru_utils/vibration/vibrate_model.dart'; + +import 'settings_model.dart'; + +class SettingsController extends LifecycleController { + final SettingsModel model = SettingsModel(); + + + void switchSounds(bool value) { + final effect = value ? SoundEffect.on : SoundEffect.off; + if (effect == SoundEffect.on) { + // GuruSettings.instance.soundEffect.set(SoundEffect.off); + // GuruAnalytics.instance.logEventEx("sound_clk", itemCategory: "off", itemName: "settings"); + AudioEffector.soundEffect = SoundEffect.off; + } else { + // GuruSettings.instance.soundEffect.set(SoundEffect.on); + // GuruAnalytics.instance.logEventEx("sound_clk", itemCategory: "on", itemName: "settings"); + AudioEffector.soundEffect = SoundEffect.on; + } + } + + void switchVibration(bool value) { + final effect = value ? VibrationState.on : VibrationState.off; + if (effect == SoundEffect.on) { + VibrateUtils.disableVibrate(); + // GuruSettings.instance.vibration.set(VibrationState.off); + // GuruAnalytics.instance.logEventEx("vibration_clk", itemCategory: "off", itemName: "settings"); + } else { + VibrateUtils.enableVibrate(); + // GuruSettings.instance.vibration.set(VibrationState.on); + // GuruAnalytics.instance.logEventEx("vibration_clk", itemCategory: "on", itemName: "settings"); + } + } + + @override + void onReady() { + super.onReady(); + } + + @override + void onClose() { + super.onClose(); + } +} diff --git a/guru_app/packages/guru_assistant/example/lib/pages/settings/settings_model.dart b/guru_app/packages/guru_assistant/example/lib/pages/settings/settings_model.dart new file mode 100644 index 0000000..a579269 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/settings/settings_model.dart @@ -0,0 +1,5 @@ +class SettingsModel { + SettingsModel() { + ///Initialize variables + } +} diff --git a/guru_app/packages/guru_assistant/example/lib/pages/settings/settings_view.dart b/guru_app/packages/guru_assistant/example/lib/pages/settings/settings_view.dart new file mode 100644 index 0000000..35552d0 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/pages/settings/settings_view.dart @@ -0,0 +1,53 @@ +import 'package:example/route/router.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:guru_utils/router/router.dart'; +import 'package:guru_widgets/pages/settings/guru_settings_page.dart'; +import 'package:guru_utils/settings/settings.dart'; +import 'package:guru_utils/vibration/vibrate_model.dart'; +import 'package:guru_utils/audio/audio_effector.dart'; + + +import '../../data/settings/assistant_settings.dart'; +import '../../generated/assets.dart'; +import 'settings_controller.dart'; + +class SettingsPage extends GetWidget { + const SettingsPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final controller = Get.find(); + final model = Get.find().model; + + return GuruSettingsPage( + items: [ + GroupSetting( + title: "偏好", + items: [ + SwitchSetting( + enabledTitle: "声音", + settingData: AssistantSettings.instance.soundEffect.toBoolData((p0) => false, (p0) => SoundEffect.on), + onChanged: (value) { + controller.switchSounds(value); + }), + SwitchSetting( + enabledTitle: "震动", + settingData: AssistantSettings.instance.vibration.toBoolData((p0) => false, (p0) => VibrationState.on), + onChanged: (value) { + controller.switchVibration(value); + }), + ] + ) + ], + policyUrl: Uri.parse("www.test.com"), + termsOfServiceUrl: Uri.parse("www.baidu.com"), + onEnterDebugView: () { + RouteCenter.instance.openPath(Routes.debug.path()); + }, + onTestTap: () { + + } + ); + } +} diff --git a/guru_app/packages/guru_assistant/example/lib/route/app_pages.dart b/guru_app/packages/guru_assistant/example/lib/route/app_pages.dart new file mode 100644 index 0000000..18dcd01 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/route/app_pages.dart @@ -0,0 +1,71 @@ +part of "router.dart"; + +class AppPages { + AppPages._(); + + static String get initialPath => "/"; + + static final routes = [ + GetPage( + preventDuplicates: true, + name: Routes.root.mainPath, + page: () => const RootPage(), + transition: Transition.fadeIn, + bindings: [ + RootBinding(), + ], + title: null), + GetPage( + preventDuplicates: true, + name: Routes.settings.mainPath, + page: () => const SettingsPage(), + transition: Transition.fadeIn, + bindings: [ + SettingsBinding(), + ], + title: null), + GetPage( + preventDuplicates: true, + name: Routes.debug.mainPath, + page: () => const DebugPage(), + transition: Transition.fadeIn, + bindings: [ + DebugBinding(), + ], + title: null, + children: [ + GetPage( + preventDuplicates: true, + name: Routes.adsDebug.mainPath, + page: () => const AdsDebugPage(), + transition: Transition.fadeIn, + bindings: [ + AdsDebugBinding(), + ], + title: null, + children: [ + GetPage( + preventDuplicates: true, + name: Routes.testAds.mainPath, + page: () => const TestAdsPage(), + transition: Transition.fadeIn, + bindings: [ + TestAdsBinding(), + ], + title: null) + ] + ), + GetPage( + preventDuplicates: true, + name: Routes.adsDebug.mainPath, + page: () => const DataDebugPage(), + transition: Transition.fadeIn, + bindings: [ + DataDebugBinding(), + ], + title: null + ), + ] + ), + ]; +} diff --git a/guru_app/packages/guru_assistant/example/lib/route/router.dart b/guru_app/packages/guru_assistant/example/lib/route/router.dart new file mode 100644 index 0000000..481e98c --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/route/router.dart @@ -0,0 +1,22 @@ +import 'package:example/pages/root/root_binding.dart'; +import 'package:example/pages/root/root_view.dart'; +import 'package:example/pages/settings/settings_binding.dart'; +import 'package:example/pages/settings/settings_view.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:guru_utils/id/id_utils.dart'; +import 'package:guru_utils/router/router.dart'; + +import 'package:example/pages/debug/debug_view.dart'; +import 'package:example/pages/debug/debug_binding.dart'; +import 'package:example/pages/debug/ads/ads_debug_view.dart'; +import 'package:example/pages/debug/ads/ads_debug_binding.dart'; +import 'package:example/pages/debug/data/data_debug_view.dart'; +import 'package:example/pages/debug/data/data_debug_binding.dart'; +import 'package:example/pages/debug/ads/test/test_ads_view.dart'; +import 'package:example/pages/debug/ads//test/test_ads_binding.dart'; + + +part "./app_pages.dart"; + +part "./routes.dart"; diff --git a/guru_app/packages/guru_assistant/example/lib/route/routes.dart b/guru_app/packages/guru_assistant/example/lib/route/routes.dart new file mode 100644 index 0000000..1c363c7 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/route/routes.dart @@ -0,0 +1,38 @@ +/// Created by @Haoyi on 2021/4/13 +/// + +part of "router.dart"; + +class Routes { + static const canMuxDlg = "can_mux_dlg"; + + static const String scheme = "https"; + static const String authority = "example"; + static const root = RoutePath(""); + static const settings = RoutePath("settings"); + static const debug = RoutePath("debug"); + static const dataDebug = RoutePath("dataDebug", parentPath: debug); + static const adsDebug = RoutePath("adsDebug", parentPath: debug); + static const testAds = RoutePath("testAds", parentPath: adsDebug); + + static RouteSettings buildAnonymous( + {bool canShowBanner = false, Map? arguments}) { + final Map args = {"canShowBanner": canShowBanner}; + if (arguments != null && arguments.isNotEmpty) { + args.addAll(arguments); + } + return RouteSettings(name: IdUtils.uuidV4(), arguments: args); + } + + // anonymous path + // static String get anonymous => RouteUtils.anonymous; + + static int routeId = 0; + static final List matchers = [ + RouteMatcher(checker: (uri) { + return uri.path == "" || uri.path == "/home" || uri.path == "/"; + }, dispatcher: (uri) async { + return; + }), + ]; +} diff --git a/guru_app/packages/guru_assistant/example/lib/theme/example_theme.dart b/guru_app/packages/guru_assistant/example/lib/theme/example_theme.dart new file mode 100644 index 0000000..accbba3 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/theme/example_theme.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:guru_widgets/theme/guru_theme.dart'; + +/// Created by Haoyi on 2021/12/14 + +class ExampleThemes { + static const _primaryColor = Color(0xFF7B6CDC); + + static ThemeData get theme => defaultLightTheme; + + static GuruThemeData defaultGuruTheme = GuruThemeData( + iconScheme: const GuruIconScheme( + dialogCloseIcon: "assets/images/ic_close.png", closeIcon: "assets/images/ic_close.png"), + colorScheme: const GuruColorScheme( + primaryColor: _primaryColor, + secondaryColor: _primaryColor, + primaryContentColor: Colors.white, + secondaryContentColor: white40, + backgroundColor: Color(0xFF121212), + containerColor: Color(0xFF1D1D1D)), + ); + + static ThemeData defaultLightTheme = ThemeData( + brightness: Brightness.light, + primaryColor: _primaryColor, + fontFamily: "EncodeSansExpanded", + dialogBackgroundColor: const Color(0xFFFFFEFC), + canvasColor: Colors.transparent, + backgroundColor: Colors.white, + scaffoldBackgroundColor: Colors.white, + unselectedWidgetColor: _primaryColor, +/* timePickerTheme: TimePickerThemeData( + // dayPeriodColor: Colors.green, + entryModeIconColor: Colors.deepPurple, + hourMinuteColor: Colors.blueAccent, + hourMinuteTextColor: Colors.orangeAccent, + ),*/ + radioTheme: RadioThemeData( + overlayColor: MaterialStateProperty.all(_primaryColor.withOpacity(0.3)), + fillColor: MaterialStateProperty.all(_primaryColor)), + switchTheme: SwitchThemeData( + thumbColor: MaterialStateColor.resolveWith((Set states) { + if (states.contains(MaterialState.selected)) { + return _primaryColor; + } + return Colors.white; + }), + trackColor: MaterialStateColor.resolveWith((Set states) { + if (states.contains(MaterialState.selected)) { + return _primaryColor.withOpacity(0.2); + } + return const Color(0xFFB9B9B9); + }), + overlayColor: MaterialStateColor.resolveWith((Set states) { + if (states.contains(MaterialState.selected)) { + return _primaryColor.withOpacity(0.2); + } + return Colors.black12; + }), + ), + iconTheme: const IconThemeData(color: Colors.black), + buttonColor: _primaryColor); + + static const FontWeight fwThin = FontWeight.w100; // 纤细体 + static const FontWeight fwExtraLight = FontWeight.w200; // 加细体 + static const FontWeight fwLight = FontWeight.w300; // 细体 + static const FontWeight fwRegular = FontWeight.w400; // 常规体 + static const FontWeight fwMedium = FontWeight.w500; // 中黑体 + static const FontWeight fwSemiBold = FontWeight.w600; // 中粗体 + static const FontWeight fwBold = FontWeight.w700; // 粗体 + static const FontWeight fwExtraBold = FontWeight.w800; // 加粗体 + static const FontWeight fwBlack = FontWeight.w900; // 黑体 + + static const Color white17 = Color(0x23FFFFFF); + static const Color white22 = Color(0x38FFFFFF); + static const Color white40 = Color(0x66FFFFFF); + static const Color white44 = Color(0x70FFFFFF); + static const Color white48 = Color(0x7AFFFFFF); + static const Color white50 = Color(0x7FFFFFFF); + static const Color white60 = Color(0x99FFFFFF); + static const Color white70 = Colors.white70; + static const Color white80 = Color(0xCCFFFFFF); + static const Color white90 = Color(0xE5FFFFFF); + static const Color white94 = Color(0xF0FFFFFF); + + static const Color black17 = Color(0x23000000); + static const Color black22 = Color(0x38000000); + static const Color black40 = Color(0x66000000); + static const Color black44 = Color(0x70000000); + static const Color black48 = Color(0x7A000000); + static const Color black50 = Color(0x7F000000); + static const Color black60 = Color(0x99000000); + static const Color black70 = Color(0xB3000000); + static const Color black80 = Color(0xCC000000); + static const Color black90 = Color(0xE5000000); + static const Color black94 = Color(0xF0000000); + + static TextStyle mergeTextStyle(BuildContext context, TextStyle style) { + return DefaultTextStyle.of(context).style.merge(style); + } +} diff --git a/guru_app/packages/guru_assistant/example/lib/widgets/console_button/console_button.dart b/guru_app/packages/guru_assistant/example/lib/widgets/console_button/console_button.dart new file mode 100644 index 0000000..ea7c747 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/widgets/console_button/console_button.dart @@ -0,0 +1,52 @@ +import 'package:example/widgets/console_button/console_button_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:draggable_float_widget/draggable_float_widget.dart'; +import 'package:guru_utils/router/router.dart'; +import 'package:get/get.dart'; + + + +class ConsoleButton extends GetWidget { + + ConsoleButton({ + Key? key}) : super(key: key) { + Get.create(() => ConsoleButtonController()); + } + + @override + Widget build(BuildContext context) { + + return DraggableFloatWidget( + eventStreamController: controller.eventStreamController, + config: const DraggableFloatWidgetBaseConfig( + isFullScreen: true, + initPositionYInTop: false, + initPositionYMarginBorder: 50, + borderBottom: defaultBorderWidth, + ), + onTap: () { + controller.showOverlayEntry(); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + ), + alignment: Alignment.center, + padding: const EdgeInsets.all(5), + child: const Material( + color: Colors.transparent, + child: Text( + "Console", + style: TextStyle( + fontSize: 15, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/example/lib/widgets/console_button/console_button_controller.dart b/guru_app/packages/guru_assistant/example/lib/widgets/console_button/console_button_controller.dart new file mode 100644 index 0000000..5cc6a0a --- /dev/null +++ b/guru_app/packages/guru_assistant/example/lib/widgets/console_button/console_button_controller.dart @@ -0,0 +1,69 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:guru_utils/controller/lifecycle_controller.dart'; +import 'package:injectable/injectable.dart'; +import 'package:get/get.dart'; +import 'package:draggable_float_widget/draggable_float_widget.dart'; + + +@injectable +class ConsoleButtonController extends LifecycleController { + late StreamController eventStreamController; + late OverlayEntry overlayEntry; + + void showOverlayEntry() { + final navigatorState = Navigator.of(Get.overlayContext!, rootNavigator: false); + final overlayState = navigatorState.overlay!; + overlayState.insert(overlayEntry); + } + + void hideOverlayEntry() { + overlayEntry.remove(); + } + + ConsoleButtonController(); + + void _createOverlayEntry() { + overlayEntry = OverlayEntry( + builder: (context) => GestureDetector( + onTap: () { + hideOverlayEntry(); + }, + child: DecoratedBox( + decoration: const BoxDecoration( + color: Colors.black54, + ), + child: Material( + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Container( + color: Colors.white, + height: 400, + ) + ], + ), + ), + ), + )); + } + + @override + void onInit() { + eventStreamController = StreamController.broadcast(); + _createOverlayEntry(); + super.onInit(); + } + + @override + void onReady() { + super.onReady(); + } + + @override + void onClose() { + eventStreamController.close(); + overlayEntry.remove(); + super.onClose(); + } +} \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/example/pubspec.lock b/guru_app/packages/guru_assistant/example/pubspec.lock new file mode 100644 index 0000000..e9fca40 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/pubspec.lock @@ -0,0 +1,1414 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.flutter-io.cn" + source: hosted + version: "50.0.0" + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.12" + adjust_sdk: + dependency: transitive + description: + name: adjust_sdk + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.33.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.2.0" + android_id: + dependency: transitive + description: + name: android_id + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.3+1" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.3.7" + args: + dependency: transitive + description: + name: args + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.9.0" + auto_size_text: + dependency: transitive + description: + name: auto_size_text + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + build: + dependency: transitive + description: + name: build + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.1" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.1" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.3" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.2.7" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.6.1" + cached_network_image: + dependency: transitive + description: + name: cached_network_image + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.3" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.2" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + cloud_firestore: + dependency: transitive + description: + name: cloud_firestore + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.3.1" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.10.1" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.1" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.4.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.16.0" + connectivity_plus: + dependency: transitive + description: + name: connectivity_plus + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.6" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.4" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.5" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.5" + dartx: + dependency: transitive + description: + name: dartx + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + dbus: + dependency: transitive + description: + name: dbus + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.8" + design: + dependency: "direct dev" + description: + path: "packages/design" + ref: "v2.3.0" + resolved-ref: "57ef494b3db78c71932234986eab58a4a9063c6b" + url: "git@github.com:castbox/guru_ui.git" + source: git + version: "2.0.2" + design_generator: + dependency: "direct dev" + description: + path: "packages/design_generator" + ref: "v2.3.0" + resolved-ref: "57ef494b3db78c71932234986eab58a4a9063c6b" + url: "git@github.com:castbox/guru_ui.git" + source: git + version: "2.0.2" + design_spec: + dependency: "direct dev" + description: + path: "packages/design_spec" + ref: "v2.3.0" + resolved-ref: "57ef494b3db78c71932234986eab58a4a9063c6b" + url: "git@github.com:castbox/guru_ui.git" + source: git + version: "2.0.2" + device_apps: + dependency: transitive + description: + name: device_apps + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + device_info_plus: + dependency: transitive + description: + name: device_info_plus + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.2.2" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.0" + dio: + dependency: transitive + description: + name: dio + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.6" + draggable_float_widget: + dependency: transitive + description: + name: draggable_float_widget + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.0" + facebook_app_events: + dependency: transitive + description: + name: facebook_app_events + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.19.0" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.2" + file: + dependency: transitive + description: + name: file + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.4" + firebase_analytics: + dependency: transitive + description: + name: firebase_analytics + url: "https://pub.flutter-io.cn" + source: hosted + version: "10.1.0" + firebase_analytics_platform_interface: + dependency: transitive + description: + name: firebase_analytics_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.3.17" + firebase_analytics_web: + dependency: transitive + description: + name: firebase_analytics_web + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.1+8" + firebase_auth: + dependency: transitive + description: + name: firebase_auth + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.2.4" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.11.7" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.2.4" + firebase_core: + dependency: transitive + description: + name: firebase_core + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.8.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.6.0" + firebase_crashlytics: + dependency: transitive + description: + name: firebase_crashlytics + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.9" + firebase_crashlytics_platform_interface: + dependency: transitive + description: + name: firebase_crashlytics_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.3.11" + firebase_dynamic_links: + dependency: transitive + description: + name: firebase_dynamic_links + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.11" + firebase_dynamic_links_platform_interface: + dependency: transitive + description: + name: firebase_dynamic_links_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.3+26" + firebase_in_app_messaging: + dependency: transitive + description: + name: firebase_in_app_messaging + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.0+10" + firebase_in_app_messaging_platform_interface: + dependency: transitive + description: + name: firebase_in_app_messaging_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.1+29" + firebase_messaging: + dependency: transitive + description: + name: firebase_messaging + url: "https://pub.flutter-io.cn" + source: hosted + version: "14.2.1" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.2.10" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.11" + firebase_remote_config: + dependency: transitive + description: + name: firebase_remote_config + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.9" + firebase_remote_config_platform_interface: + dependency: transitive + description: + name: firebase_remote_config_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.29" + firebase_remote_config_web: + dependency: transitive + description: + name: firebase_remote_config_web + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.18" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_blurhash: + dependency: transitive + description: + name: flutter_blurhash + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.3.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_native_timezone: + dependency: transitive + description: + name: flutter_native_timezone + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + flutter_spinkit: + dependency: transitive + description: + name: flutter_spinkit + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.2.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fluttertoast: + dependency: transitive + description: + name: fluttertoast + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.0.9" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.0" + get: + dependency: transitive + description: + name: get + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.6.5" + get_it: + dependency: transitive + description: + name: get_it + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.6.0" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.1" + guru_analytics_flutter: + dependency: transitive + description: + path: "." + ref: "v2.3.1" + resolved-ref: e4438b7ece793a85da477b685e60c79981be281a + url: "git@github.com:castbox/guru_analytics_flutter.git" + source: git + version: "2.0.0" + guru_app: + dependency: "direct dev" + description: + path: "." + ref: "v2.3.0" + resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2" + url: "git@github.com:castbox/guru_app.git" + source: git + version: "2.1.0" + guru_applifecycle_flutter: + dependency: transitive + description: + path: "." + ref: main + resolved-ref: d2f20e0000e0f46bdf245e35687b03e5c09ef61d + url: "git@github.com:castbox/guru_applifecycle_flutter.git" + source: git + version: "0.0.1" + guru_applovin_flutter: + dependency: transitive + description: + path: "." + ref: "v2.3.8" + resolved-ref: "6fb5191d4cffc6a5728df7ee8af793fcc27fd081" + url: "git@github.com:castbox/guru_applovin_flutter.git" + source: git + version: "2.3.0" + guru_assistant: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" + guru_navigator: + dependency: transitive + description: + path: "plugins/guru_navigator" + ref: "v2.3.0" + resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2" + url: "git@github.com:castbox/guru_app.git" + source: git + version: "0.0.1" + guru_platform_data: + dependency: transitive + description: + path: "plugins/guru_platform_data" + ref: "v2.3.0" + resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2" + url: "git@github.com:castbox/guru_app.git" + source: git + version: "0.0.1" + guru_popup: + dependency: transitive + description: + path: "packages/guru_popup" + ref: "v2.3.0" + resolved-ref: "57ef494b3db78c71932234986eab58a4a9063c6b" + url: "git@github.com:castbox/guru_ui.git" + source: git + version: "2.3.0" + guru_spec: + dependency: "direct dev" + description: + path: "packages/guru_spec" + ref: "v2.3.0" + resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2" + url: "git@github.com:castbox/guru_app.git" + source: git + version: "1.1.0" + guru_utils: + dependency: "direct dev" + description: + path: "packages/guru_utils" + ref: "v2.3.0" + resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2" + url: "git@github.com:castbox/guru_app.git" + source: git + version: "2.1.0" + guru_widgets: + dependency: "direct dev" + description: + path: "packages/guru_widgets" + ref: "v2.3.0" + resolved-ref: "57ef494b3db78c71932234986eab58a4a9063c6b" + url: "git@github.com:castbox/guru_ui.git" + source: git + version: "2.2.0" + http: + dependency: transitive + description: + name: http + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.13.5" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.2" + image: + dependency: transitive + description: + name: image + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.17" + in_app_purchase: + dependency: transitive + description: + name: in_app_purchase + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.7" + in_app_purchase_android: + dependency: transitive + description: + name: in_app_purchase_android + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.0+8" + in_app_purchase_platform_interface: + dependency: transitive + description: + name: in_app_purchase_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.4" + in_app_purchase_storekit: + dependency: transitive + description: + name: in_app_purchase_storekit + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.6+4" + injectable: + dependency: transitive + description: + name: injectable + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + injectable_generator: + dependency: "direct dev" + description: + name: injectable_generator + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.17.0" + io: + dependency: transitive + description: + name: io + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.4" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.8.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + logger: + dependency: transitive + description: + name: logger + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + lottie: + dependency: transitive + description: + name: lottie + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.12" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" + nm: + dependency: transitive + description: + name: nm + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.0" + octo_image: + dependency: transitive + description: + name: octo_image + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.2" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + path: + dependency: transitive + description: + name: path + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.2" + path_parsing: + dependency: transitive + description: + name: path_parsing + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + path_provider: + dependency: transitive + description: + name: path_provider + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.11" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.27" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.11" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.11" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.7" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.6" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.7" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.11.1" + permission_handler: + dependency: transitive + description: + name: permission_handler + url: "https://pub.flutter-io.cn" + source: hosted + version: "10.4.3" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + url: "https://pub.flutter-io.cn" + source: hosted + version: "10.3.3" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + url: "https://pub.flutter-io.cn" + source: hosted + version: "9.1.4" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.11.3" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.3" + persistent: + dependency: transitive + description: + path: "plugins/persistent" + ref: "v2.3.0" + resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2" + url: "git@github.com:castbox/guru_app.git" + source: git + version: "0.0.1" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.1.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + pointycastle: + dependency: transitive + description: + name: pointycastle + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.7.3" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.1" + process: + dependency: transitive + description: + name: process + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.2.4" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.3" + recase: + dependency: transitive + description: + name: recase + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.0" + retrofit: + dependency: transitive + description: + name: retrofit + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.3.1" + retrofit_generator: + dependency: "direct dev" + description: + name: retrofit_generator + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.2.0" + rxdart: + dependency: transitive + description: + name: rxdart + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.27.7" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + soundpool: + dependency: transitive + description: + path: "plugins/soundpool" + ref: "v2.3.0" + resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2" + url: "git@github.com:castbox/guru_app.git" + source: git + version: "2.3.0" + soundpool_macos: + dependency: transitive + description: + name: soundpool_macos + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + soundpool_platform_interface: + dependency: transitive + description: + name: soundpool_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + soundpool_web: + dependency: transitive + description: + name: soundpool_web + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.6" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.0" + sqflite: + dependency: transitive + description: + name: sqflite + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.5+1" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" + system_clock: + dependency: transitive + description: + name: system_clock + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.12" + time: + dependency: transitive + description: + name: time + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + tuple: + dependency: transitive + description: + name: tuple + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.2" + url_launcher: + dependency: transitive + description: + name: url_launcher + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.10" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.0.36" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.5" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.17" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.6" + uuid: + dependency: transitive + description: + name: uuid + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.7" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + vibration: + dependency: transitive + description: + path: "plugins/vibration" + ref: "v2.3.0" + resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2" + url: "git@github.com:castbox/guru_app.git" + source: git + version: "1.7.5" + vibration_web: + dependency: transitive + description: + name: vibration_web + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.6.5" + vm_service: + dependency: transitive + description: + name: vm_service + url: "https://pub.flutter-io.cn" + source: hosted + version: "11.2.0" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.0" + webview_flutter: + dependency: transitive + description: + name: webview_flutter + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.2.2" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.9.3" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.7.3" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.4" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.0" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.1" +sdks: + dart: ">=2.18.6 <3.0.0" + flutter: ">=3.3.0" diff --git a/guru_app/packages/guru_assistant/example/pubspec.yaml b/guru_app/packages/guru_assistant/example/pubspec.yaml new file mode 100644 index 0000000..d5332f6 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/pubspec.yaml @@ -0,0 +1,161 @@ +name: example +description: A new Flutter project. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: '>=2.18.6 <3.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + guru_assistant: + path: ../ + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + +dev_dependencies: + + build_runner: 2.3.3 + + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + + injectable_generator: 2.1.3 + retrofit_generator: 4.2.0 + + guru_app: + git: + url: git@git.chengdu.pundit.company:castbox/guru_sdk.git + path: guru_app + ref: v3.0.0 + guru_utils: + git: + url: git@git.chengdu.pundit.company:castbox/guru_sdk.git + path: guru_app/packages/guru_utils + ref: v3.0.0 + + guru_spec: + git: + url: git@git.chengdu.pundit.company:castbox/guru_sdk.git + path: guru_app/packages/guru_spec + ref: v3.0.0 + + guru_widgets: + git: + url: git@git.chengdu.pundit.company:castbox/guru_sdk.git + path: guru_ui/packages/guru_widgets + ref: v3.0.0 + + design: + git: + url: git@git.chengdu.pundit.company:castbox/guru_sdk.git + path: guru_ui/packages/design + ref: v3.0.0 + + design_spec: + git: + url: git@git.chengdu.pundit.company:castbox/guru_sdk.git + path: guru_ui/packages/design_spec + ref: v3.0.0 + + design_generator: + git: + url: git@git.chengdu.pundit.company:castbox/guru_sdk.git + path: guru_ui/packages/design_generator + ref: v3.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - assets/ + - assets/images/ + - assets/fonts/ + + fonts: + - family: EncodeSansExpanded + fonts: + - asset: assets/fonts/EncodeSansExpanded-Regular.ttf + - asset: assets/fonts/EncodeSansExpanded-Thin.ttf + weight: 100 + - asset: assets/fonts/EncodeSansExpanded-Light.ttf + weight: 200 + - asset: assets/fonts/EncodeSansExpanded-Regular.ttf + weight: 400 + - asset: assets/fonts/EncodeSansExpanded-Medium.ttf + weight: 500 + - asset: assets/fonts/EncodeSansExpanded-SemiBold.ttf + weight: 600 + - asset: assets/fonts/EncodeSansExpanded-Bold.ttf + weight: 700 + - asset: assets/fonts/EncodeSansExpanded-ExtraBold.ttf + weight: 800 + - asset: assets/fonts/EncodeSansExpanded-Black.ttf + weight: 900 + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/guru_app/packages/guru_assistant/example/test/widget_test.dart b/guru_app/packages/guru_assistant/example/test/widget_test.dart new file mode 100644 index 0000000..2e19d77 --- /dev/null +++ b/guru_app/packages/guru_assistant/example/test/widget_test.dart @@ -0,0 +1,17 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:example/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + }); +} diff --git a/guru_app/packages/guru_assistant/lib/console/console.dart b/guru_app/packages/guru_assistant/lib/console/console.dart new file mode 100644 index 0000000..15b3ea1 --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/console/console.dart @@ -0,0 +1,110 @@ +import 'dart:async'; + +import 'package:design/design.dart'; +import 'package:flutter/rendering.dart'; +import 'package:guru_widgets/button/guru_button.dart'; +import 'package:guru_widgets/guru_widgets.dart'; +import 'package:guru_widgets/image/adaptive_image.dart'; +import 'package:guru_widgets/theme/guru_theme.dart'; +import 'package:guru_utils/router/router.dart'; + + +class ConsolePopup extends StatefulWidget { + final VoidCallback? onClosed; + + const ConsolePopup( + {super.key, + this.onClosed}); + + @override + State createState() => _ConsolePopupState(); +} + +class _ConsolePopupState extends State { + late GuruDialogContainerDesignSpec designSpec; + late GuruDialogDesignSpec dialogSpec; + late GuruThemeData guruTheme; + + @override + void initState() { + super.initState(); + designSpec = GuruDialogContainerDesignSpec.get(); + dialogSpec = designSpec.dialogSpec; + } + + @override + Widget build(BuildContext context) { + final td = Directionality.of(context); + guruTheme = GuruTheme.of(context); + final closeIcon = guruTheme.iconScheme.closeIcon; + const spacer = SizedSpacer(height: 8, width: 8); + final stackItems = [ + Positioned.directional( + textDirection: td, + top: dialogSpec.closeButtonMargin.top, + start: dialogSpec.closeButtonMargin.start, + child: TapWidget( + inkWell: true, + shape: const CircleBorder(), + onTap: () { + widget.onClosed!(); + }, + child: SizedBox( + width: dialogSpec.closeButtonSize, + height: dialogSpec.closeButtonSize, + child: Center( + child: (closeIcon != null) + ? Image.asset(closeIcon, + fit: BoxFit.contain, + width: dialogSpec.closeIconSize, + height: dialogSpec.closeIconSize) + : Icon(Icons.close, size: dialogSpec.closeIconSize))), + )), + Padding( + padding: dialogSpec.contentPadding, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + GuruButton( + size: const Size(271, 48), + action: "Paint Size", + onPressed: () { + debugPaintSizeEnabled = !debugPaintSizeEnabled; + }, + ), + spacer, + GuruButton( + size: const Size(271, 48), + action: "Paint Baselines", + onPressed: () { + debugPaintBaselinesEnabled = !debugPaintBaselinesEnabled; + }, + ), + spacer, + GuruButton( + size: const Size(271, 48), + action: "Paint Layer Borders", + onPressed: () { + debugPaintLayerBordersEnabled = !debugPaintLayerBordersEnabled; + }, + ), + spacer, + GuruButton( + size: const Size(271, 48), + action: "Paint Pointers", + onPressed: () { + debugPaintPointersEnabled = !debugPaintPointersEnabled; + }, + ), + ], + ), + ) + ]; + + return Stack( + fit: StackFit.loose, + alignment: Alignment.center, + children: stackItems, + ); + } +} diff --git a/guru_app/packages/guru_assistant/lib/console/console_button.dart b/guru_app/packages/guru_assistant/lib/console/console_button.dart new file mode 100644 index 0000000..3c9a0a8 --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/console/console_button.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:draggable_float_widget/draggable_float_widget.dart'; +import 'package:guru_utils/router/router.dart'; +import 'package:get/get.dart'; + +import 'console_button_controller.dart'; + + + +class ConsoleButton extends GetWidget { + + ConsoleButton({ + Key? key}) : super(key: key) { + Get.create(() => ConsoleButtonController()); + } + + @override + Widget build(BuildContext context) { + + return DraggableFloatWidget( + eventStreamController: controller.eventStreamController, + config: const DraggableFloatWidgetBaseConfig( + isFullScreen: true, + initPositionYInTop: false, + initPositionYMarginBorder: 50, + borderBottom: defaultBorderWidth, + ), + onTap: () { + controller.showOverlayEntry(); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + ), + alignment: Alignment.center, + padding: const EdgeInsets.all(5), + child: const Material( + color: Colors.transparent, + child: Text( + "Console", + style: TextStyle( + fontSize: 15, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/lib/console/console_button_controller.dart b/guru_app/packages/guru_assistant/lib/console/console_button_controller.dart new file mode 100644 index 0000000..e424458 --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/console/console_button_controller.dart @@ -0,0 +1,88 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:guru_utils/controller/lifecycle_controller.dart'; +import 'package:get/get.dart'; +import 'package:draggable_float_widget/draggable_float_widget.dart'; +import 'package:guru_widgets/animation/standard/transform_animation.dart'; + +import 'console.dart'; + + +class ConsoleButtonController extends LifecycleController { + late StreamController eventStreamController; + late OverlayEntry overlayEntry; + + void showOverlayEntry() { + final navigatorState = Navigator.of(Get.overlayContext!, rootNavigator: false); + final overlayState = navigatorState.overlay!; + overlayState.insert(overlayEntry); + } + + void hideOverlayEntry() { + overlayEntry.remove(); + } + + ConsoleButtonController(); + + void _createOverlayEntry() { + overlayEntry = OverlayEntry( + builder: (BuildContext context) => GestureDetector( + onTap: () { + hideOverlayEntry(); + }, + child: DecoratedBox( + decoration: const BoxDecoration( + color: Colors.transparent, + ), + child: Stack( + children: [ + Positioned( + bottom: 0, + left: 0, + child: AnimatedTransformBuilder.single( + fromTranslation: const Offset(0, 300), + toTranslation: Offset.zero, + alignment: Alignment.topCenter, + curve: const Cubic(0.175, 0.885, 0.32, 1.1), + duration: const Duration(milliseconds: 300), + child: GestureDetector( + onTap: () {}, + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20)) + ), + height: 300, + width: MediaQuery.of(context).size.width, + child: ConsolePopup(onClosed: () { + hideOverlayEntry(); + }), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + @override + void onInit() { + eventStreamController = StreamController.broadcast(); + _createOverlayEntry(); + super.onInit(); + } + + @override + void onReady() { + super.onReady(); + } + + @override + void onClose() { + eventStreamController.close(); + super.onClose(); + } +} \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/lib/console/vm_helper.dart b/guru_app/packages/guru_assistant/lib/console/vm_helper.dart new file mode 100644 index 0000000..f61e2dd --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/console/vm_helper.dart @@ -0,0 +1,257 @@ +import 'dart:ui'; + +import 'dart:async'; +import 'dart:developer'; +import 'package:collection/collection.dart' show IterableExtension; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:vm_service/utils.dart'; +import 'package:vm_service/vm_service.dart'; +import 'package:vm_service/vm_service_io.dart'; + +class VMHelper with ChangeNotifier { + factory VMHelper() => _vMHelper; + + VMHelper._() { + assert(() { + _startConnect().whenComplete(() { + _timer = Timer.periodic(const Duration(seconds: 2), (Timer timer) { + VMHelper()._updateMemoryUsage().whenComplete(() { + notifyListeners(); + }); + }); + }); + return true; + }()); + } + + static final VMHelper _vMHelper = VMHelper._(); + + late bool connected; + VmService? serviceClient; + VM? vm; + + late MemoryUsage mainMemoryUsage; + late Timer _timer; + List mainHistoryMemoryInfo = []; + + IsolateRef? get main => vm!.isolates! + .firstWhereOrNull((IsolateRef element) => element.name == 'main'); + + int get count => _count; + int _count = 0; + + Future _startConnect() async { + try { + final ServiceProtocolInfo info = await Service.getInfo(); + if (info.serverUri == null) { + print('Service protocol url is null, start vm service fail.'); + return; + } + final Uri uri = convertToWebSocketUrl( + serviceProtocolUrl: info.serverUri!, + ); + serviceClient = await vmServiceConnectUri( + uri.toString(), + log: StdoutLog(), + ); + print('Socket connected in service $info'); + connected = true; + vm = await serviceClient!.getVM(); + await _updateMemoryUsage(); + } catch (e) { + print( + 'Socket connect failed due to $e. ' + "Make sure you're not using simulators.", + ); + } + } + + Future _updateMemoryUsage() async { + if (vm != null && connected) { + final MemoryUsage memoryUsage = await serviceClient!.getMemoryUsage( + main!.id!, + ); + mainMemoryUsage = memoryUsage; + final MyMemoryUsage latest = MyMemoryUsage.copyFromMemoryUsage( + memoryUsage, + ); + mainHistoryMemoryInfo.add(latest); + mainHistoryMemoryInfo.removeWhere( + (MyMemoryUsage element) => element.dataTime.isBefore( + latest.dataTime.subtract(const Duration(minutes: 1)), + ), + ); + } + } + + void clear() { + _count = 0; + mainHistoryMemoryInfo.clear(); + } + + @override + void dispose() { + _timer.cancel(); + super.dispose(); + } + + void forceGC() { + if (kDebugMode) + serviceClient?.getAllocationProfile(main?.id ?? '', gc: true); + } +} + +class MyMemoryUsage { + MyMemoryUsage({ + required int externalUsage, + required int heapCapacity, + required int heapUsage, + }) : dataTime = DateTime.now(), + externalUsage = externalUsage / 1024 / 1024, + heapCapacity = heapCapacity / 1024 / 1024, + heapUsage = heapUsage / 1024 / 1024; + + final DateTime dataTime; + + /// The amount of non-Dart memory that is retained by Dart objects. For + /// example, memory associated with Dart objects through APIs such as + /// Dart_NewFinalizableHandle, Dart_NewWeakPersistentHandle and + /// Dart_NewExternalTypedData. This usage is only as accurate as the values + /// supplied to these APIs from the VM embedder or native extensions. This + /// external memory applies GC pressure, but is separate from heapUsage and + /// heapCapacity. + final double externalUsage; + + /// The total capacity of the heap in bytes. This is the amount of memory used + /// by the Dart heap from the perspective of the operating system. + final double heapCapacity; + + /// The current heap memory usage in bytes. Heap usage is always less than or + /// equal to the heap capacity. + final double heapUsage; + + static MyMemoryUsage copyFromMemoryUsage(MemoryUsage memoryUsage) => + MyMemoryUsage( + externalUsage: memoryUsage.externalUsage!, + heapCapacity: memoryUsage.heapCapacity!, + heapUsage: memoryUsage.heapUsage!, + ); + + double toDouble(double d) { + return double.parse(d.toStringAsFixed(2)); + } +} + +class StdoutLog extends Log { + @override + void warning(String message) => print(message); + + @override + void severe(String message) => print(message); +} + +class ByteUtil { + const ByteUtil._(); + + static String toByteString(int bytes) { + if (bytes <= 1024) { + return '${bytes}B'; + } else if (bytes <= 1024 * 1024) { + return '${(bytes / (1024)).toStringAsFixed(2)}K'; + } else { + return '${(bytes / (1024 * 1024)).toStringAsFixed(2)}M'; + } + } +} + + +class MemoryUsageView extends StatefulWidget { + @override + _MemoryUsageViewState createState() => _MemoryUsageViewState(); +} + +class _MemoryUsageViewState extends State { + @override + void initState() { + super.initState(); + VMHelper().addListener(_updateMemoryUsage); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + } + + void _updateMemoryUsage() { + if (mounted) { + setState(() {}); + } + } + + @override + void dispose() { + VMHelper().removeListener(_updateMemoryUsage); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (VMHelper().serviceClient == null) { + return Container(); + } + final MemoryUsage main = VMHelper().mainMemoryUsage; + + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + const Text('Used: '), + const SizedBox( + height: 5, + ), + Text( + ByteUtil.toByteString(main.heapUsage!), + style: const TextStyle( + color: Colors.red, + height: 1.4, + fontSize: 16, + ), + ), + Divider( + color: Colors.white.withOpacity(0.1), + thickness: 1, + ), + const Text('Capacity: '), + const SizedBox( + height: 5, + ), + Text( + ByteUtil.toByteString(main.heapCapacity!), + style: const TextStyle( + color: Colors.blue, + height: 1.4, + fontSize: 16, + ), + ), + Divider( + color: Colors.white.withOpacity(0.1), + thickness: 1, + ), + const Text('External: '), + const SizedBox( + height: 5, + ), + Text( + ByteUtil.toByteString(main.externalUsage!), + style: const TextStyle( + color: Colors.green, + height: 1.4, + fontSize: 16, + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/lib/core/model.dart b/guru_app/packages/guru_assistant/lib/core/model.dart new file mode 100644 index 0000000..783b45f --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/core/model.dart @@ -0,0 +1,4 @@ +class GdprPopupType { + static const guru = "guru"; + static const admob = "admob"; +} \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/lib/core/property_keys.dart b/guru_app/packages/guru_assistant/lib/core/property_keys.dart new file mode 100644 index 0000000..bfe8758 --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/core/property_keys.dart @@ -0,0 +1,42 @@ +import 'package:guru_utils/property/property.dart'; + + +class DataViewerPropertyKeys { + static const defaultGroup = "demo"; + + static const PropertyKey globalFloatingWindow = PropertyKey.setting("global_floating_window", group: defaultGroup); +} + + +class AdsDebugPropertyKeys { + static const defaultGroup = "demo"; + + static const PropertyKey forceGdpr = PropertyKey.setting("force_gdpr", group: defaultGroup); + + static const PropertyKey forceGdprType = + PropertyKey.setting("force_gdpr_type", tag: AdsDebugPropertyTags.consent, group: defaultGroup); + + static const PropertyKey consentDebugGeography = PropertyKey.setting( + "admob_consent_debug_geography", + tag: AdsDebugPropertyTags.consent, + group: defaultGroup); + + static const PropertyKey latestShownFundingChoicesTime = PropertyKey.setting( + "latest_shown_funding_choices_time", + tag: AdsDebugPropertyTags.consent, + group: defaultGroup); + + static const PropertyKey admobConsentTestDeviceId = PropertyKey.setting( + "admob_consent_test_device_id", + tag: AdsDebugPropertyTags.consent, + group: defaultGroup); + + static const PropertyKey forcePubmatic = + PropertyKey.setting("force_pubmatic", tag: AdsDebugPropertyTags.ads, group: defaultGroup); + +} + +class AdsDebugPropertyTags { + static const String consent = "consent"; + static const String ads = "ads"; +} \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/lib/data/setting.dart b/guru_app/packages/guru_assistant/lib/data/setting.dart new file mode 100644 index 0000000..6053635 --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/data/setting.dart @@ -0,0 +1,81 @@ +import 'package:guru_utils/property/property_model.dart'; +import 'package:guru_utils/settings/settings.dart'; +import 'package:guru_utils/property/runtime_property.dart'; +import 'package:guru_utils/property/app_property.dart'; +import 'package:guru_app/ads/ads_manager.dart'; +import 'package:guru_app/property/settings/guru_settings.dart'; + +import '../core/property_keys.dart'; + +class DebugModeSettings extends Settings { + static DebugModeSettings instance = DebugModeSettings._(); + + final SettingData fakeInterstitialAds = SettingBoolData(UtilsPropertyKeys.fakeInterstitialAds, defaultValue: RuntimeProperty.instance.getBool(UtilsPropertyKeys.fakeInterstitialAds, defValue: false)); + + final SettingData fakeRewardedAds = SettingBoolData(UtilsPropertyKeys.fakeRewardedAds, defaultValue: RuntimeProperty.instance.getBool(UtilsPropertyKeys.fakeRewardedAds, defValue: false)); + + final SettingData forcePubmatic = SettingBoolData(UtilsPropertyKeys.forcePubmatic, defaultValue: false); + + final SettingData forceGdpr = SettingBoolData(UtilsPropertyKeys.forceGdpr, defaultValue: false); + + final SettingData globalFloatingWindow = SettingBoolData(DataViewerPropertyKeys.globalFloatingWindow, defaultValue: false); + + DebugModeSettings._(); +} + +class KeywordsTestSetting { + static KeywordsTestSetting instance = KeywordsTestSetting._(); + + KeywordsTestSetting._(); + + static const tag = "keywords"; + + static const appVersionEnabledKey = PropertyKey.setting("app_version_enabled", tag: tag); + static const ltEnabledKey = PropertyKey.setting("lt_enabled", tag: tag); + static const paidEnabledKey = PropertyKey.setting("paid_enabled", tag: tag); + static const appVersionKey = PropertyKey.setting("app_version", tag: tag); + static const paidKey = PropertyKey.setting("paid", tag: tag); + static const ltValueKey = PropertyKey.setting("lt_value", tag: tag); + + final RuntimeSettingBoolData appVersionEnabled = + RuntimeSettingBoolData(appVersionEnabledKey, defaultValue: false); + final RuntimeSettingBoolData ltEnabled = + RuntimeSettingBoolData(ltEnabledKey, defaultValue: false); + final RuntimeSettingBoolData paidEnabled = + RuntimeSettingBoolData(paidEnabledKey, defaultValue: false); + + final RuntimeSettingStringData appVersion = + RuntimeSettingStringData(appVersionKey, defaultValue: '3.8.0'); + final RuntimeSettingBoolData paidValue = RuntimeSettingBoolData(paidKey, defaultValue: false); + + final RuntimeSettingIntData ltValue = RuntimeSettingIntData(ltValueKey, defaultValue: 0); + + Future> init() async { + final keywords = AdsManager.instance.adsKeywords; + + final _appVersion = keywords["app_version"]; + if (_appVersion != null) { + appVersionEnabled.set(true); + appVersion.set(_appVersion); + } else { + appVersion.set(GuruSettings.instance.version.get()); + } + + final _paid = keywords["paid"]; + if (_paid != null) { + paidEnabled.set(true); + paidValue.set(_paid == "true"); + } else { + paidValue.set(false); + } + + final _lt = keywords["lt"]; + if (_lt != null) { + ltEnabled.set(true); + ltValue.set(int.parse(_lt)); + } else { + ltValue.set(AdsManager.ltSamples[0]); + } + return keywords; + } +} \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/lib/dialog/editor/editor_controller.dart b/guru_app/packages/guru_assistant/lib/dialog/editor/editor_controller.dart new file mode 100644 index 0000000..9eaa7af --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/dialog/editor/editor_controller.dart @@ -0,0 +1,29 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:guru_utils/controller/lifecycle_controller.dart'; +import 'package:injectable/injectable.dart'; + +import 'editor_design_model.dart'; + +/// Created by Haoyi on 2022/8/20 + +@injectable +class EditorController extends LifecycleController { + final EditorDesignSpec designModel = EditorDesignSpec.get(); + Completer? loading; + + EditorDialogDesignSpec get dialogDesignModel => designModel.dialogSpec; + + final FocusNode focusNode = FocusNode(); + final TextEditingController editorController = TextEditingController(); + + String get text => editorController.text; + + EditorController(); + + @override + void onClose() { + super.onClose(); + } +} diff --git a/guru_app/packages/guru_assistant/lib/dialog/editor/editor_design_model.dart b/guru_app/packages/guru_assistant/lib/dialog/editor/editor_design_model.dart new file mode 100644 index 0000000..7d6bf32 --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/dialog/editor/editor_design_model.dart @@ -0,0 +1,78 @@ +import 'dart:io'; + +import 'package:design/design.dart'; + +part "editor_design_model.g.dart"; + +/// Created by Haoyi on 2022/8/19 + +@DesignSpec(width: 750, height: 1624) +abstract class EditorDesignSpec implements BasicDesignSpec { + @NestedAspectSpec(654 / 500, 0.872, widthUpperLimit: 400) + EditorDialogDesignSpec get dialogSpec; + + static EditorDesignSpec get() => _EditorDesignSpec.get(); +} + +@DesignSpec(width: 654, height: 500, nestedSpec: true) +abstract class EditorDialogDesignSpec implements BasicDesignSpec { + @SpecRadius.circular(SpecHeight(40)) + BorderRadius get radius; + + @SpecHeight(8) + double get bottomMargin; + + @SpecHeight(40) + double get headerTopSpacing; + + @SpecEdgeInsets.symmetric(horizontal: SpecWidth(32)) + EdgeInsetsGeometry get headerPadding; + + @SpecHeight(85) + double get headerHeight; + + @SpecHeight(48) + double get closeIconSize; + + @SpecHeight(48) + double get titleTopSpacing; + + @SpecAbsoluteFontSize(36) + double get titleFontSize; + + @SpecVertical(28) + double get textFieldFontSize; + + @SpecVertical(56) + double get textFieldTopSpacing; + + @SpecHeight(104) + double get textFieldHeight; + + @SpecRadius.circular(SpecHeight(24)) + BorderRadius get textFieldRadius; + + @SpecEdgeInsets.symmetric(horizontal: SpecHorizontal(40)) + EdgeInsets get textFieldHorizontalPadding; + + @SpecVertical(16) + double get tipsTopSpacing; + + @SpecVertical(56) + double get buttonVerticalSpacing; + + @SpecAspectHeightSize(96, 5.64583333) + Size get buttonSize; + + @SpecAspectHeightSize(96, 2.52) + Size get smallButtonSize; + + @SpecFontSize(32) + double get buttonFontSize; + + @SpecOffset(SpecOrigin(0), SpecHeight(2)) + Offset get buttonShadowOffset; + + static EditorDialogDesignSpec create(Size measuredSize, {Offset offset = Offset.zero}) => + _EditorDialogDesignSpec._create(measuredSize); +} diff --git a/guru_app/packages/guru_assistant/lib/dialog/editor/editor_design_model.g.dart b/guru_app/packages/guru_assistant/lib/dialog/editor/editor_design_model.g.dart new file mode 100644 index 0000000..d578b5c --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/dialog/editor/editor_design_model.g.dart @@ -0,0 +1,192 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'editor_design_model.dart'; + +// ************************************************************************** +// DesignSpecGenerator +// ************************************************************************** + +class _EditorDesignSpec extends EditorDesignSpec { + _EditorDesignSpec._( + this.measuredMetrics, + this.specOffset, + this.dialogSpec, + ); + + static final designMetrics = DesignMetrics.create(const Size(750.0, 1624.0)); + + static final Map _cache = {}; + + @override + final EditorDialogDesignSpec dialogSpec; + + @override + final MeasuredMetrics measuredMetrics; + + @override + final Offset specOffset; + + @override + Size get measuredSize => measuredMetrics.size; + static _EditorDesignSpec _create( + Size measuredSize, { + Offset offset = Offset.zero, + }) { + final _measuredMetrics = designMetrics.measure(measuredSize); + return _EditorDesignSpec._( + _measuredMetrics, + offset, + EditorDialogDesignSpec.create( + Size( + (_measuredMetrics.measuredWidth * 0.872).clamp(0.0, 400.0), + (_measuredMetrics.measuredWidth * 0.872).clamp(0.0, 400.0) / + (1.308)), + offset: offset), + ); + } + + static EditorDesignSpec get({Offset offset = Offset.zero}) { + final Size measuredSize = Get.size; + final key = BasicDesignSpec.buildSpecKey(measuredSize, offset); + _EditorDesignSpec? designSpec = _cache[key]; + if (kDebugMode || designSpec == null) { + designSpec = _create(measuredSize, offset: offset); + _cache[key] = designSpec; + } + return designSpec; + } +} + +class _EditorDialogDesignSpec extends EditorDialogDesignSpec { + _EditorDialogDesignSpec._( + this.measuredMetrics, + this.specOffset, + this.radius, + this.bottomMargin, + this.headerTopSpacing, + this.headerPadding, + this.headerHeight, + this.closeIconSize, + this.titleTopSpacing, + this.titleFontSize, + this.textFieldFontSize, + this.textFieldTopSpacing, + this.textFieldHeight, + this.textFieldRadius, + this.textFieldHorizontalPadding, + this.tipsTopSpacing, + this.buttonVerticalSpacing, + this.buttonSize, + this.smallButtonSize, + this.buttonFontSize, + this.buttonShadowOffset, + ); + + static final designMetrics = DesignMetrics.create(const Size(654.0, 500.0)); + + static final Map _cache = {}; + + @override + final BorderRadius radius; + + @override + final double bottomMargin; + + @override + final double headerTopSpacing; + + @override + final EdgeInsetsGeometry headerPadding; + + @override + final double headerHeight; + + @override + final double closeIconSize; + + @override + final double titleTopSpacing; + + @override + final double titleFontSize; + + @override + final double textFieldFontSize; + + @override + final double textFieldTopSpacing; + + @override + final double textFieldHeight; + + @override + final BorderRadius textFieldRadius; + + @override + final EdgeInsets textFieldHorizontalPadding; + + @override + final double tipsTopSpacing; + + @override + final double buttonVerticalSpacing; + + @override + final Size buttonSize; + + @override + final Size smallButtonSize; + + @override + final double buttonFontSize; + + @override + final Offset buttonShadowOffset; + + @override + final MeasuredMetrics measuredMetrics; + + @override + final Offset specOffset; + + @override + Size get measuredSize => measuredMetrics.size; + static _EditorDialogDesignSpec _create( + Size measuredSize, { + Offset offset = Offset.zero, + }) { + final _measuredMetrics = designMetrics.measure(measuredSize); + return _EditorDialogDesignSpec._( + _measuredMetrics, offset, + BorderRadius.circular(_measuredMetrics.measureHeight(40.0)), + _measuredMetrics.measureHeight(8.0), // bottomMargin + _measuredMetrics.measureHeight(40.0), // headerTopSpacing + EdgeInsetsDirectional.only( + start: _measuredMetrics.measureWidth(32.0), + end: _measuredMetrics.measureWidth(32.0), + top: 0.0, + bottom: 0.0), // headerPadding + _measuredMetrics.measureHeight(85.0), // headerHeight + _measuredMetrics.measureHeight(48.0), // closeIconSize + _measuredMetrics.measureHeight(48.0), // titleTopSpacing + _measuredMetrics.measureAbsoluteFontSize(36.0), // titleFontSize + _measuredMetrics.measureVertical(28.0), // textFieldFontSize + _measuredMetrics.measureVertical(56.0), // textFieldTopSpacing + _measuredMetrics.measureHeight(104.0), // textFieldHeight + BorderRadius.circular(_measuredMetrics.measureHeight(24.0)), + EdgeInsets.only( + left: _measuredMetrics.measureHorizontal(40.0), + right: _measuredMetrics.measureHorizontal(40.0), + top: 0.0, + bottom: 0.0), + _measuredMetrics.measureVertical(16.0), // tipsTopSpacing + _measuredMetrics.measureVertical(56.0), // buttonVerticalSpacing + Size(_measuredMetrics.measureHeight(96.0) * 5.64583333, + _measuredMetrics.measureHeight(96.0)), // buttonSize + Size(_measuredMetrics.measureHeight(96.0) * 2.52, + _measuredMetrics.measureHeight(96.0)), // smallButtonSize + _measuredMetrics.measureFontSize(32.0), // buttonFontSize + Offset(0.0, _measuredMetrics.measureHeight(2.0)), // buttonShadowOffset + ); + } +} diff --git a/guru_app/packages/guru_assistant/lib/dialog/editor/editor_dialog.dart b/guru_app/packages/guru_assistant/lib/dialog/editor/editor_dialog.dart new file mode 100644 index 0000000..e1dfc13 --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/dialog/editor/editor_dialog.dart @@ -0,0 +1,183 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:design/design.dart'; +import 'package:get/get.dart'; +import 'package:guru_widgets/guru_widgets.dart'; +import 'package:guru_widgets/button/guru_button.dart'; +import 'package:guru_utils/router/router.dart'; +import 'package:guru_widgets/theme/guru_theme.dart'; + +import 'editor_controller.dart'; +import 'editor_design_model.dart'; + +/// Created by Haoyi on 2022/8/19 +/// +class EditorDialog extends GetWidget { + @override + String? get tag => "EditorDialog"; + + final String title; + final Widget? tips; + final String? hint; + final String positiveText; + final String? negativeText; + final void Function(String text)? onPositive; + final void Function(String text)? onNegative; + + EditorDialogDesignSpec get dialogDesignModel => controller.dialogDesignModel; + + EditorDialog( + {Key? key, + this.title = "Editor", + this.hint = "editor", + this.tips, + this.positiveText = "ok", + this.negativeText, + this.onPositive, + this.onNegative}) + : super(key: key) { + Get.create(() => EditorController(), + tag: tag, permanent: false); + } + + @override + Widget build(BuildContext context) { + final hasNegative = negativeText != null && onNegative != null; + return Center( + child: FlexibleContainer( + constraints: BoxConstraints( + minWidth: dialogDesignModel.measuredSize.width, + maxWidth: dialogDesignModel.measuredSize.width, + minHeight: dialogDesignModel.measuredSize.height), + radius: dialogDesignModel.radius, + color: const Color(0xFF252525), + padding: EdgeInsets.only(bottom: dialogDesignModel.bottomMargin), + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: dialogDesignModel.radius, + color: const Color(0xFF252525), + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFF242424), Color(0xFF1D1D1D)]), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + SizedSpacer(height: dialogDesignModel.headerTopSpacing), + Align( + alignment: AlignmentDirectional.topStart, + child: Padding( + padding: dialogDesignModel.headerPadding, + child: SizedBox( + height: dialogDesignModel.headerHeight, + child: Stack( + children: [ + Align( + alignment: AlignmentDirectional.topStart, + child: TapWidget( + onTap: () async { + RouteCenter.instance.back(); + }, + child: Image.asset( + "assets/images/ic_close.png", + width: dialogDesignModel.closeIconSize, + height: dialogDesignModel.closeIconSize, + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: AutoSizeText( + title, + minFontSize: 10, + maxLines: 1, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: dialogDesignModel.titleFontSize, + fontWeight: GuruTheme.fwBold, + color: Colors.white), + ), + ) + ], + ), + )), + ), + SizedSpacer(height: dialogDesignModel.textFieldTopSpacing), + FlexibleContainer( + height: dialogDesignModel.textFieldHeight, + width: dialogDesignModel.buttonSize.width, + radius: dialogDesignModel.textFieldRadius, + color: Colors.white.withOpacity(0.07), + padding: dialogDesignModel.textFieldHorizontalPadding, + needClip: true, + alignment: AlignmentDirectional.centerStart, + child: TextField( + controller: controller.editorController, + focusNode: controller.focusNode, + showCursor: true, + // autofocus: true, + // maxLengthEnforcement: MaxLengthEnforcement.enforced, + // buildCounter: _buildCounter, + style: TextStyle( + fontSize: dialogDesignModel.textFieldFontSize, + fontWeight: GuruTheme.fwMedium, + color: Colors.white), + maxLines: 1, + onChanged: (_) {}, + decoration: InputDecoration( + border: InputBorder.none, + hintText: hint, + hintStyle: TextStyle( + fontSize: dialogDesignModel.textFieldFontSize, + fontWeight: GuruTheme.fwMedium, + color: const Color(0xFF808080)) + // filled: true, + )), + ), + if (tips != null) ...[ + SizedSpacer(height: dialogDesignModel.tipsTopSpacing), + tips! + ], + SizedSpacer(height: dialogDesignModel.buttonVerticalSpacing), + Row( + children: [ + if (hasNegative) ...[ + Expanded( + flex: 1, + child: Center( + child: GuruButton( + size: dialogDesignModel.smallButtonSize, + style: GuruButtonStyle.negative, + action: negativeText!, + onPressed: () async { + onNegative?.call(controller.text); + }), + ), + ), + ], + Expanded( + flex: 1, + child: Center( + child: GuruButton( + size: hasNegative + ? dialogDesignModel.smallButtonSize + : dialogDesignModel.buttonSize, + action: positiveText, + onPressed: () async { + if (onPositive != null) { + onPositive?.call(controller.text); + } else { + RouteCenter.instance.back(); + } + }), + ), + ), + ], + ), + SizedSpacer(height: dialogDesignModel.buttonVerticalSpacing), + ], + ))), + ); + } +} \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/lib/guru_assistant.dart b/guru_app/packages/guru_assistant/lib/guru_assistant.dart new file mode 100644 index 0000000..b886b0d --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/guru_assistant.dart @@ -0,0 +1,12 @@ +library guru_assistant; + +export 'ui/guru_debug_page.dart'; +export 'ui/ads_debug_page.dart'; +export 'ui/ads_test/ads_test_controller.dart'; +export 'ui/ads_test/ads_test_page.dart'; +export 'ui/assets/assets_debug_page.dart'; +export 'ui/assets/assets_dubug_controller.dart'; +export 'ui/account/account_debug_page.dart'; +export 'ui/account/account_dubug_controller.dart'; +export 'core/property_keys.dart'; +export 'data/setting.dart'; diff --git a/guru_app/packages/guru_assistant/lib/ui/account/account_debug_page.dart b/guru_app/packages/guru_assistant/lib/ui/account/account_debug_page.dart new file mode 100644 index 0000000..feb4b61 --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/ui/account/account_debug_page.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:get/get.dart'; +import 'package:guru_widgets/appbar/guru_app_bar.dart'; +import 'package:guru_widgets/pages/settings/guru_settings_page.dart'; +import 'package:guru_app/financial/igc/igc_manager.dart'; +import 'package:guru_app/financial/iap/iap_manager.dart'; +import 'package:guru_app/controller/assets_aware.dart'; +import 'package:guru_utils/controller/lifecycle_controller.dart'; +import 'package:guru_app/financial/product/product_model.dart'; +import 'package:guru_app/ads/ads_manager.dart'; + +import 'account_dubug_controller.dart'; + + +class AccountDebugPage extends GetWidget { + final LeadingType leadingType; + + AccountDebugPage( + {Key? key, + this.leadingType = LeadingType.close}) + : super(key: key) { + Get.create(() => GetIt.instance()); + } + + + @override + Widget build(BuildContext context) { + return GuruSettingsPage( + items: [ + GroupSetting( + title: "PROFILE", + items: [ + EntranceSetting(title: "Reset Avatar Assets", onTap: () async { + + }) + ] + ), + GroupSetting( + title: "LEADERBOARD", + items: [ + EntranceSetting(title: "Select Fake Type", onTap: () async { + + }), + EntranceSetting(title: "Free Modify Nickname", onTap: () async { + + }), + EntranceSetting(title: "Force Load data from server", onTap: () async { + + }), + EntranceSetting(title: "Disable Upload Score", onTap: () async { + + }), + EntranceSetting(title: "Force Show Version", onTap: () async { + + }), + EntranceSetting(title: "Enalbe Peek User", onTap: () async { + + }), + EntranceSetting(title: "Clear Nickname(Mock!)", onTap: () async { + + }), + EntranceSetting(title: "Clear Statistic And Remote Best Score", onTap: () async { + + }), + EntranceSetting(title: "Load Count", onTap: () async { + + }), + ] + ) + ] + ); + } + +} \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/lib/ui/account/account_dubug_controller.dart b/guru_app/packages/guru_assistant/lib/ui/account/account_dubug_controller.dart new file mode 100644 index 0000000..6b72332 --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/ui/account/account_dubug_controller.dart @@ -0,0 +1,21 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:guru_utils/controller/lifecycle_controller.dart'; +import 'package:injectable/injectable.dart'; +import 'package:guru_app/controller/assets_aware.dart'; +import 'package:guru_app/financial/igc/igc_manager.dart'; +import 'package:guru_app/financial/iap/iap_manager.dart'; +import 'package:guru_app/financial/product/product_model.dart'; + + +@injectable +class AccountDebugController extends LifecycleController { + + AccountDebugController(); + + @override + void onClose() { + super.onClose(); + } +} diff --git a/guru_app/packages/guru_assistant/lib/ui/ads_debug_page.dart b/guru_app/packages/guru_assistant/lib/ui/ads_debug_page.dart new file mode 100644 index 0000000..4a1c1ea --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/ui/ads_debug_page.dart @@ -0,0 +1,391 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/gestures/tap.dart'; +import 'package:guru_widgets/appbar/guru_app_bar.dart'; +import 'package:guru_widgets/pages/settings/guru_settings_page.dart'; +import 'package:guru_popup/guru_popup.dart'; +import 'package:guru_app/ads/ads_manager.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:flutter/services.dart'; +import 'package:guru_applovin_flutter/guru_applovin_flutter.dart'; +import 'package:get/get.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/property/property.dart'; +import 'package:guru_widgets/theme/guru_theme.dart'; +import 'package:guru_utils/core/ext.dart'; +import 'package:guru_utils/router/router.dart'; +import 'package:guru_utils/property/property_model.dart'; + +import '../core/model.dart'; +import '../data/setting.dart'; +import '../core/property_keys.dart'; +import '../dialog/editor/editor_dialog.dart'; + +class AdsDebugPage extends StatefulWidget { + final LeadingType leadingType; + final VoidCallback? initialCallback; + final VoidCallback? onEnterAdsTest; + + const AdsDebugPage( + {Key? key, + this.leadingType = LeadingType.close, + this.initialCallback, + this.onEnterAdsTest}) + : super(key: key); + + @override + State createState() => AdsDebugState(); +} + + +class AdsDebugState extends State{ + final profile = AdsManager.instance.adsProfile; + static const gdprType = [GdprPopupType.admob, GdprPopupType.guru]; + static final List geographyName = ["DISABLED", "EEA", "NOT_EEA", "DEFAULT"]; + + bool forceGdpr = false; + int forceGdprType = 0; + DateTime latestShownFundingChoicesTime = DateTime(0); + int consentDebugGeography = ConsentDebugGeography.names.length - 1; + + void resetGdpr() { + AppProperty.getInstance().removeAllWithTag(UtilsPropertyTags.consent); + AdsManager.instance.resetGdpr(); + setState(() { + consentDebugGeography = ConsentDebugGeography.names.length - 1; + }); + AppProperty.getInstance().remove(UtilsPropertyKeys.forceGdprType); + } + + + void resetPubmatic() { + AppProperty.getInstance().remove(UtilsPropertyKeys.forcePubmatic); + // forcePubmaticSubject.add(AdsManager.instance.adsConfig.bannerConfig.pubmaticEnable); + } + + void setForceGdprType(int index) { + AppProperty.getInstance().setString(UtilsPropertyKeys.forceGdprType, gdprType[index]); + setState(() { + forceGdprType = index; + }); + } + + void setLatestShownFundingChoicesTimestamp(DateTime date) { + AppProperty.getInstance().setInt(UtilsPropertyKeys.latestShownFundingChoicesTime, date.millisecondsSinceEpoch); + setState(() { + latestShownFundingChoicesTime = date; + }); + } + + void _init() async { + final forceGdprBol = await AppProperty.getInstance().getBool(UtilsPropertyKeys.forceGdpr, defValue: false); + final consentDebugGeographyInt = await AppProperty.getInstance().getInt(UtilsPropertyKeys.consentDebugGeography, defValue: ConsentDebugGeography.names.length - 1); + final forceGdprTypeIdx = gdprType.indexOf(GdprPopupType.admob); + final ts = await AppProperty.getInstance().getInt(UtilsPropertyKeys.latestShownFundingChoicesTime, defValue: 0); + + setState(() { + forceGdpr = forceGdprBol; + consentDebugGeography = consentDebugGeographyInt; + forceGdprType = forceGdprTypeIdx == -1 || forceGdprTypeIdx >= gdprType.length ? 0 : forceGdprTypeIdx; + latestShownFundingChoicesTime = DateTime.fromMillisecondsSinceEpoch(ts); + }); + } + + @override + void initState() { + super.initState(); + _init(); + widget.initialCallback?.call(); + } + + SettingItem _buildGdprGroup() { + if (forceGdprType == 0) { + return GroupSetting( + title: "AdMob GDPR", + items: [ + EntranceSetting(title: "Admob Consent Debug Geography", onTap: () async { + await GuruPopup.instance.showCustomDialog(child: RadioGroupWidget(consentDebugGeography, geographyName.map((t) => Text( + t, + style: const TextStyle(fontSize: 16, color: Colors.black), + )) + .toList(), onChanged: (idx) { + setForceGdprType(idx); + })); + }), + EntranceSetting(title: "Admob Consent Test Device Id", onTap: () async { + final testDeviceId = await AppProperty.getInstance().getStringOrNull(UtilsPropertyKeys.admobConsentTestDeviceId); + await GuruPopup.instance.showCustomDialog(child: EditorDialog( + title: "Consent Test Device Id", + hint: testDeviceId, + onPositive: (text) async { + if (DartExt.isNotBlank(text)) { + await AppProperty.getInstance().setString(UtilsPropertyKeys.admobConsentTestDeviceId, text); + GuruPopup.instance.showCommonToast("Set consent test device id success! ($text)"); + } + RouteCenter.instance.back(); + }, + onNegative: (_) async { + await AppProperty.getInstance().remove(UtilsPropertyKeys.admobConsentTestDeviceId); + GuruPopup.instance.showCommonToast("clear consent test device id success!"); + RouteCenter.instance.back(); + }, + tips: RichText( + text: TextSpan( + style: const TextStyle( + fontSize: 9, + fontWeight: GuruTheme.fwRegular, + color: Colors.white, + fontStyle: FontStyle.italic), + children: [ + const TextSpan( + text: "ENTER ", + ), + TextSpan( + text: "`adb logcat -s UserMessagingPlatform`", + style: const TextStyle(color: Colors.orange, decoration: TextDecoration.underline), + recognizer: TapGestureRecognizer() + ..onTap = () { + Clipboard.setData( + const ClipboardData(text: "adb logcat -s UserMessagingPlatform")); + GuruPopup.instance.showCommonToast("command copied"); + }, + ), + const TextSpan( + text: " to fetch DeviceId!", + ), + ])))); + }), + EntranceSetting( + title: "Modify Latest Shown Funding Choices Ts", + summary: latestShownFundingChoicesTime.isAfter(DateTime(2022, 6, 10)) + ? latestShownFundingChoicesTime.toIso8601String() : "Not Shown", + onTap: () async { + final dt = latestShownFundingChoicesTime; + final selectedDate = await showDatePicker( + context: Get.context!, + initialDate: dt?.isAfter(DateTime(2022, 6, 10)) == true + ? dt ?? DateTime.now() + : DateTime.now(), + firstDate: DateTime(2022, 6, 10), + lastDate: DateTime.now().add(const Duration(days: 365))); + + final selectedTime = await showTimePicker( + context: Get.context!, + initialTime: + dt != null ? TimeOfDay(hour: dt.hour, minute: dt.minute) : TimeOfDay.now()); + + final newDateTime = DateTime(selectedDate?.year ?? dt.year, selectedDate?.month ?? dt.month, + selectedDate?.day ?? dt.day, selectedTime?.hour ?? dt.hour, selectedTime?.minute ?? dt.minute); + + setLatestShownFundingChoicesTimestamp(newDateTime); + }) + ] + ); + } else { + return GroupSetting( + title: "GURU GDPR", + items: [ + SwitchSetting( + enabledTitle: "Force Show Guru Gdpr", + settingData: DebugModeSettings.instance.forceGdpr, + onChanged: (value) { + DebugModeSettings.instance.forceGdpr.set(value); + }), + ] + ); + } + } + + @override + Widget build(BuildContext context) { + return GuruSettingsPage( + leadingType: LeadingType.back, + subpage: true, + items: [ + GroupSetting( + title: "MAX SUPPORT", + items: [ + EntranceSetting(title: "MAX Mediation Debugger", onTap: () async { + GuruApplovinFlutter.instance.openDebugger(); + }), + EntranceSetting(title: "GURU Mediation Debugger", onTap: () { + widget.onEnterAdsTest?.call(); + }), + SwitchSetting( + enabledTitle: "Fake Interstitial Ads", + settingData: DebugModeSettings.instance.fakeInterstitialAds, + onChanged: (value) { + DebugModeSettings.instance.fakeInterstitialAds.set(value); + RuntimeProperty.instance.setBool(UtilsPropertyKeys.fakeInterstitialAds, value); + }), + SwitchSetting( + enabledTitle: "Fake Rewarded Ads", + settingData: DebugModeSettings.instance.fakeRewardedAds, + onChanged: (value) { + DebugModeSettings.instance.fakeRewardedAds.set(value); + RuntimeProperty.instance.setBool(UtilsPropertyKeys.fakeRewardedAds, value); + }), + ] + ), + GroupSetting( + title: "GDPR", + items: [ + EntranceSetting(title: "Reset Gdpr", onTap: () async { + resetGdpr(); + }), + EntranceSetting(title: "GDPR TYPE", onTap: () async { + await GuruPopup.instance.showCustomDialog(child: RadioGroupWidget(forceGdprType, gdprType.map((t) => Text( + t, + style: const TextStyle(fontSize: 16, color: Colors.black), + )) + .toList(), onChanged: (i) { + setForceGdprType(i); + })); + }) + ] + ), + _buildGdprGroup(), + GroupSetting( + title: "PUBMATIC", + items: [ + SwitchSetting( + enabledTitle: "Force Pubmatic", + settingData: DebugModeSettings.instance.forcePubmatic, + onChanged: (value) { + DebugModeSettings.instance.forcePubmatic.set(value); + }), + EntranceSetting(title: "Reset Pubmatic", onTap: () async { + resetPubmatic(); + }) + ] + ), + GroupSetting( + title: "DATA", + items: [ + EntranceSetting(title: "Ads Remote Config", onTap: () async { + GuruPopup.instance.showKeyValueDialog(AdsManager.instance.adsConfig.dump().entries.toList()); + }), + EntranceSetting(title: "Ads Properties", onTap: () async { + + }) + ] + ), + GroupSetting( + title: "IDS", + items: [ + EntranceSetting(title: "Banner AdUnitId", summary: profile.bannerId.id, onTap: () async { + Clipboard.setData(ClipboardData(text: profile.bannerId.id)); + Log.d("saveTokenToClipboard:${profile.bannerId.id}"); + GuruPopup.instance.showCommonToast("Copied Banner AdUnitId to your clipboard"); + }), + EntranceSetting(title: "Interstitial AdUnitId", summary: profile.interstitialId.id, onTap: () async { + Clipboard.setData(ClipboardData(text: profile.interstitialId.id)); + Log.d("saveTokenToClipboard:${profile.interstitialId.id}"); + GuruPopup.instance.showCommonToast("Copied Interstitial AdUnitId to your clipboard"); + }), + EntranceSetting(title: "Rewarded AdUnitId", summary: profile.rewardsId.id, onTap: () async { + Clipboard.setData(ClipboardData(text: profile.rewardsId.id)); + Log.d("saveTokenToClipboard:${profile.rewardsId.id}"); + GuruPopup.instance.showCommonToast("Copied Rewarded AdUnitId to your clipboard"); + }) + ] + ) + ] + ); + } +} + + + +class RadioGroupWidget extends StatefulWidget { + final MainAxisAlignment mainAxisAlignment; + final MainAxisSize mainAxisSize; + final CrossAxisAlignment crossAxisAlignment; + + final TextDirection? textDirection; + final Axis direction; + + final int initialIndex; + final List items; + final void Function(int)? onChanged; + + RadioGroupWidget(this.initialIndex, this.items, + {this.mainAxisAlignment = MainAxisAlignment.start, + this.mainAxisSize = MainAxisSize.min, + this.crossAxisAlignment = CrossAxisAlignment.start, + this.direction = Axis.vertical, + this.textDirection, + this.onChanged}); + + @override + State createState() { + return RadioGroupState(); + } +} + +class RadioGroupState extends State { + int groupValue = 0; + + final List _items = []; + + void _refresh() { + _items.clear(); + + for (int index = 0; index < widget.items.length; ++index) { + _items.add(Row( + mainAxisSize: MainAxisSize.min, + children: [ + Radio( + value: index, + groupValue: groupValue, + onChanged: (v) { + setState(() { + groupValue = v ?? 0; + }); + widget.onChanged?.call(v ?? 0); + }, + ), + InkWell( + onTap: () { + setState(() { + groupValue = index; + }); + widget.onChanged?.call(index); + }, + child: widget.items[index], + ) + ], + )); + } + } + + @override + void initState() { + super.initState(); + groupValue = widget.initialIndex; + } + + @override + void didUpdateWidget(covariant RadioGroupWidget oldWidget) { + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + _refresh(); + if (widget.direction == Axis.vertical) { + return Column( + mainAxisAlignment: widget.mainAxisAlignment, + mainAxisSize: widget.mainAxisSize, + crossAxisAlignment: widget.crossAxisAlignment, + children: _items, + ); + } else { + return Row( + mainAxisAlignment: widget.mainAxisAlignment, + mainAxisSize: widget.mainAxisSize, + crossAxisAlignment: widget.crossAxisAlignment, + children: _items, + ); + } + } +} \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/lib/ui/ads_test/ads_test_controller.dart b/guru_app/packages/guru_assistant/lib/ui/ads_test/ads_test_controller.dart new file mode 100644 index 0000000..4282efd --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/ui/ads_test/ads_test_controller.dart @@ -0,0 +1,18 @@ +import 'package:guru_utils/controller/aware/controller_aware.dart'; +import 'package:guru_utils/controller/ads_controller.dart'; + + + + +class AdsTestController extends AdsController with RewardedAware, BannerAware, InterstitialAware { + + @override + void onReady() { + super.onReady(); + } + + @override + void onClose() { + super.onClose(); + } +} \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/lib/ui/ads_test/ads_test_page.dart b/guru_app/packages/guru_assistant/lib/ui/ads_test/ads_test_page.dart new file mode 100644 index 0000000..420fc60 --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/ui/ads_test/ads_test_page.dart @@ -0,0 +1,55 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:guru_widgets/appbar/guru_app_bar.dart'; +import 'package:guru_widgets/pages/settings/guru_settings_page.dart'; + +import 'ads_test_controller.dart'; + + +class AdsTestPage extends GetWidget { + final LeadingType leadingType; + + AdsTestPage( + {Key? key, + this.leadingType = LeadingType.close}) + : super(key: key) { + Get.create(() => AdsTestController()); + } + + + @override + Widget build(BuildContext context) { + return GuruSettingsPage( + items: [ + GroupSetting( + title: "TEST BANNER ADS", + items: [ + EntranceSetting(title: "Pubmatic", onTap: () async { + controller.showBanner(); + }), + ] + ), + GroupSetting( + title: "TEST INTERSTITIAL ADS", + items: [ + EntranceSetting(title: "Pubmatic", onTap: () async { + controller.showInterstitialAd(scene: "test"); + }), + EntranceSetting(title: "Guru INTER2INTER", onTap: () async { + + }) + ] + ), + GroupSetting( + title: "TEST REWARDS ADS", + items: [ + EntranceSetting(title: "Pubmatic", onTap: () async { + controller.showRewardedAd(scene: "test"); + }) + ] + ) + ] + ); + } +} diff --git a/guru_app/packages/guru_assistant/lib/ui/ads_test/keywords_test_controller.dart b/guru_app/packages/guru_assistant/lib/ui/ads_test/keywords_test_controller.dart new file mode 100644 index 0000000..b831b61 --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/ui/ads_test/keywords_test_controller.dart @@ -0,0 +1,34 @@ +import 'dart:async'; +import 'package:guru_assistant/data/setting.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:guru_app/property/settings/guru_settings.dart'; +import 'package:guru_utils/controller/controller.dart'; +import 'package:guru_utils/property/property.dart'; +import 'package:injectable/injectable.dart'; +import 'package:guru_utils/controller/aware/controller_aware.dart'; +import 'package:guru_utils/controller/ads_controller.dart'; +import 'package:guru_app/ads/ads_manager.dart'; + + + +class GuruKeywordsTestController extends LifecycleController { + final BehaviorSubject> originKeywords = + BehaviorSubject.seeded({}); + + Stream> get observableKeywords => AdsManager.instance.observableKeywords; + + Stream> get observableOriginKeywords => originKeywords.stream; + + @override + void onInit() async { + super.onInit(); + final currentKeywords = await KeywordsTestSetting.instance.init(); + originKeywords.add(currentKeywords); + } + + @override + void onClose() { + super.onClose(); + AdsManager.instance.restoreKeywords(originKeywords.value); + } +} \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/lib/ui/ads_test/keywords_test_page.dart b/guru_app/packages/guru_assistant/lib/ui/ads_test/keywords_test_page.dart new file mode 100644 index 0000000..05a30ac --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/ui/ads_test/keywords_test_page.dart @@ -0,0 +1,448 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:guru_assistant/data/setting.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_widgets/pages/settings/guru_settings_page.dart' as guru; +import 'package:guru_widgets/theme/guru_theme.dart'; +import 'package:guru_utils/ads/data/ads_model.dart'; +import 'package:flutter/src/services/clipboard.dart'; +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:guru_app/ads/ads_manager.dart'; +import 'package:guru_app/property/settings/guru_settings.dart'; +import 'package:guru_app/ads/core/ads.dart'; +import 'package:guru_app/ads/applovin/banner/applovin_banner_ads.dart'; +import 'package:guru_app/ads/applovin/interstitial/applovin_interstitial_ads.dart'; +import 'package:guru_app/ads/applovin/rewarded/applovin_rewarded_ads.dart'; +import 'package:guru_widgets/guru_widgets.dart'; +import 'package:guru_utils/ads/handler/ads_handler.dart'; +import 'package:guru_widgets/tile/guru_list_tile.dart'; + +import 'keywords_test_controller.dart'; + +/// Created by Haoyi on 2023/8/18 + +class KeywordsTestPage extends GetWidget { + const KeywordsTestPage({super.key}); + + @override + Widget build(BuildContext context) { + return guru.GuruSettingsPage( + items: [ + guru.WidgetSetting( + widget: const KeywordAdsListTile( + adUnitId: AdUnitId(android: "c6b418becb8abd43", ios: "62d52c7e556bc423"), + adType: AdType.banner, + )), + guru.WidgetSetting( + widget: const KeywordAdsListTile( + adUnitId: AdUnitId(android: "bc16ace919c9dde9", ios: "0708364e378f1a00"), + adType: AdType.interstitial, + )), + guru.WidgetSetting( + widget: const KeywordAdsListTile( + adUnitId: AdUnitId(android: "4ec9e2cdd1b368c3", ios: "1fdac0946dcee04d"), + adType: AdType.rewarded, + )), + guru.GroupSetting(title: "KEYWORDS RAW DATA", items: [ + guru.StreamSetting(controller.observableKeywords, (context, snapshot) { + final keywords = snapshot.data ?? {}; + Log.d("keywords changed!:$keywords"); + final guruTheme = GuruTheme.of(context); + final tileTheme = guruTheme.listTileTheme; + final designSpec = guruTheme.designSpec.pageListTileDesignSpec; + final colorScheme = guruTheme.colorScheme; + return Material( + color: tileTheme.backgroundColor ?? colorScheme.containerColor ?? Colors.white, + child: Ink( + height: designSpec.measuredSize.height * 1.5, + child: InkWell( + onTap: () { + Clipboard.setData(ClipboardData(text: keywords.toString())); + }, + child: Padding( + padding: designSpec.itemPadding, + child: SizedBox.expand( + child: AutoSizeText(keywords.toString(), + minFontSize: 10, + maxLines: 3, + style: TextStyle( + fontSize: designSpec.titleFontSize, + fontWeight: GuruTheme.fwMedium, + fontStyle: FontStyle.italic, + color: tileTheme.primaryContentColor ?? + colorScheme.primaryContentColor ?? + Colors.black)), + ), + )))); + }) + ]), + guru.GroupSetting( + title: "DEFAULT CONFIG", + items: [ + guru.StreamSetting(KeywordsTestSetting.instance.appVersionEnabled.observe(), + (context, snapshot) { + final enabled = snapshot.data ?? false; + return DropdownSwitchListTile( + title: "app_version", + selectableValues: [GuruSettings.instance.version.get()], + selected: KeywordsTestSetting.instance.appVersion, + enabled: enabled, + onSelected: (selected) { + if (enabled && selected != null) { + KeywordsTestSetting.instance.appVersion.set(selected); + AdsManager.instance.setKeyword("app_version", selected, debugForce: true); + } + }, + onChanged: (enabled) { + KeywordsTestSetting.instance.appVersionEnabled.set(enabled); + Log.d("set app_version enabled $enabled"); + if (enabled) { + AdsManager.instance.setKeyword( + "app_version", KeywordsTestSetting.instance.appVersion.get().toString(), + debugForce: true); + } else { + AdsManager.instance.removeKeyword("app_version", debugForce: true); + } + }, + ); + }), + guru.StreamSetting(KeywordsTestSetting.instance.paidEnabled.observe(), + (context, snapshot) { + final enabled = snapshot.data ?? false; + + return DropdownSwitchListTile( + title: "paid", + selectableValues: const [true, false], + selected: KeywordsTestSetting.instance.paidValue, + enabled: enabled, + onSelected: (selected) { + if (enabled && selected != null) { + KeywordsTestSetting.instance.paidValue.set(selected); + AdsManager.instance.setKeyword("paid", selected.toString(), debugForce: true); + } + }, + onChanged: (enabled) { + KeywordsTestSetting.instance.paidEnabled.set(enabled); + if (enabled) { + AdsManager.instance.setKeyword( + "paid", KeywordsTestSetting.instance.paidValue.get().toString(), + debugForce: true); + } else { + AdsManager.instance.removeKeyword("paid", debugForce: true); + } + }, + ); + }), + guru.StreamSetting(KeywordsTestSetting.instance.ltEnabled.observe(), + (context, snapshot) { + final enabled = snapshot.data ?? false; + + return DropdownSwitchListTile( + title: "lt(days)", + selectableValues: AdsManager.ltSamples, + selected: KeywordsTestSetting.instance.ltValue, + enabled: enabled, + onSelected: (selected) { + Log.d("onSelected:$selected"); + if (enabled && selected != null) { + KeywordsTestSetting.instance.ltValue.set(selected); + AdsManager.instance.setKeyword("lt", selected.toString(), debugForce: true); + } + }, + onChanged: (enabled) { + KeywordsTestSetting.instance.ltEnabled.set(enabled); + if (enabled) { + AdsManager.instance.setKeyword( + "lt", KeywordsTestSetting.instance.ltValue.get().toString(), + debugForce: true); + } else { + AdsManager.instance.removeKeyword("lt", debugForce: true); + } + }, + ); + }) + ], + ) + ], + subpage: true, + ); + } +} + +class KeywordAdsListTile extends StatefulWidget { + final AdUnitId adUnitId; + final AdType adType; + + const KeywordAdsListTile({required this.adUnitId, required this.adType, Key? key}) + : super(key: key); + + @override + State createState() => KeywordAdsListTileState(); +} + +class KeywordAdsListTileState extends State { + final BehaviorSubject testingSubject = BehaviorSubject.seeded(false); + final BehaviorSubject waterfallNameSubject = BehaviorSubject.seeded("Unknown"); + + Stream get observableWaterfallName => waterfallNameSubject.stream; + + Stream get observableTesting => testingSubject.stream; + + SingleAds? currentAds; + + Timer? timeoutTimer; + + String getAdsTypeName() { + switch (widget.adType) { + case AdType.banner: + return "Banner"; + case AdType.interstitial: + return "Interstitial"; + case AdType.rewarded: + return "Rewarded"; + default: + return "Unknown"; + } + } + + SingleAds? createAds() { + switch (widget.adType) { + case AdType.banner: + return ApplovinBannerAds.create(widget.adUnitId, null); + case AdType.interstitial: + return ApplovinInterstitialAds.create(widget.adUnitId, null); + case AdType.rewarded: + return ApplovinRewardedAds.create(widget.adUnitId); + default: + return null; + } + } + + void testAds() { + cancelTest(); + currentAds = createAds() + ?..addObserver(AdsLifecycleObserverDelegate( + onAdLoadedCallback: (bundle) { + Log.d("onAdLoadedCallback"); + final waterfallName = bundle.getString("ad_waterfall_name", defValue: "Unknown"); + waterfallNameSubject.addIfChanged(waterfallName); + }, + onAdLoadFailedCallback: (bundle) { + Log.d("onAdLoadFailedCallback"); + final waterfallName = bundle.getString("ad_waterfall_name", defValue: "Unknown"); + waterfallNameSubject.addIfChanged(waterfallName); + }, + )) + ..init() + ..load(); + timeoutTimer = Timer(const Duration(seconds: 30), () { + currentAds?.dispose(); + testingSubject.addIfChanged(false); + currentAds = null; + }); + testingSubject.addIfChanged(true); + } + + void cancelTest() { + currentAds?.dispose(); + timeoutTimer?.cancel(); + currentAds = null; + testingSubject.addIfChanged(false); + } + + @override + Widget build(BuildContext context) { + final guruTheme = GuruTheme.of(context); + final tileTheme = guruTheme.listTileTheme; + final designSpec = guruTheme.designSpec.pageListTileDesignSpec; + final colorScheme = guruTheme.colorScheme; + final items = []; + + items.add(Expanded( + flex: 1, + child: Padding( + padding: designSpec.titlePadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(getAdsTypeName(), + style: TextStyle( + fontSize: designSpec.titleFontSize, + fontWeight: GuruTheme.fwMedium, + color: tileTheme.primaryContentColor ?? + colorScheme.primaryContentColor ?? + Colors.black)), + Text(widget.adUnitId.id, + style: TextStyle( + fontStyle: FontStyle.italic, + fontSize: designSpec.summaryFontSize, + color: tileTheme.secondaryContentColor ?? + colorScheme.secondaryContentColor ?? + Colors.grey)) + ], + )), + )); + + items.add(Expanded( + flex: 1, + child: Padding( + padding: designSpec.titlePadding, + child: StreamBuilder( + stream: observableWaterfallName, + builder: (context, snapshot) { + return AutoSizeText(snapshot.data ?? "Unknown", + minFontSize: 10, + maxLines: 1, + style: TextStyle( + fontSize: designSpec.titleFontSize, + fontWeight: GuruTheme.fwMedium, + color: tileTheme.primaryContentColor ?? + colorScheme.primaryContentColor ?? + Colors.black)); + })), + )); + + items.add(StreamBuilder( + stream: observableTesting, + builder: (context, snapshot) { + final testing = snapshot.data == true; + return GuruButton( + size: const Size(64, 28), + onPressed: () async { + if (testing) { + cancelTest(); + } else { + testAds(); + } + }, + child: Center( + child: AutoSizeText( + testing ? "CANCEL" : "TEST", + minFontSize: 8, + style: + const TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: Colors.white), + )), + ); + })); + return Material( + color: tileTheme.backgroundColor ?? colorScheme.containerColor ?? Colors.white, + child: SizedBox( + height: designSpec.measuredSize.height, + child: Padding( + padding: designSpec.switchItemPadding, + child: Row(mainAxisSize: MainAxisSize.max, children: items), + ))); + } +} + +class DropdownSwitchListTile extends GuruTile { + final String title; + final bool enabled; + final List selectableValues; + final SettingData selected; + final ValueChanged? onChanged; + final ValueChanged? onSelected; + + const DropdownSwitchListTile( + {required this.title, + required this.selectableValues, + required this.selected, + required this.enabled, + this.onChanged, + this.onSelected, + Key? key}) + : super(key: key); + + Widget buildSwitch(GuruThemeData guruTheme) { + final iconScheme = guruTheme.iconScheme; + final designSpec = guruTheme.designSpec.pageListTileDesignSpec; + if (iconScheme.switchOnIcon != null && iconScheme.switchOffIcon != null) { + return Image.asset( + enabled ? iconScheme.switchOnIcon! : iconScheme.switchOffIcon!, + width: designSpec.switchBarSize.width, + height: designSpec.switchBarSize.height, + ); + } else { + return SizedBox( + width: designSpec.switchBarSize.width, + height: designSpec.switchBarSize.height, + child: Switch( + value: enabled, + onChanged: (value) { + onChanged?.call(value); + }, + )); + } + } + + Widget buildDropdown() { + return StreamBuilder( + stream: selected.observe(), + builder: (context, snapshot) { + return DropdownButton( + value: snapshot.data, + dropdownColor: const Color.fromARGB(255, 72, 72, 72), + style: const TextStyle(color: Colors.white), + underline: Container(), + onChanged: (T? value) { + selected.set(value); + onSelected?.call(value); + }, + items: selectableValues.map>((T value) { + return DropdownMenuItem( + value: value, + child: Text(value.toString()), + ); + }).toList(), + ); + }); + } + + @override + Widget build(BuildContext context) { + final guruTheme = GuruTheme.of(context); + final tileTheme = guruTheme.listTileTheme; + final designSpec = guruTheme.designSpec.pageListTileDesignSpec; + final colorScheme = guruTheme.colorScheme; + final items = []; + + items.add(Expanded( + flex: 3, + child: Padding( + padding: designSpec.titlePadding, + child: AutoSizeText(title, + minFontSize: 10, + maxLines: 1, + style: TextStyle( + fontSize: designSpec.titleFontSize, + fontWeight: GuruTheme.fwMedium, + color: tileTheme.primaryContentColor ?? + colorScheme.primaryContentColor ?? + Colors.black))), + )); + items.add(const VerticalDivider( + thickness: 1, + indent: 10, + endIndent: 10, + color: Colors.grey, + )); + items.add(Expanded( + flex: 4, child: Padding(padding: designSpec.titlePadding, child: buildDropdown()))); + items.add(buildSwitch(guruTheme)); + return Material( + color: tileTheme.backgroundColor ?? colorScheme.containerColor ?? Colors.white, + child: Ink( + height: designSpec.measuredSize.height, + child: InkWell( + onTap: () { + onChanged?.call(!enabled); + }, + child: Padding( + padding: designSpec.switchItemPadding, + child: Row(mainAxisSize: MainAxisSize.max, children: items), + )))); + } +} diff --git a/guru_app/packages/guru_assistant/lib/ui/assets/assets_debug_page.dart b/guru_app/packages/guru_assistant/lib/ui/assets/assets_debug_page.dart new file mode 100644 index 0000000..af0d22e --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/ui/assets/assets_debug_page.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:get/get.dart'; +import 'package:guru_widgets/appbar/guru_app_bar.dart'; +import 'package:guru_widgets/pages/settings/guru_settings_page.dart'; +import 'package:guru_app/financial/igc/igc_manager.dart'; +import 'package:guru_app/financial/iap/iap_manager.dart'; +import 'package:guru_app/controller/assets_aware.dart'; +import 'package:guru_utils/controller/lifecycle_controller.dart'; +import 'package:guru_app/financial/product/product_model.dart'; +import 'package:guru_app/ads/ads_manager.dart'; + +import 'assets_dubug_controller.dart'; + +// guru_app finacial/product/model +class ProductIds { + late ProductId noAds; +} + +class AssetsDebugPage extends GetWidget { + final LeadingType leadingType; + final ProductIds? productIds; + + AssetsDebugPage( + {Key? key, + this.leadingType = LeadingType.close, + this.productIds}) + : super(key: key) { + Get.create(() => GetIt.instance()); + } + + @override + Widget build(BuildContext context) { + return GuruSettingsPage( + items: [ + GroupSetting( + title: "Games", + items: [ + EntranceSetting(title: "MAX Mediation Debugger", onTap: () async { + + }), + EntranceSetting(title: "Clear All Gems", onTap: () { + controller.clearGems(); + }) + ] + ), + GroupSetting( + title: "IAP", + items: [ + EntranceSetting(title: "Test Buy No Ads", onTap: () async { + if (productIds != null) { + final products = await IapManager.instance + .buildProducts({productIds!.noAds.createIntent(scene: "debug")}); + final product = products.getProduct(productIds!.noAds); + if (product != null) { + controller.requestProduct(product); + } + } + + }), + EntranceSetting(title: "Test Subscription Premium Month", onTap: () async { + + }), + EntranceSetting(title: "Beginner Pack State", onTap: () async { + + }), + EntranceSetting(title: "Restore IAP", onTap: () async { + controller.restorePurchases(); + }), + EntranceSetting(title: "Clear IAP Asset", onTap: () async { + controller.clearIapAssets(); + AdsManager.instance.noBannerAndInterstitialAdsSubject.add(false); + }), + ] + ), + GroupSetting( + title: "SPECIAL OFFER", + items: [ + EntranceSetting(title: "Refresh Special Offer", onTap: () async { + + }), + EntranceSetting(title: "Modify Special Offer SKU", onTap: () async { + + }), + EntranceSetting(title: "Modify Special Off End Date Time", onTap: () async { + + }), + EntranceSetting(title: "Refresh Special Offer Id", onTap: () async { + controller.restorePurchases(); + }), + EntranceSetting(title: "Refresh Special Offer Latest Shown Id", onTap: () async { + + }), + EntranceSetting(title: "Modify Special Offer New Date", onTap: () async { + + }), + EntranceSetting(title: "Modify Special Offer Status", onTap: () async { + + }), + EntranceSetting(title: "Clear All Special Offer", onTap: () async { + + }), + ] + ), + GroupSetting( + title: "DAILY QUEST", + items: [ + EntranceSetting(title: "Force Enable Daily Quest", onTap: () async { + + }), + EntranceSetting(title: "Quick Complete!!", onTap: () async { + + }), + EntranceSetting(title: "Reset Mystery Jackpot", onTap: () async { + + }), + EntranceSetting(title: "Clear Daily Quest items", onTap: () async { + + }), + EntranceSetting(title: "Reset All Daily Quest Data", onTap: () async { + + }), + ] + ), + GroupSetting( + title: "REWARDS", + items: [ + EntranceSetting(title: "Clear Daily Rewards", onTap: () async { + + }), + EntranceSetting(title: "Reset Today Claimed Status", onTap: () async { + + }), + EntranceSetting(title: "Increase Daily Rewards Cumulative Days", onTap: () async { + + }), + EntranceSetting(title: "Decrease Daily Rewards Cumulative Days", onTap: () async { + + }) + ] + ), + GroupSetting( + title: "PROPS", + items: [ + EntranceSetting(title: "Claim a Hammer Props of AdsReward", onTap: () async { + + }), + EntranceSetting(title: "Claim a Swap Props of AdsReward", onTap: () async { + + }), + EntranceSetting(title: "Claim a Undo Props of AdsReward", onTap: () async { + + }) + ] + ), + GroupSetting( + title: "MISC", + items: [ + EntranceSetting(title: "Clear Redeenmed Promo Code", onTap: () async { + + }) + ] + ), + ] + ); + } + +} \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/lib/ui/assets/assets_dubug_controller.dart b/guru_app/packages/guru_assistant/lib/ui/assets/assets_dubug_controller.dart new file mode 100644 index 0000000..589b223 --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/ui/assets/assets_dubug_controller.dart @@ -0,0 +1,25 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:guru_utils/controller/lifecycle_controller.dart'; +import 'package:injectable/injectable.dart'; +import 'package:guru_app/controller/assets_aware.dart'; +import 'package:guru_app/financial/igc/igc_manager.dart'; +import 'package:guru_app/financial/iap/iap_manager.dart'; +import 'package:guru_app/financial/product/product_model.dart'; + + +@injectable +class AssetsDebugController extends LifecycleController with AssetsAware { + + AssetsDebugController(); + + void clearGems() { + IgcManager.instance.clear(); + } + + @override + void onClose() { + super.onClose(); + } +} diff --git a/guru_app/packages/guru_assistant/lib/ui/data_viewer_page.dart b/guru_app/packages/guru_assistant/lib/ui/data_viewer_page.dart new file mode 100644 index 0000000..34f7507 --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/ui/data_viewer_page.dart @@ -0,0 +1,172 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:intl/src/intl/date_format.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:guru_app/firebase/messaging/remote_messaging_manager.dart'; +import 'package:guru_analytics_flutter/guru/guru_statistic.dart'; +import 'package:guru_app/analytics/guru_analytics.dart'; +import 'package:guru_app/property/property_keys.dart'; +import 'package:guru_app/account/account_data_store.dart'; +import 'package:guru_app/firebase/remoteconfig/remote_config_manager.dart'; +import 'package:guru_widgets/pages/settings/guru_settings_page.dart'; +import 'package:guru_widgets/appbar/guru_app_bar.dart'; +import 'package:guru_popup/guru_popup.dart'; +import 'package:guru_utils/analytics/analytics.dart'; +import 'package:guru_utils/property/runtime_property.dart'; +import 'package:guru_utils/property/app_property.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/device/device_utils.dart'; + +import '../core/property_keys.dart'; +import '../data/setting.dart'; + + +class DataViewerPage extends StatefulWidget { + final List? items; + final List? firebaseItems; + final List? propertiesItems; + final List? commonItems; + final List? accountItems; + + const DataViewerPage( + {Key? key, + this.items, + this.firebaseItems, + this.propertiesItems, + this.commonItems, + this.accountItems}) + : super(key: key); + + @override + State createState() => DataViewerState(); +} + +class DataViewerState extends State{ + DateTime firstInstallDate = DateTime(0); + String? fcmToken = RemoteMessagingManager.instance.fcmToken.value; + GuruStatistic? statistic = GuruAnalytics.instance.guruEventStatistic.value ?? GuruStatistic.invalid; + + void setFirstInstallDate(DateTime? date) { + if (date != null) { + final firstInstallTime = date.millisecondsSinceEpoch; + AppProperty.getInstance().setInt(PropertyKeys.firstInstallTime, firstInstallTime); + RuntimeProperty.instance.setInt(PropertyKeys.firstInstallTime, firstInstallTime); + setState(() { + firstInstallDate = date; + }); + } + } + + void _init() async { + final firstInstallTime = await AppProperty.getInstance() + .getOrCreateInt(PropertyKeys.firstInstallTime, DateTime.now().millisecondsSinceEpoch); + setState(() { + firstInstallDate = DateTime.fromMillisecondsSinceEpoch(firstInstallTime); + }); + } + + @override + void initState() { + super.initState(); + _init(); + } + + @override + Widget build(BuildContext context) { + return GuruSettingsPage( + leadingType: LeadingType.back, + subpage: true, + items: [ + GroupSetting( + title: "FIREBASE", + items: [ + EntranceSetting(title: "Push Token", summary: fcmToken ?? "", onTap: () async { + Clipboard.setData(ClipboardData(text: fcmToken ?? "")); + Log.d("saveTokenToClipboard:$fcmToken"); + GuruPopup.instance.showCommonToast("Copied FCMToken to your clipboard"); + }), + EntranceSetting(title: "Analytics Event Data", summary: "uploaded:${statistic?.uploaded} logged:${statistic?.logged} }", onTap: () { + GuruAnalytics.instance.refreshEventStatistic(force: true); + GuruPopup.instance.showKeyValueDialog(Analytics.latestEventRecords.toList()); + }), + EntranceSetting(title: "Export Guru Analytics Log", onTap: () async { + final path = await GuruAnalytics.instance.zipGuruLogs(); + final file = File(path); + // ShareUtils.shareZipFile(file); + }), + EntranceSetting(title: "Remote Config", onTap: () { + GuruPopup.instance.showKeyValueDialog(RemoteConfigManager.instance.allData().entries.toList()); + }), + EntranceSetting(title: "User properties / Screen", onTap: () { + GuruPopup.instance.showKeyValueDialog(Analytics.userProperties.entries.toList()); + }), + ...?widget?.firebaseItems + ] + ), + GroupSetting( + title: "PROPERTIES", + items: [ + EntranceSetting(title: "Runtime Properties", onTap: () async { + GuruPopup.instance.showKeyValueDialog(RuntimeProperty.instance.toNamedMapEntries()); + }), + EntranceSetting(title: "App Properties", onTap: () async { + GuruPopup.instance.showKeyValueDialog( + (await AppProperty.getInstance().loadAllValues()).toNamedMapEntries()); + }), + ...?widget?.propertiesItems + ] + ), + GroupSetting( + title: "COMMON", + items: [ + SwitchSetting( + enabledTitle: "Global Floating Window", + settingData: DebugModeSettings.instance.globalFloatingWindow, + onChanged: (value) { + DebugModeSettings.instance.fakeRewardedAds.set(value); + RuntimeProperty.instance.setBool(DataViewerPropertyKeys.globalFloatingWindow, value); + }), + EntranceSetting( + title: "Modify First Install Time", + summary: firstInstallDate.isAfter(DateTime(2022, 6, 10)) + ? DateFormat.yMd().format(firstInstallDate) + : "Not Shown", + onTap: () async { + final selectedDate = await showDatePicker( + context: context, + initialDate: firstInstallDate.isAfter(DateTime(2022, 6, 10)) == true + ? firstInstallDate ?? DateTime.now() + : DateTime.now(), + firstDate: DateTime(2022, 6, 10), + lastDate: DateTime.now().add(const Duration(days: 365))); + setFirstInstallDate(selectedDate); + } + ), + ...?widget?.commonItems + ] + ), + GroupSetting( + title: "ACCOUNT", + items: [ + EntranceSetting(title: "Account Data Store", onTap: () async { + final accountDataStore = AccountDataStore.instance; + GuruPopup.instance.showKeyValueDialog({ + "nickname": accountDataStore.nickname ?? "", + "uid": accountDataStore.uid ?? "", + // "bestScore": accountDataStore.bestScore.displayNum ?? "", + "countryCode": + "${DeviceUtils.countryCodeToFlagEmoji(accountDataStore.countryCode ?? "CN")}(${accountDataStore.countryCode})", + "avatar": accountDataStore.avatar ?? "", + "saasToken": accountDataStore.saasToken ?? "", + "accountDataStatus": accountDataStore.accountDataStatus.toString(), + "initRetryCount": accountDataStore.initRetryCount.toString() + }.entries.toList()); + }), + ...?widget?.accountItems + ] + ), + ...?widget?.items + ] + ); + } +} \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/lib/ui/guru_debug_page.dart b/guru_app/packages/guru_assistant/lib/ui/guru_debug_page.dart new file mode 100644 index 0000000..c5f7736 --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/ui/guru_debug_page.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:guru_widgets/appbar/guru_app_bar.dart'; +import 'package:guru_widgets/pages/settings/guru_settings_page.dart'; + +/// Created by Haoyi on 2023/6/28 + +class GuruDebugPage extends StatelessWidget { + final LeadingType leadingType; + final List? items; + final VoidCallback? onEnterGameDebug; + final VoidCallback? onEnterDataViewer; + final VoidCallback? onEnterAdsDebug; + final VoidCallback? onEnterAssetsDebug; + final VoidCallback? onEnterDialogDebug; + final VoidCallback? onEnterAccountDebug; + final VoidCallback? onEnterTestSuit; + + const GuruDebugPage( + {Key? key, + this.leadingType = LeadingType.close, + this.items, + this.onEnterGameDebug, + this.onEnterDataViewer, + this.onEnterAdsDebug, + this.onEnterAssetsDebug, + this.onEnterDialogDebug, + this.onEnterAccountDebug, + this.onEnterTestSuit}) + : super(key: key); + + + @override + Widget build(BuildContext context) { + return GuruSettingsPage( + leadingType: leadingType, + subpage: true, + items: [ + EntranceSetting( + title: "Game Debug", + // leading: Assets.imagesIcAds, + // trailing: Assets.imagesIcArrowRight, + onTap: onEnterGameDebug + ), + EntranceSetting( + title: "Data Viewer", + // leading: Assets.imagesIcAds, + // trailing: Assets.imagesIcArrowRight, + onTap: onEnterDataViewer + ), + EntranceSetting( + title: "Ads Debug", + // leading: Assets.imagesIcAds, + // trailing: Assets.imagesIcArrowRight, + onTap: onEnterAdsDebug + ), + EntranceSetting( + title: "Assets Debug", + // leading: Assets.imagesIcAds, + // trailing: Assets.imagesIcArrowRight, + onTap: onEnterAssetsDebug + ), + EntranceSetting( + title: "Dialog Debug", + // leading: Assets.imagesIcAds, + // trailing: Assets.imagesIcArrowRight, + onTap: onEnterDialogDebug + ), + EntranceSetting( + title: "Account Debug", + // leading: Assets.imagesIcAds, + // trailing: Assets.imagesIcArrowRight, + onTap: onEnterAccountDebug + ), + EntranceSetting( + title: "Test Suit", + // leading: Assets.imagesIcAds, + // trailing: Assets.imagesIcArrowRight, + onTap: onEnterTestSuit + ), + ...?items, + ] + ); + } +} \ No newline at end of file diff --git a/guru_app/packages/guru_assistant/lib/ui/visual/visual_debug_controller.dart b/guru_app/packages/guru_assistant/lib/ui/visual/visual_debug_controller.dart new file mode 100644 index 0000000..ff913f6 --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/ui/visual/visual_debug_controller.dart @@ -0,0 +1,34 @@ +import 'package:guru_assistant/ui/visual/visual_debug_dialog.dart'; +import 'package:guru_utils/controller/lifecycle_controller.dart'; +import 'package:guru_popup/guru_popup.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:guru_utils/extensions/extensions.dart'; + +enum VisualType { none, lottie } + +class VisualInfo { + final VisualType type; + final String uri; + final Duration duration; + final int? repeat; + + static const VisualInfo invalid = + VisualInfo(type: VisualType.none, uri: "", duration: Duration.zero, repeat: 0); + + const VisualInfo( + {required this.type, required this.uri, required this.duration, this.repeat = 0}); +} + +class VisualDebugController extends LifecycleController { + final BehaviorSubject visualInfoSubject = BehaviorSubject.seeded(VisualInfo.invalid); + + Stream get observableVisualInfo => visualInfoSubject.stream; + + Future loadVisual() async { + final visualInfo = await GuruPopup.instance + .showCustomDialog(constraintBuilder: null, child: VisualDebugDialog()); + if (visualInfo != null && visualInfo is VisualInfo) { + visualInfoSubject.addEx(visualInfo); + } + } +} diff --git a/guru_app/packages/guru_assistant/lib/ui/visual/visual_debug_design_model.dart b/guru_app/packages/guru_assistant/lib/ui/visual/visual_debug_design_model.dart new file mode 100644 index 0000000..e79ef28 --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/ui/visual/visual_debug_design_model.dart @@ -0,0 +1,78 @@ +import 'dart:io'; + +import 'package:design/design.dart'; + +part "visual_debug_design_model.g.dart"; + +/// Created by Haoyi on 2022/8/19 + +@DesignSpec(width: 750, height: 1624) +abstract class VisualDebugDesignSpec implements BasicDesignSpec { + @NestedAspectSpec(654 / 500, 0.872, widthUpperLimit: 400) + VisualDebugDialogDesignSpec get dialogSpec; + + static VisualDebugDesignSpec get() => _VisualDebugDesignSpec.get(); +} + +@DesignSpec(width: 654, height: 500, nestedSpec: true) +abstract class VisualDebugDialogDesignSpec implements BasicDesignSpec { + @SpecRadius.circular(SpecHeight(40)) + BorderRadius get radius; + + @SpecHeight(8) + double get bottomMargin; + + @SpecHeight(40) + double get headerTopSpacing; + + @SpecEdgeInsets.symmetric(horizontal: SpecWidth(32)) + EdgeInsetsGeometry get headerPadding; + + @SpecHeight(85) + double get headerHeight; + + @SpecHeight(48) + double get closeIconSize; + + @SpecHeight(48) + double get titleTopSpacing; + + @SpecAbsoluteFontSize(36) + double get titleFontSize; + + @SpecVertical(28) + double get textFieldFontSize; + + @SpecVertical(56) + double get textFieldTopSpacing; + + @SpecHeight(104) + double get textFieldHeight; + + @SpecRadius.circular(SpecHeight(24)) + BorderRadius get textFieldRadius; + + @SpecEdgeInsets.symmetric(horizontal: SpecHorizontal(40)) + EdgeInsets get textFieldHorizontalPadding; + + @SpecVertical(16) + double get tipsTopSpacing; + + @SpecVertical(56) + double get buttonVerticalSpacing; + + @SpecAspectHeightSize(96, 5.64583333) + Size get buttonSize; + + @SpecAspectHeightSize(96, 2.52) + Size get smallButtonSize; + + @SpecFontSize(32) + double get buttonFontSize; + + @SpecOffset(SpecOrigin(0), SpecHeight(2)) + Offset get buttonShadowOffset; + + static VisualDebugDialogDesignSpec create(Size measuredSize, {Offset offset = Offset.zero}) => + _VisualDebugDialogDesignSpec._create(measuredSize); +} diff --git a/guru_app/packages/guru_assistant/lib/ui/visual/visual_debug_design_model.g.dart b/guru_app/packages/guru_assistant/lib/ui/visual/visual_debug_design_model.g.dart new file mode 100644 index 0000000..0a3908b --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/ui/visual/visual_debug_design_model.g.dart @@ -0,0 +1,192 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'visual_debug_design_model.dart'; + +// ************************************************************************** +// DesignSpecGenerator +// ************************************************************************** + +class _VisualDebugDesignSpec extends VisualDebugDesignSpec { + _VisualDebugDesignSpec._( + this.measuredMetrics, + this.specOffset, + this.dialogSpec, + ); + + static final designMetrics = DesignMetrics.create(const Size(750.0, 1624.0)); + + static final Map _cache = {}; + + @override + final VisualDebugDialogDesignSpec dialogSpec; + + @override + final MeasuredMetrics measuredMetrics; + + @override + final Offset specOffset; + + @override + Size get measuredSize => measuredMetrics.size; + static _VisualDebugDesignSpec _create( + Size measuredSize, { + Offset offset = Offset.zero, + }) { + final _measuredMetrics = designMetrics.measure(measuredSize); + return _VisualDebugDesignSpec._( + _measuredMetrics, + offset, + VisualDebugDialogDesignSpec.create( + Size( + (_measuredMetrics.measuredWidth * 0.872).clamp(0.0, 400.0), + (_measuredMetrics.measuredWidth * 0.872).clamp(0.0, 400.0) / + (1.308)), + offset: offset), + ); + } + + static VisualDebugDesignSpec get({Offset offset = Offset.zero}) { + final Size measuredSize = Get.size; + final key = BasicDesignSpec.buildSpecKey(measuredSize, offset); + _VisualDebugDesignSpec? designSpec = _cache[key]; + if (kDebugMode || designSpec == null) { + designSpec = _create(measuredSize, offset: offset); + _cache[key] = designSpec; + } + return designSpec; + } +} + +class _VisualDebugDialogDesignSpec extends VisualDebugDialogDesignSpec { + _VisualDebugDialogDesignSpec._( + this.measuredMetrics, + this.specOffset, + this.radius, + this.bottomMargin, + this.headerTopSpacing, + this.headerPadding, + this.headerHeight, + this.closeIconSize, + this.titleTopSpacing, + this.titleFontSize, + this.textFieldFontSize, + this.textFieldTopSpacing, + this.textFieldHeight, + this.textFieldRadius, + this.textFieldHorizontalPadding, + this.tipsTopSpacing, + this.buttonVerticalSpacing, + this.buttonSize, + this.smallButtonSize, + this.buttonFontSize, + this.buttonShadowOffset, + ); + + static final designMetrics = DesignMetrics.create(const Size(654.0, 500.0)); + + static final Map _cache = {}; + + @override + final BorderRadius radius; + + @override + final double bottomMargin; + + @override + final double headerTopSpacing; + + @override + final EdgeInsetsGeometry headerPadding; + + @override + final double headerHeight; + + @override + final double closeIconSize; + + @override + final double titleTopSpacing; + + @override + final double titleFontSize; + + @override + final double textFieldFontSize; + + @override + final double textFieldTopSpacing; + + @override + final double textFieldHeight; + + @override + final BorderRadius textFieldRadius; + + @override + final EdgeInsets textFieldHorizontalPadding; + + @override + final double tipsTopSpacing; + + @override + final double buttonVerticalSpacing; + + @override + final Size buttonSize; + + @override + final Size smallButtonSize; + + @override + final double buttonFontSize; + + @override + final Offset buttonShadowOffset; + + @override + final MeasuredMetrics measuredMetrics; + + @override + final Offset specOffset; + + @override + Size get measuredSize => measuredMetrics.size; + static _VisualDebugDialogDesignSpec _create( + Size measuredSize, { + Offset offset = Offset.zero, + }) { + final _measuredMetrics = designMetrics.measure(measuredSize); + return _VisualDebugDialogDesignSpec._( + _measuredMetrics, offset, + BorderRadius.circular(_measuredMetrics.measureHeight(40.0)), + _measuredMetrics.measureHeight(8.0), // bottomMargin + _measuredMetrics.measureHeight(40.0), // headerTopSpacing + EdgeInsetsDirectional.only( + start: _measuredMetrics.measureWidth(32.0), + end: _measuredMetrics.measureWidth(32.0), + top: 0.0, + bottom: 0.0), // headerPadding + _measuredMetrics.measureHeight(85.0), // headerHeight + _measuredMetrics.measureHeight(48.0), // closeIconSize + _measuredMetrics.measureHeight(48.0), // titleTopSpacing + _measuredMetrics.measureAbsoluteFontSize(36.0), // titleFontSize + _measuredMetrics.measureVertical(28.0), // textFieldFontSize + _measuredMetrics.measureVertical(56.0), // textFieldTopSpacing + _measuredMetrics.measureHeight(104.0), // textFieldHeight + BorderRadius.circular(_measuredMetrics.measureHeight(24.0)), + EdgeInsets.only( + left: _measuredMetrics.measureHorizontal(40.0), + right: _measuredMetrics.measureHorizontal(40.0), + top: 0.0, + bottom: 0.0), + _measuredMetrics.measureVertical(16.0), // tipsTopSpacing + _measuredMetrics.measureVertical(56.0), // buttonVerticalSpacing + Size(_measuredMetrics.measureHeight(96.0) * 5.64583333, + _measuredMetrics.measureHeight(96.0)), // buttonSize + Size(_measuredMetrics.measureHeight(96.0) * 2.52, + _measuredMetrics.measureHeight(96.0)), // smallButtonSize + _measuredMetrics.measureFontSize(32.0), // buttonFontSize + Offset(0.0, _measuredMetrics.measureHeight(2.0)), // buttonShadowOffset + ); + } +} diff --git a/guru_app/packages/guru_assistant/lib/ui/visual/visual_debug_dialog.dart b/guru_app/packages/guru_assistant/lib/ui/visual/visual_debug_dialog.dart new file mode 100644 index 0000000..9aa982d --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/ui/visual/visual_debug_dialog.dart @@ -0,0 +1,251 @@ +import 'dart:io'; + +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:design/design.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:guru_assistant/dialog/editor/editor_design_model.dart'; +import 'package:guru_assistant/ui/visual/visual_debug_controller.dart'; +import 'package:guru_widgets/guru_widgets.dart'; +import 'package:guru_widgets/button/guru_button.dart'; +import 'package:guru_utils/router/router.dart'; +import 'package:guru_widgets/theme/guru_theme.dart'; +import 'package:guru_utils/log/log.dart'; + +import 'visual_debug_dialog_controller.dart'; +import 'visual_debug_design_model.dart'; + +/// Created by Haoyi on 2022/8/19 +/// +/// +class TimeUnitTextInputFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { + Log.d("oldValue:${oldValue.text} newValue:${newValue.text}"); + + String newText = newValue.text; + int offset = newText.indexOf(" ms"); + if (offset == -1) { + offset = newText.length; + newText = "$newText ms"; + } else if (offset > 8) { + newText = oldValue.text; + offset = oldValue.selection.baseOffset; + } + return TextEditingValue(text: newText, selection: TextSelection.collapsed(offset: offset)); + } +} + +class VisualDebugDialog extends GetWidget { + @override + String? get tag => "VisualDebugDialog"; + + final String title; + final Widget? tips; + final String? hint; + final String positiveText; + final Future Function()? didPickFile; + + // final String? negativeText; + // final void Function(String text)? onPositive; + // final void Function(String text)? onNegative; + + VisualDebugDialogDesignSpec get dialogDesignModel => controller.dialogDesignModel; + + VisualDebugDialog( + {Key? key, + this.title = "Visual Debug", + this.hint = "Duration(ms)", + this.tips, + this.positiveText = "ok", + this.didPickFile}) + : super(key: key) { + Get.create(() => VisualDebugDialogController(), + tag: tag, permanent: false); + } + + @override + Widget build(BuildContext context) { + return Center( + child: FlexibleContainer( + constraints: BoxConstraints( + minWidth: dialogDesignModel.measuredSize.width, + maxWidth: dialogDesignModel.measuredSize.width, + minHeight: dialogDesignModel.measuredSize.height), + radius: dialogDesignModel.radius, + color: const Color(0xFF252525), + padding: EdgeInsets.only(bottom: dialogDesignModel.bottomMargin), + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: dialogDesignModel.radius, + color: const Color(0xFF252525), + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFF242424), Color(0xFF1D1D1D)]), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + SizedSpacer(height: dialogDesignModel.headerTopSpacing), + Align( + alignment: AlignmentDirectional.topStart, + child: Padding( + padding: dialogDesignModel.headerPadding, + child: SizedBox( + height: dialogDesignModel.headerHeight, + child: Stack( + children: [ + Align( + alignment: AlignmentDirectional.topStart, + child: TapWidget( + onTap: () async { + RouteCenter.instance.back(); + }, + child: Image.asset( + "assets/images/ic_close.png", + width: dialogDesignModel.closeIconSize, + height: dialogDesignModel.closeIconSize, + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: AutoSizeText( + title, + minFontSize: 10, + maxLines: 1, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: dialogDesignModel.titleFontSize, + fontWeight: GuruTheme.fwBold, + color: Colors.white), + ), + ) + ], + ), + )), + ), + SizedSpacer(height: dialogDesignModel.textFieldTopSpacing), + FlexibleContainer( + height: dialogDesignModel.textFieldHeight, + width: dialogDesignModel.buttonSize.width, + radius: dialogDesignModel.textFieldRadius, + color: Colors.white.withOpacity(0.07), + padding: dialogDesignModel.textFieldHorizontalPadding, + needClip: true, + alignment: AlignmentDirectional.centerStart, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + flex: 1, + child: StreamBuilder( + stream: controller.observablePickedUri, + builder: (context, snapshot) { + final uri = snapshot.data; + return AutoSizeText( + uri?.isNotEmpty == true ? uri! : "Lottie File Path", + minFontSize: 4, + style: TextStyle( + fontSize: dialogDesignModel.textFieldFontSize, + fontWeight: GuruTheme.fwMedium, + color: Colors.white), + ); + })), + IconButton( + icon: const Icon(Icons.animation_outlined), + iconSize: 24, + color: Colors.white, + onPressed: () async { + final file = await didPickFile?.call(); + controller.onPickedFile(file); + }), + ], + ), + ), + SizedSpacer(height: dialogDesignModel.textFieldTopSpacing), + FlexibleContainer( + height: dialogDesignModel.textFieldHeight, + width: dialogDesignModel.buttonSize.width, + radius: dialogDesignModel.textFieldRadius, + color: Colors.white.withOpacity(0.07), + padding: dialogDesignModel.textFieldHorizontalPadding, + needClip: true, + alignment: AlignmentDirectional.centerStart, + child: TextField( + controller: controller.editorController, + focusNode: controller.focusNode, + showCursor: true, + keyboardType: TextInputType.number, + // autofocus: true, + // maxLengthEnforcement: MaxLengthEnforcement.enforced, + // buildCounter: _buildCounter, + inputFormatters: [TimeUnitTextInputFormatter()], + style: TextStyle( + fontSize: dialogDesignModel.textFieldFontSize, + fontWeight: GuruTheme.fwMedium, + color: Colors.white), + maxLines: 1, + onChanged: (_) {}, + decoration: InputDecoration( + border: InputBorder.none, + hintText: hint, + hintStyle: TextStyle( + fontSize: dialogDesignModel.textFieldFontSize, + fontWeight: GuruTheme.fwMedium, + color: const Color(0xFF808080)) + // filled: true, + )), + ), + if (tips != null) ...[ + SizedSpacer(height: dialogDesignModel.tipsTopSpacing), + tips! + ], + SizedSpacer(height: dialogDesignModel.buttonVerticalSpacing), + Row( + children: [ + // if (hasNegative) ...[ + // Expanded( + // flex: 1, + // child: Center( + // child: GuruButton( + // size: dialogDesignModel.smallButtonSize, + // style: GuruButtonStyle.negative, + // action: negativeText!, + // onPressed: () async { + // onNegative?.call(controller.text); + // }), + // ), + // ), + // ], + Expanded( + flex: 1, + child: Center( + child: GuruButton( + size: dialogDesignModel.buttonSize, + action: positiveText, + onPressed: () async { + RouteCenter.instance.back( + result: VisualInfo( + type: VisualType.lottie, + uri: controller.currentPickedUri, + duration: Duration( + milliseconds: int.tryParse(controller.text) ?? 1000))); + // if (onPositive != null) { + // onPositive?.call(controller.text); + // } else { + // RouteCenter.instance.back(); + // } + }), + ), + ), + ], + ), + SizedSpacer(height: dialogDesignModel.buttonVerticalSpacing), + ], + ))), + ); + } +} diff --git a/guru_app/packages/guru_assistant/lib/ui/visual/visual_debug_dialog_controller.dart b/guru_app/packages/guru_assistant/lib/ui/visual/visual_debug_dialog_controller.dart new file mode 100644 index 0000000..fc7f982 --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/ui/visual/visual_debug_dialog_controller.dart @@ -0,0 +1,56 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:guru_assistant/dialog/editor/editor_design_model.dart'; +import 'package:guru_assistant/ui/visual/visual_debug_design_model.dart'; +import 'package:guru_utils/controller/lifecycle_controller.dart'; +import 'package:injectable/injectable.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:guru_utils/extensions/extensions.dart'; + +/// Created by Haoyi on 2022/8/20 + +@injectable +class VisualDebugDialogController extends LifecycleController { + final VisualDebugDesignSpec designModel = VisualDebugDesignSpec.get(); + + VisualDebugDialogDesignSpec get dialogDesignModel => designModel.dialogSpec; + + final FocusNode focusNode = FocusNode(); + final TextEditingController editorController = TextEditingController(); + + final BehaviorSubject pickedUriSubject = BehaviorSubject.seeded(""); + + Stream get observablePickedUri => pickedUriSubject.stream; + + String get currentPickedUri => pickedUriSubject.value; + + String get text => editorController.text; + + VisualDebugDialogController(); + + Future onPickedFile(File? file) async { + if (file != null && file.existsSync()) { + pickedUriSubject.addEx(file.path); + } + // final result = await FilePicker.platform.pickFiles(); + // if (result != null) { + // final files = result.files; + // if (files.isNotEmpty) { + // final filePath = files.first.path; + // if (filePath!= null) { + // final pickedFile = File(filePath); + // if (pickedFile.existsSync()) { + // pickedUriSubject.addEx(pickedFile.path); + // } + // } + // } + // } + } + + @override + void onClose() { + super.onClose(); + } +} diff --git a/guru_app/packages/guru_assistant/lib/ui/visual/visual_debug_page.dart b/guru_app/packages/guru_assistant/lib/ui/visual/visual_debug_page.dart new file mode 100644 index 0000000..9ad20c2 --- /dev/null +++ b/guru_app/packages/guru_assistant/lib/ui/visual/visual_debug_page.dart @@ -0,0 +1,93 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:guru_assistant/ui/visual/visual_debug_controller.dart'; +import 'package:get/get.dart'; +import 'package:guru_assistant/ui/visual/visual_debug_dialog.dart'; +import 'package:guru_widgets/appbar/guru_app_bar.dart'; +import 'package:guru_widgets/button/guru_button.dart'; +import 'package:guru_widgets/theme/guru_theme.dart'; +import 'package:guru_popup/guru_popup.dart'; +import 'package:guru_widgets/animation/lottie/lottie_widget.dart'; +import 'package:guru_widgets/guru_widgets.dart'; + +class VisualDebugBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => VisualDebugController()); + } +} + +class VisualItemWidget extends StatefulWidget { + final VisualInfo info; + + const VisualItemWidget({super.key, required this.info}); + + @override + State createState() => _VisualItemState(); +} + +class _VisualItemState extends State with SingleTickerProviderStateMixin { + @override + Widget build(BuildContext context) { + return Center( + child: LottieWidget( + uri: widget.info.uri, duration: widget.info.duration, action: LottieAction.repeat), + ); + } +} + +class VisualDebugPage extends GetWidget { + const VisualDebugPage({super.key}); + + Widget buildRootWidget({ + required BuildContext context, + Color? backgroundColor, + String? title, + Color? appbarColor, + required Widget body, + }) { + final guruTheme = GuruTheme.of(context); + final colorScheme = guruTheme.colorScheme; + return MediaQuery.removePadding( + context: context, + removeTop: true, + child: Scaffold( + backgroundColor: backgroundColor ?? colorScheme.backgroundColor, + appBar: _buildAppBar(context), + body: body, + ), + ); + } + + PreferredSizeWidget _buildAppBar(BuildContext context) { + return GuruAppBar(backgroundColor: Colors.transparent, actions: [ + GuruButton( + size: const Size(128, 48), + action: "Open Lottie", + style: GuruButtonStyle.positive, + onPressed: () async { + await controller.loadVisual(); + }, + ) + ]); + } + + @override + Widget build(BuildContext context) { + final guruTheme = GuruTheme.of(context); + return buildRootWidget( + context: context, + backgroundColor: guruTheme.colorScheme.backgroundColor, + body: StreamBuilder( + stream: controller.observableVisualInfo, + builder: (context, snapshot) { + final visualInfo = snapshot.data; + if (visualInfo == null || visualInfo == VisualInfo.invalid) { + return const EmptySpacer(); + } + return VisualItemWidget(info: visualInfo); + })); + } +} diff --git a/guru_app/packages/guru_assistant/pubspec.lock b/guru_app/packages/guru_assistant/pubspec.lock new file mode 100644 index 0000000..dabe946 --- /dev/null +++ b/guru_app/packages/guru_assistant/pubspec.lock @@ -0,0 +1,1607 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + url: "https://pub.flutter-io.cn" + source: hosted + version: "61.0.0" + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: f5628cd9c92ed11083f425fd1f8f1bc60ecdda458c81d73b143aeda036c35fe7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.16" + adjust_sdk: + dependency: transitive + description: + name: adjust_sdk + sha256: "0e1e54f3d8aa180ed908a488d534fa2f43b3a6a8141da7ad4a459c0404fbab2c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.36.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.13.0" + android_id: + dependency: transitive + description: + name: android_id + sha256: "5c2d3a259afcd173dbe367ba452817bd530c4df75d251d652c69b8d3c8ac0d36" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.6" + archive: + dependency: transitive + description: + name: archive + sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.4.9" + args: + dependency: transitive + description: + name: args + sha256: c372bb384f273f0c2a8aaaa226dad84dc27c8519a691b888725dec59518ad53a + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.11.0" + auto_size_text: + dependency: transitive + description: + name: auto_size_text + sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.1" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "7c35a3a7868626257d8aee47b51c26b9dba11eaddf3431117ed2744951416aab" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "67d591d602906ef9201caf93452495ad1812bea2074f04e25dbd7c133785821b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.7" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.2.7" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.6.1" + cached_network_image: + dependency: transitive + description: + name: cached_network_image + sha256: f98972704692ba679db144261172a8e20feb145636c617af0eb4022132a6797f + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.3.0" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "56aa42a7a01e3c9db8456d9f3f999931f1e05535b5a424271e9a38cabf066613" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "759b9a9f8f6ccbb66c185df805fac107f05730b1dab9c64626d1008cca532257" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.2" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + cloud_firestore: + dependency: transitive + description: + name: cloud_firestore + sha256: cb978c7512624144f24f3d06e4312b2f4ac00b016f2fed62dc8f6d56b8585d78 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.13.6" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + sha256: "73ff438fe46028f0e19f55da18b6ddc6906ab750562cd7d9ffab77ff8c0c4307" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.0" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + sha256: "232e45e95970d3a6baab8f50f9c3a6e2838d145d9d91ec9a7392837c44296397" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.9.0" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.4.0" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.18.0" + connectivity_plus: + dependency: transitive + description: + name: connectivity_plus + sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.2" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.4" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.3" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + dartx: + dependency: transitive + description: + name: dartx + sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + dbus: + dependency: transitive + description: + name: dbus + sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.8" + design: + dependency: "direct dev" + description: + path: "packages/design" + ref: "v3.0.0" + resolved-ref: "027f3aa24d64a684af0f710e56bd547ccca14fc2" + url: "git@github.com:castbox/guru_ui.git" + source: git + version: "3.0.0" + design_generator: + dependency: "direct dev" + description: + path: "packages/design_generator" + ref: "v3.0.0" + resolved-ref: "027f3aa24d64a684af0f710e56bd547ccca14fc2" + url: "git@github.com:castbox/guru_ui.git" + source: git + version: "3.0.0" + design_spec: + dependency: "direct dev" + description: + path: "packages/design_spec" + ref: "v3.0.0" + resolved-ref: "027f3aa24d64a684af0f710e56bd547ccca14fc2" + url: "git@github.com:castbox/guru_ui.git" + source: git + version: "3.0.0" + device_apps: + dependency: transitive + description: + name: device_apps + sha256: e84dc74d55749993fd671148cc0bd53096e1be0c268fc364285511b1d8a4c19b + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + device_info_plus: + dependency: transitive + description: + name: device_info_plus + sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "9.1.1" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.0" + dio: + dependency: transitive + description: + name: dio + sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.4.0" + draggable_float_widget: + dependency: "direct main" + description: + name: draggable_float_widget + sha256: "075675c56f6b2bfc9f972a3937dc1b59838489a312f75fe7e90ba6844a84dce4" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.0" + facebook_app_events: + dependency: transitive + description: + name: facebook_app_events + sha256: "88f8564e065be24ba902577709be3a62144deefb0ede2ce86a758c895db0ed41" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.19.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.2" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.4" + firebase_analytics: + dependency: transitive + description: + name: firebase_analytics + sha256: "5e92d510eacd66c354718fd9cc8f66ffdfa025640b645c4742297fb973770508" + url: "https://pub.flutter-io.cn" + source: hosted + version: "10.7.4" + firebase_analytics_platform_interface: + dependency: transitive + description: + name: firebase_analytics_platform_interface + sha256: "6d9baa077d16b47ef5f19d982c4fc475597991aa53b0c601216faa3e1cdab45f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.9.0" + firebase_analytics_web: + dependency: transitive + description: + name: firebase_analytics_web + sha256: "89a740249bce9d52a99db4e501be6087ca6749c73c47cff2b174802be10abd81" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.5+12" + firebase_auth: + dependency: transitive + description: + name: firebase_auth + sha256: "88f88d541a2c1903c023355e13d077835573a200bbf57e12a6a2c24bf99665a1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.15.3" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + sha256: "3c9cfaccb7549492edf5b0c67c6dd1c6727c7830891aa6727f2fb225f0226626" + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.9" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + sha256: c7b1379ccef7abf4b6816eede67a868c44142198e42350f51c01d8fc03f95a7d + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.8.13" + firebase_core: + dependency: transitive + description: + name: firebase_core + sha256: "96607c0e829a581c2a483c658f04e8b159964c3bae2730f73297070bc85d40bb" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.24.2" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: c437ae5d17e6b5cc7981cf6fd458a5db4d12979905f9aafd1fea930428a9fe63 + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: d585bdf3c656c3f7821ba1bd44da5f13365d22fcecaf5eb75c4295246aaa83c0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.10.0" + firebase_crashlytics: + dependency: transitive + description: + name: firebase_crashlytics + sha256: "5ccdf05de039f9544d0ba41c5ae2052ca2425985d32229911b09f69981164518" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.4.8" + firebase_crashlytics_platform_interface: + dependency: transitive + description: + name: firebase_crashlytics_platform_interface + sha256: "359197344def001589c84f8d1d57c05f6e2e773f559205610ce58c25e2045a57" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.6.16" + firebase_dynamic_links: + dependency: transitive + description: + name: firebase_dynamic_links + sha256: b0522806658428803aeb5e7be0b22a29acb8f8697a8909c36965feaeb1f655bd + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.4.8" + firebase_dynamic_links_platform_interface: + dependency: transitive + description: + name: firebase_dynamic_links_platform_interface + sha256: "8b90384d8f85c7211f2b5e2d9d5ae98bd08091f116ef2bd1a74b33574efacc61" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.6+16" + firebase_in_app_messaging: + dependency: transitive + description: + name: firebase_in_app_messaging + sha256: "1d82ecbb72e2de2161c07c1aab056797aefc80b97cc87772525028882412ec3d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.4+8" + firebase_in_app_messaging_platform_interface: + dependency: transitive + description: + name: firebase_in_app_messaging_platform_interface + sha256: "7e7aa5338b8aa82ccee5b5c9468bae27acf5ec710208317ffbe72f51f29e97d6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.4+16" + firebase_messaging: + dependency: transitive + description: + name: firebase_messaging + sha256: "199fe8186a5370d1cf5ce0819191079afc305914e8f38715f5e23943940dfe2d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "14.7.9" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + sha256: "54e283a0e41d81d854636ad0dad73066adc53407a60a7c3189c9656e2f1b6107" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.5.18" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + sha256: "90dc7ed885e90a24bb0e56d661d4d2b5f84429697fd2cbb9e5890a0ca370e6f4" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.5.18" + firebase_remote_config: + dependency: transitive + description: + name: firebase_remote_config + sha256: "60fc92273d1db338a6fad1839c42dedc4ad64f812043acad0cbb200702f5c9ce" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.3.8" + firebase_remote_config_platform_interface: + dependency: transitive + description: + name: firebase_remote_config_platform_interface + sha256: "41813ef8dfbc40ef7a59a73f9e5acef2608dbcb2933241b6c03d52e90677040f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.16" + firebase_remote_config_web: + dependency: transitive + description: + name: firebase_remote_config_web + sha256: "089e92f333c2fb2c05c640c80fecea9d1e06dada0ba85efe34a580987ef94a0a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.16" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "04be3e934c52e082558cc9ee21f42f5c1cd7a1262f4c63cd0357c08d5bba81ec" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + flame: + dependency: transitive + description: + name: flame + sha256: "24ce7b69bddc4f23aebdd9317fc24f08c37d4b42239baa00a34b768b6de9af85" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_animate: + dependency: transitive + description: + name: flutter_animate + sha256: "1dbc1aabfb8ec1e9d9feed2b675c21fb6b0a11f99be53ec3bc0f1901af6a8eb7" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.3.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.3.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_spinkit: + dependency: transitive + description: + name: flutter_spinkit + sha256: b39c753e909d4796906c5696a14daf33639a76e017136c8d82bf3e620ce5bb8e + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.2.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fluttertoast: + dependency: transitive + description: + name: fluttertoast + sha256: b528e78a4e69957bb8a33d9e8ceaa728801bb7c6ce599e811e49cf6d94d17fef + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.0.9" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.0" + get: + dependency: transitive + description: + name: get + sha256: "2ba20a47c8f1f233bed775ba2dd0d3ac97b4cf32fc17731b3dfc672b06b0e92a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.6.5" + get_it: + dependency: transitive + description: + name: get_it + sha256: "529de303c739fca98cd7ece5fca500d8ff89649f1bb4b4e94fb20954abcd7468" + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.6.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.1" + guru_analytics_flutter: + dependency: transitive + description: + path: "plugins/guru_analytics_flutter" + ref: "v3.0.0" + resolved-ref: f5d536f1388c27e9cd9f0f496b4098a4174882e1 + url: "git@github.com:castbox/guru_app.git" + source: git + version: "2.0.0" + guru_app: + dependency: "direct dev" + description: + path: "." + ref: "v3.0.0" + resolved-ref: f5d536f1388c27e9cd9f0f496b4098a4174882e1 + url: "git@github.com:castbox/guru_app.git" + source: git + version: "3.0.0" + guru_applifecycle_flutter: + dependency: transitive + description: + path: "plugins/guru_applifecycle_flutter" + ref: "v3.0.0" + resolved-ref: f5d536f1388c27e9cd9f0f496b4098a4174882e1 + url: "git@github.com:castbox/guru_app.git" + source: git + version: "0.0.1" + guru_applovin_flutter: + dependency: transitive + description: + path: "plugins/guru_applovin_flutter" + ref: "v3.0.0" + resolved-ref: f5d536f1388c27e9cd9f0f496b4098a4174882e1 + url: "git@github.com:castbox/guru_app.git" + source: git + version: "2.3.0" + guru_navigator: + dependency: transitive + description: + path: "plugins/guru_navigator" + ref: "v3.0.0" + resolved-ref: f5d536f1388c27e9cd9f0f496b4098a4174882e1 + url: "git@github.com:castbox/guru_app.git" + source: git + version: "0.0.1" + guru_platform_data: + dependency: transitive + description: + path: "plugins/guru_platform_data" + ref: "v3.0.0" + resolved-ref: f5d536f1388c27e9cd9f0f496b4098a4174882e1 + url: "git@github.com:castbox/guru_app.git" + source: git + version: "0.0.1" + guru_popup: + dependency: transitive + description: + path: "packages/guru_popup" + ref: "v3.0.0" + resolved-ref: "027f3aa24d64a684af0f710e56bd547ccca14fc2" + url: "git@github.com:castbox/guru_ui.git" + source: git + version: "3.0.0" + guru_spec: + dependency: "direct dev" + description: + path: "packages/guru_spec" + ref: "v3.0.0" + resolved-ref: f5d536f1388c27e9cd9f0f496b4098a4174882e1 + url: "git@github.com:castbox/guru_app.git" + source: git + version: "1.1.0" + guru_utils: + dependency: "direct dev" + description: + path: "packages/guru_utils" + ref: "v3.0.0" + resolved-ref: f5d536f1388c27e9cd9f0f496b4098a4174882e1 + url: "git@github.com:castbox/guru_app.git" + source: git + version: "3.0.0" + guru_widgets: + dependency: "direct dev" + description: + path: "packages/guru_widgets" + ref: "v3.0.0" + resolved-ref: "027f3aa24d64a684af0f710e56bd547ccca14fc2" + url: "git@github.com:castbox/guru_ui.git" + source: git + version: "3.0.0" + http: + dependency: transitive + description: + name: http + sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.3" + in_app_purchase: + dependency: transitive + description: + name: in_app_purchase + sha256: bdda02b5b11b56d5e29c7f0c57c433db3452b0c8ce1c37cbfcf1de52946efd9f + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.11" + in_app_purchase_android: + dependency: transitive + description: + name: in_app_purchase_android + sha256: c4b84caa4e2c7ffebda444c5033fd8423cc3a45a6e1066929bbbcd4daf665db5 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.0+15" + in_app_purchase_platform_interface: + dependency: transitive + description: + name: in_app_purchase_platform_interface + sha256: "356a855d5a1f92b0d06a739e702fd732b56ee1914e6ba0f2ba08a2cb8e2cc3f8" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.4" + in_app_purchase_storekit: + dependency: transitive + description: + name: in_app_purchase_storekit + sha256: f598169999604d9c40c28bb6f47b58e03c8c498e344cdc015f937a1f528982ab + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.6+4" + injectable: + dependency: "direct main" + description: + name: injectable + sha256: cd3c422e13270c81f64ab73c80406b2b2ed563fe59d0ff2093eb7eee63d0bbeb + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + intl: + dependency: transitive + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.18.1" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: a5e201311cb08bf3912ebbe9a2be096e182d703f881136ec1e81a2338a9e120d + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.4" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.8.1" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.7.1" + lints: + dependency: transitive + description: + name: lints + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + logger: + dependency: transitive + description: + name: logger + sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.2+1" + logging: + dependency: transitive + description: + name: logging + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + lottie: + dependency: transitive + description: + name: lottie + sha256: a93542cc2d60a7057255405f62252533f8e8956e7e06754955669fd32fb4b216 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.7.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.16" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.0" + meta: + dependency: transitive + description: + name: meta + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.0" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + ordered_set: + dependency: transitive + description: + name: ordered_set + sha256: "3858c7d84619edfab87c3e367584648020903187edb70b52697646f4b2a93022" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.3" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.2" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.1" + permission_handler: + dependency: transitive + description: + name: permission_handler + sha256: "860c6b871c94c78e202dc69546d4d8fd84bd59faeb36f8fb9888668a53ff4f78" + url: "https://pub.flutter-io.cn" + source: hosted + version: "11.1.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "2f1bec180ee2f5665c22faada971a8f024761f632e93ddc23310487df52dcfa6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "12.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: "1a816084338ada8d574b1cb48390e6e8b19305d5120fe3a37c98825bacc78306" + url: "https://pub.flutter-io.cn" + source: hosted + version: "9.2.0" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "11b762a8c123dced6461933a88ea1edbbe036078c3f9f41b08886e678e7864df" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.0+2" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: d87349312f7eaf6ce0adaf668daf700ac5b06af84338bd8b8574dfbd93ffe1a1 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.2" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1e8640c1e39121128da6b816d236e714d2cf17fac5a105dd6acdd3403a628004" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" + persistent: + dependency: transitive + description: + path: "plugins/persistent" + ref: "v3.0.0" + resolved-ref: f5d536f1388c27e9cd9f0f496b4098a4174882e1 + url: "git@github.com:castbox/guru_app.git" + source: git + version: "0.0.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.1.0" + platform: + dependency: transitive + description: + name: platform + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.7.3" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.1" + process: + dependency: transitive + description: + name: process + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.2.4" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.3" + retrofit: + dependency: transitive + description: + name: retrofit + sha256: "04ed77c82cadb655bb9357e8d0cb9da72ff704749a2d0cfe6540dd1f1f7ca4b9" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.3" + retrofit_generator: + dependency: "direct dev" + description: + name: retrofit_generator + sha256: "591edbd43e4098e33155c7bd908ac6cb32faa85d140aadf71bd06a5cf01a0092" + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.0.5" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.27.7" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + soundpool: + dependency: transitive + description: + path: "plugins/soundpool" + ref: "v3.0.0" + resolved-ref: f5d536f1388c27e9cd9f0f496b4098a4174882e1 + url: "git@github.com:castbox/guru_app.git" + source: git + version: "2.3.0" + soundpool_macos: + dependency: transitive + description: + name: soundpool_macos + sha256: dd389a629d32e3960fbe55958f2f7e03e074c4987fadf38fbe6c7e0a1ba32528 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + soundpool_platform_interface: + dependency: transitive + description: + name: soundpool_platform_interface + sha256: "9787f96b54a12e236cd3715cd2cc4bcd1fc1131e834ea7b295ac849ab4e24eab" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + soundpool_web: + dependency: transitive + description: + name: soundpool_web + sha256: "3d1eb8d6cceb8a0aec38ff9aec4fbd11a9a8101d27b27a6eb29305b83d46aee5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.3" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.0" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: bb4738f15b23352822f4c42a531677e5c6f522e079461fd240ead29d8d8a54a6 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.0+2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" + system_clock: + dependency: transitive + description: + name: system_clock + sha256: "4926afa2ab15480a420697779d156038bdc6d6b944f434a383e98d8fd97cec09" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.1" + time: + dependency: transitive + description: + name: time + sha256: "83427e11d9072e038364a5e4da559e85869b227cf699a541be0da74f14140124" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.2" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: e9aa5ea75c84cf46b3db4eea212523591211c3cf2e13099ee4ec147f54201c86 + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.2.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.2.2" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.2.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "4aca1e060978e19b2998ee28503f40b5ba6226819c2b5e3e4d1821e8ccd92198" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.0" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.1" + uuid: + dependency: transitive + description: + name: uuid + sha256: b715b8d3858b6fa9f68f87d20d98830283628014750c2b09b6f516c1da4af2a7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + vibration: + dependency: transitive + description: + path: "plugins/vibration" + ref: "v3.0.0" + resolved-ref: f5d536f1388c27e9cd9f0f496b4098a4174882e1 + url: "git@github.com:castbox/guru_app.git" + source: git + version: "1.7.5" + vibration_web: + dependency: transitive + description: + name: vibration_web + sha256: "9126c3941ca2af78a3bd7d412f34d0d2e48a52bca8a307c49970d5dbb97a7f12" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.6.5" + vm_service: + dependency: "direct main" + description: + name: vm_service + sha256: eb3cf3f45fc1500ae30481ac9ab788302fa5e8edc3f3eaddf183945ee93a8bf3 + url: "https://pub.flutter-io.cn" + source: hosted + version: "11.2.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2" + web: + dependency: transitive + description: + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.0" + webview_flutter: + dependency: transitive + description: + name: webview_flutter + sha256: "42393b4492e629aa3a88618530a4a00de8bb46e50e7b3993fedbfdc5352f0dbf" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.4.2" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "161af93c2abaf94ef2192bffb53a3658b2d721a3bf99b69aa1e47814ee18cc96" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.13.2" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: dbe745ee459a16b6fec296f7565a8ef430d0d681001d8ae521898b9361854943 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.9.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: "4d062ad505390ecef1c4bfb6001cd857a51e00912cc9dfb66edb1886a9ebd80c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.10.2" + win32: + dependency: transitive + description: + name: win32 + sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.6" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: e4506d60b7244251bc59df15656a3093501c37fb5af02105a944d73eb95be4c9 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" + xml: + dependency: transitive + description: + name: xml + sha256: ac0e3f4bf00ba2708c33fbabbbe766300e509f8c82dbd4ab6525039813f7e2fb + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" diff --git a/guru_app/packages/guru_assistant/pubspec.yaml b/guru_app/packages/guru_assistant/pubspec.yaml new file mode 100644 index 0000000..a83d9d8 --- /dev/null +++ b/guru_app/packages/guru_assistant/pubspec.yaml @@ -0,0 +1,112 @@ +name: guru_assistant +description: A new Flutter project. +version: 0.0.1 +homepage: + +environment: + sdk: '>=2.18.6 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + injectable: 2.3.2 + draggable_float_widget: ^0.1.0 + vm_service: ^11.2.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + build_runner: 2.4.7 + json_serializable: 6.7.1 + retrofit_generator: 8.0.5 +# file_picker: 5.2.4 + guru_app: + git: + url: git@git.chengdu.pundit.company:castbox/guru_sdk.git + path: guru_app + ref: v3.0.0 + guru_utils: + git: + url: git@git.chengdu.pundit.company:castbox/guru_sdk.git + path: guru_app/packages/guru_utils + ref: v3.0.0 + + guru_spec: + git: + url: git@git.chengdu.pundit.company:castbox/guru_sdk.git + path: guru_app/packages/guru_spec + ref: v3.0.0 + + guru_widgets: + git: + url: git@git.chengdu.pundit.company:castbox/guru_sdk.git + path: guru_ui/packages/guru_widgets + ref: v3.0.0 + + design: + git: + url: git@git.chengdu.pundit.company:castbox/guru_sdk.git + path: guru_ui/packages/design + ref: v3.0.0 + + design_spec: + git: + url: git@git.chengdu.pundit.company:castbox/guru_sdk.git + path: guru_ui/packages/design_spec + ref: v3.0.0 + + design_generator: + git: + url: git@git.chengdu.pundit.company:castbox/guru_sdk.git + path: guru_ui/packages/design_generator + ref: v3.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +dependency_overrides: +# guru_app: +# path: ../../ +# +# guru_utils: +# path: ../../packages/guru_utils +# +# guru_widgets: +# path: ../../../../guru_ui/packages/guru_widgets + + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/guru_app/packages/guru_assistant/test/guru_assistant_test.dart b/guru_app/packages/guru_assistant/test/guru_assistant_test.dart new file mode 100644 index 0000000..acda7da --- /dev/null +++ b/guru_app/packages/guru_assistant/test/guru_assistant_test.dart @@ -0,0 +1,8 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:guru_assistant/guru_assistant.dart'; + +void main() { + test('adds one to input values', () { + }); +} diff --git a/guru_app/packages/guru_spec/.gitignore b/guru_app/packages/guru_spec/.gitignore new file mode 100644 index 0000000..2ee8465 --- /dev/null +++ b/guru_app/packages/guru_spec/.gitignore @@ -0,0 +1,120 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +.vscode +.gradle +.idea +/local.properties +.DS_Store +/build +.metadata +ios/Flutter/flutter_export_environment.sh + +**/.settings +**/.project +**/.classpath + + +# build id +build_id.properties + +# IntelliJ related + +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +/build/ +flutter_* +.flutter-plugins-dependencies + +# goCli related +go-cli/.packages +go-cli/pubspec.lock + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/gradlew +**/android/gradlew.bat +**/android/fastlane/report.xml +**/android/fastlane/README.md + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* +**/ios/build/* +**/ios/fastlane/README.md +**/ios/fastlane/report.xml + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages + +# Crashlytics +debugSymbols/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# i18n +**/android/res/values/strings_en.arb +lib/generated/i18n.dart + +# output +output/ios/adhok/* +output/ios/appstore/* +ios/fastlane/report.xml +android/fastlane/report.xml + +**/android/fastlane/metadata/android/**/images/** +**/ios/fastlane/screenshots/** \ No newline at end of file diff --git a/guru_app/packages/guru_spec/CHANGELOG.md b/guru_app/packages/guru_spec/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/guru_app/packages/guru_spec/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/guru_app/packages/guru_spec/LICENSE b/guru_app/packages/guru_spec/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/guru_app/packages/guru_spec/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/guru_app/packages/guru_spec/README.md b/guru_app/packages/guru_spec/README.md new file mode 100644 index 0000000..8b55e73 --- /dev/null +++ b/guru_app/packages/guru_spec/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/guru_app/packages/guru_spec/analysis_options.yaml b/guru_app/packages/guru_spec/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/guru_app/packages/guru_spec/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/guru_app/packages/guru_spec/build.yaml b/guru_app/packages/guru_spec/build.yaml new file mode 100644 index 0000000..2675226 --- /dev/null +++ b/guru_app/packages/guru_spec/build.yaml @@ -0,0 +1,18 @@ +# Read about `build.yaml` at https://pub.dartlang.org/packages/build_config +targets: + $default: + builders: + source_gen|guru_spec: + generate_for: + - "**/*.yaml" + enabled: True + +builders: + guru_spec: + target: ":guru_spec" + import: "package:guru_spec/guru_spec_generator.dart" + builder_factories: ["guruSpecBuilder"] + build_extensions: { ".dart": [".spec.g.part"] } + auto_apply: dependents + build_to: cache + applies_builders: ["source_gen|combining_builder"] \ No newline at end of file diff --git a/guru_app/packages/guru_spec/lib/guru_spec.dart b/guru_app/packages/guru_spec/lib/guru_spec.dart new file mode 100644 index 0000000..7a4ea94 --- /dev/null +++ b/guru_app/packages/guru_spec/lib/guru_spec.dart @@ -0,0 +1,12 @@ +/// Created by Haoyi on 2022/8/30 + +class GuruSpecCreator { + const GuruSpecCreator._(); +} + +class IntelligentKeys { + const IntelligentKeys(); +} + + +const guruSpecCreator = GuruSpecCreator._(); diff --git a/guru_app/packages/guru_spec/lib/guru_spec_generator.dart b/guru_app/packages/guru_spec/lib/guru_spec_generator.dart new file mode 100644 index 0000000..19ba5ba --- /dev/null +++ b/guru_app/packages/guru_spec/lib/guru_spec_generator.dart @@ -0,0 +1,7 @@ +library guru_spec_generator; + +import 'package:build/build.dart'; +import 'package:guru_spec/src/guru_spec_generator.dart'; +// import 'src/generator.dart'; + +Builder guruSpecBuilder(BuilderOptions options) => generatorFactoryBuilder(options); diff --git a/guru_app/packages/guru_spec/lib/intelligent_keys_generator.dart b/guru_app/packages/guru_spec/lib/intelligent_keys_generator.dart new file mode 100644 index 0000000..286d702 --- /dev/null +++ b/guru_app/packages/guru_spec/lib/intelligent_keys_generator.dart @@ -0,0 +1,7 @@ +library intelligent_keys_generator; + +import 'package:build/build.dart'; +import 'package:guru_spec/src/intelligent_keys_generator.dart'; +/// Created by Haoyi on 2023/3/1 + +Builder intelligentKeysBuilder(BuilderOptions options) => intelligentKeysGeneratorFactoryBuilder(options); \ No newline at end of file diff --git a/guru_app/packages/guru_spec/lib/src/generator.dart b/guru_app/packages/guru_spec/lib/src/generator.dart new file mode 100644 index 0000000..a2385c9 --- /dev/null +++ b/guru_app/packages/guru_spec/lib/src/generator.dart @@ -0,0 +1,468 @@ +// import 'dart:convert'; +// import 'dart:io'; +// +// import 'package:analyzer/dart/element/element.dart'; +// import 'package:build/build.dart'; +// import 'package:code_builder/code_builder.dart'; +// import 'package:dart_style/dart_style.dart'; +// import 'package:guru_spec/guru_spec.dart'; +// import 'package:source_gen/source_gen.dart'; +// import 'package:yaml/yaml.dart'; +// +// /// Created by Haoyi on 2022/5/26 +// +// Builder generatorFactoryBuilder(BuilderOptions options) => +// SharedPartBuilder([GuruSpecGenerator()], "guru_spec"); +// +// class FieldProcessor { +// final dynamic _annotationType; +// final List _validTypeNames; +// final Code Function(FieldElement element, ConstantReader annotation) assigner; +// +// FieldProcessor(this._annotationType, this._validTypeNames, this.assigner); +// } +// +// class GuruSpecGenerator extends GeneratorForAnnotation { +// static const flavorField = "flavor"; +// +// @override +// generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) { +// if (element is! ClassElement) { +// final name = element.displayName; +// final todo = 'Remove the [GuruSpec] annotation from `$name`.'; +// throw InvalidGenerationSourceError( +// 'Generator cannot target `$name`.\n[TODO] $todo', +// todo: 'Remove the [GuruSpec] annotation from `$name`.', +// ); +// } +// if (!element.isAbstract) { +// final name = element.displayName; +// final todo = 'Please change class [$name] to `abstract` class.'; +// throw InvalidGenerationSourceError( +// 'Generator cannot target `$name`.\n[TODO] $todo', +// todo: todo, +// ); +// } +// bool isAppSpec = false; +// for (var type in element.allSupertypes) { +// if (type.getDisplayString(withNullability: false) == "AppSpec") { +// isAppSpec = true; +// break; +// } +// } +// if (!isAppSpec) { +// final name = element.displayName; +// const todo = 'Please inherit the `AppSpec` class.'; +// throw InvalidGenerationSourceError( +// 'Generator cannot target `$name`.\n[TODO] $todo', +// todo: todo, +// ); +// } +// return _implementClass(element, annotation); +// } +// +// String _implementClass(ClassElement element, ConstantReader annotation) { +// log.info("_implementClass methods:${element.fields.map((e) => e.name)}"); +// +// String specName = "guru_spec.yaml"; +// final flavor = annotation.peek(flavorField)?.stringValue ?? ""; +// if (flavor.isNotEmpty) { +// specName = "flavors/$flavor/guru_spec.yaml"; +// } +// final guruSpecFile = File(specName); +// if (!guruSpecFile.existsSync()) { +// final name = element.displayName; +// const todo = 'Not Found guru_spec.yaml File'; +// throw InvalidGenerationSourceError( +// 'Generator cannot target `$name`.\n[TODO] $todo', +// todo: todo, +// ); +// } +// final yaml = guruSpecFile.readAsStringSync(); +// final jsonStr = json.encode(loadYaml(yaml)); +// final yamlMap = json.decode(jsonStr); +// +// log.info("doc:$yamlMap"); +// +// final className = element.name; +// +// final classBuilder = Class((clazz) { +// clazz +// ..name = '_$className' +// ..constructors.add(buildAppSpecConstructor()) +// ..fields.addAll([ +// Field((field) => field +// ..name = "instance" +// ..static = true +// ..assignment = Code("_$className._()") +// ..modifier = FieldModifier.final$), +// _buildAppName(yamlMap['app_name']), +// _buildAppDetails(yamlMap['details']), +// _buildDeployment(yamlMap['deployment']), +// _buildAdsProfile(yamlMap['ads_profile']), +// _buildIapProfile(), +// _buildAdjustProfile(yamlMap['adjust_profile'] ?? {}), +// _buildDefaultRemoteConfig() +// ]) +// ..extend = refer(className); +// }); +// final emitter = DartEmitter(); +// +// final iapClassBuilder = buildIapProductIds(yamlMap['iap_profile'] ?? {}); +// +// final remoteConfigConstantsBuilder = buildRemoteConfigConstantsClass(yamlMap['remote_config']); +// +// final StringBuffer codeString = StringBuffer(); +// codeString +// .write(DartFormatter().format([remoteConfigConstantsBuilder.accept(emitter)].join('\n\n'))); +// codeString.write(DartFormatter().format([iapClassBuilder.accept(emitter)].join('\n\n'))); +// codeString.write(DartFormatter().format([classBuilder.accept(emitter)].join('\n\n'))); +// return codeString.toString(); +// } +// +// Constructor buildAppSpecConstructor() => Constructor((c) => c..name = "_"); +// +// Field _buildAppName(String name) { +// return Field((field) => field +// ..name = "appName" +// ..static = false +// ..assignment = Code("'$name'") +// ..annotations.add(const CodeExpression(Code('override'))) +// ..modifier = FieldModifier.final$); +// } +// +// static const _appDetailsFieldMapping = { +// "saas_app_id": "saasAppId", +// "authority": "authority", +// "storage_prefix": "storagePrefix", +// "default_cdn_prefix": "defaultCdnPrefix", +// "android_gp_url": "androidGooglePlayUrl", +// "ios_spp_store_url": "iosAppStoreUrl", +// "policy_url": "policyUrl", +// "terms_url": "termsUrl", +// "email_url": "emailUrl", +// "package_name": "packageName", +// "bundle_id": "bundleId" +// }; +// +// Field _buildAppDetails(Map detailsMap) { +// final codeBlocks = [const Code("AppDetails(")]; +// for (var fieldEntry in _appDetailsFieldMapping.entries) { +// final fieldName = _appDetailsFieldMapping[fieldEntry.key]; +// final fieldData = detailsMap[fieldEntry.key]; +// if (fieldName != null && fieldData != null) { +// codeBlocks.add(Code("$fieldName: '$fieldData',")); +// } +// } +// codeBlocks.add(const Code(")")); +// +// return Field((field) => field +// ..name = "details" +// ..static = false +// ..assignment = Block.of(codeBlocks) +// ..annotations.add(const CodeExpression(Code('override'))) +// ..modifier = FieldModifier.final$); +// } +// +// static const _deploymentFieldMapping = { +// "property_cache_size": "propertyCacheSize", +// "enable_dithering": "enableDithering", +// "disable_rewards_ads": "disableRewardsAds" +// }; +// +// Field _buildDeployment(Map? deploymentMap) { +// late List codeBlocks; +// if (deploymentMap == null || deploymentMap.isEmpty) { +// codeBlocks = [const Code("Deployment.fromJson({})")]; +// } else { +// codeBlocks = [const Code("Deployment(")]; +// for (var fieldEntry in _deploymentFieldMapping.entries) { +// final fieldName = _deploymentFieldMapping[fieldEntry.key]; +// final fieldData = deploymentMap[fieldEntry.key]; +// if (fieldName != null && fieldData != null) { +// codeBlocks.add(Code("$fieldName: $fieldData,")); +// } +// } +// codeBlocks.add(const Code(")")); +// } +// +// return Field((field) => field +// ..name = "deployment" +// ..static = false +// ..assignment = Block.of(codeBlocks) +// ..annotations.add(const CodeExpression(Code('override'))) +// ..modifier = FieldModifier.final$); +// } +// +// static const _adsProfileFieldMapping = { +// "banner_ad_unit_id": "bannerId", +// "interstitial_ad_unit_id": "interstitialId", +// "rewards_ad_unit_id": "rewardsId", +// "amz_app_id": "amazonAppId", +// "banner_amz_slot_id": "amazonBannerSlotId", +// "interstitial_amz_slot_id": "amazonInterstitialSlotId", +// }; +// +// static const _adsIdsMapping = { +// "banner_ad_unit_id": "AdUnitId", +// "interstitial_ad_unit_id": "AdUnitId", +// "rewards_ad_unit_id": "AdUnitId", +// "amz_app_id": "AdAppId", +// "banner_amz_slot_id": "AdSlotId", +// "interstitial_amz_slot_id": "AdSlotId" +// }; +// +// Field _buildAdsProfile(Map? map) { +// late List codeBlocks = [const Code("AdsProfile(")]; +// if (map == null || map.isEmpty) { +// codeBlocks = [const Code("AdsProfile.invalid")]; +// } else { +// codeBlocks = [const Code("AdsProfile(")]; +// for (var fieldEntry in _adsProfileFieldMapping.entries) { +// final fieldName = _adsProfileFieldMapping[fieldEntry.key]; +// final fieldType = _adsIdsMapping[fieldEntry.key]; +// final fieldData = map[fieldEntry.key]; +// +// if (fieldName != null && fieldType != null && fieldData != null) { +// final android = fieldData['android'] ?? ''; +// final ios = fieldData['ios'] ?? ''; +// codeBlocks.add(Code("$fieldName: const $fieldType(android: '$android', ios: '$ios'),")); +// } +// } +// codeBlocks.add(const Code(")")); +// } +// +// return Field((field) => field +// ..name = "adsProfile" +// ..static = false +// ..assignment = Block.of(codeBlocks) +// ..annotations.add(const CodeExpression(Code('override'))) +// ..modifier = FieldModifier.final$); +// } +// +// Field _buildIapProfile() { +// final codeBlocks = [const Code("IapProfile(")]; +// codeBlocks.add(const Code("noAds: IapProductIds.noAds,")); +// codeBlocks.add(const Code("oneOffChargeIapIds: IapProductIds.oneOffChargeIapIds,")); +// codeBlocks.add(const Code("subscriptionsIapIds: IapProductIds.subscriptionsIapIds)")); +// +// return Field((field) => field +// ..name = "iapProfile" +// ..static = false +// ..assignment = Block.of(codeBlocks) +// ..annotations.add(const CodeExpression(Code('override'))) +// ..modifier = FieldModifier.final$); +// } +// +// Field _buildDefaultRemoteConfig() { +// return Field((field) => field +// ..name = "defaultRemoteConfig" +// ..static = false +// ..assignment = const Code("RemoteConfigConstants._defaultConfigs") +// ..annotations.add(const CodeExpression(Code('override'))) +// ..modifier = FieldModifier.final$); +// } +// +// Field _buildAdjustProfile(Map map) { +// final codeBlocks = [const Code("AdjustProfile(")]; +// final appTokenMap = map['app_token']; +// if (appTokenMap != null) { +// codeBlocks.add(Code( +// "appToken: Platform.isAndroid ? '${appTokenMap['android'] ?? ''}' : '${appTokenMap['ios'] ?? ''}',")); +// } else { +// codeBlocks.add(const Code("appToken: '',")); +// } +// final eventMap = map['event_map'] ?? {}; +// +// final androidMapCodeBlocks = []; +// final iosMapCodeBlocks = []; +// +// Code? generateCode(String event, dynamic token, bool revenue) { +// if (token != null && token != '') { +// if (revenue == true) { +// return Code( +// "\"$event\": (params)=> AdjustEvent(\"$token\")..setRevenue(params['revenue'] ?? 0.0, params['currency'] ?? ''),"); +// } +// return Code("\"$event\": (_) => AdjustEvent(\"$token\"),"); +// } +// return null; +// } +// +// if (eventMap is Map) { +// for (var entry in eventMap.entries) { +// final map = entry.value; +// final androidToken = map['android']; +// final iosToken = map['ios']; +// final revenue = map['revenue'] == true; +// +// final androidCode = generateCode(entry.key, androidToken, revenue); +// if (androidCode != null) { +// androidMapCodeBlocks.add(androidCode); +// } +// +// final iosCode = generateCode(entry.key, iosToken, revenue); +// if (iosCode != null) { +// iosMapCodeBlocks.add(iosCode); +// } +// } +// } +// codeBlocks +// .add(const Code("eventNameMapping: Platform.isAndroid ? {")); +// codeBlocks.addAll(androidMapCodeBlocks); +// codeBlocks.add(const Code("} : {")); +// codeBlocks.addAll(iosMapCodeBlocks); +// codeBlocks.add(const Code("})")); +// +// return Field((field) => field +// ..name = "adjustProfile" +// ..static = false +// ..assignment = Block.of(codeBlocks) +// ..annotations.add(const CodeExpression(Code('override'))) +// ..modifier = FieldModifier.final$); +// } +// +// static String camelCase(String value) { +// if (value == '') { +// return ''; +// } +// +// final separatedWords = value.split(RegExp(r'[!@#<>?":`~;[\]\\|=+)(*&^%-\s_]+')); +// var newString = ''; +// +// for (final word in separatedWords) { +// newString += word[0].toUpperCase() + word.substring(1).toLowerCase(); +// } +// +// return newString[0].toLowerCase() + newString.substring(1); +// } +// +// List _buildIapProductField(Map map) { +// final List fieldList = []; +// String? noAds; +// final List oneOffChargeIapIds = []; +// final List subscriptionsIapIds = []; +// for (var fieldEntry in map.entries) { +// String fieldName = camelCase(fieldEntry.key); +// final fieldData = fieldEntry.value; +// if (fieldName != '' && fieldData != null) { +// final android = fieldData['android'] ?? ''; +// final ios = fieldData['ios'] ?? ''; +// final attr = fieldData['attr'] ?? ''; +// +// if (fieldEntry.key == 'no_ads') { +// fieldName = '_$fieldName'; +// noAds = fieldName; +// } +// +// if (attr == 'subscriptions') { +// subscriptionsIapIds.add(fieldName); +// } else { +// oneOffChargeIapIds.add(fieldName); +// } +// +// fieldList.add(Field((field) => field +// ..name = fieldName +// ..static = true +// ..assignment = +// Code("ProductId(android: '$android', ios: '$ios', attr: TransactionAttributes.$attr)") +// ..modifier = FieldModifier.constant)); +// } +// } +// +// fieldList.add(Field((field) => field +// ..name = "noAds" +// ..static = true +// ..type = refer("ProductId?") +// ..assignment = Code(noAds ?? 'null') +// ..modifier = FieldModifier.constant)); +// +// fieldList.add(Field((field) => field +// ..name = "oneOffChargeIapIds" +// ..static = true +// ..assignment = Code("[${oneOffChargeIapIds.join(',')}]") +// ..modifier = FieldModifier.constant)); +// +// fieldList.add(Field((field) => field +// ..name = "subscriptionsIapIds" +// ..static = true +// ..assignment = Code("[${subscriptionsIapIds.join(',')}]") +// ..modifier = FieldModifier.constant)); +// +// return fieldList; +// } +// +// List buildIapProfileMethods() { +// final List methodList = []; +// methodList.add(Method((method) => method +// ..name = 'ids' +// ..type = MethodType.getter +// ..returns = refer('List') +// ..lambda = true +// ..body = const Code("[...oneOffChargeIapIds, ...subscriptionsIapIds]") +// ..static = true)); +// return methodList; +// } +// +// Class buildIapProductIds(Map map) { +// return Class((clazz) { +// clazz +// ..name = 'IapProductIds' +// ..fields.addAll(_buildIapProductField(map)) +// ..methods.addAll(buildIapProfileMethods()); +// // ..methods.addAll([_buildCreator(element, blocks)]) +// // ..methods.addAll(designSpec.nestedSpec ? [] : [_buildGetter(element)]) +// // ..extend = refer(className); +// }); +// } +// +// Class buildRemoteConfigConstantsClass(Map map) { +// final List fieldList = []; +// final codeBlocks = [const Code("{")]; +// for (var fieldEntry in map.entries) { +// final fieldName = camelCase(fieldEntry.key); +// final fieldData = fieldEntry.value; +// if (fieldName != '' && fieldData != null) { +// fieldList.add(Field((field) => field +// ..name = fieldName +// ..static = true +// ..assignment = Code("'${fieldEntry.key}'") +// ..modifier = FieldModifier.constant)); +// codeBlocks.add(Code("$fieldName: '$fieldData',")); +// } +// } +// codeBlocks.add(const Code("}")); +// +// fieldList.add(Field((field) => field +// ..name = '_defaultConfigs' +// ..static = true +// ..type = refer("Map") +// ..assignment = Block.of(codeBlocks) +// ..modifier = FieldModifier.constant)); +// +// final List methodList = []; +// methodList.add(Method((method) => method +// ..name = 'getDefaultConfigString' +// ..returns = refer('String') +// ..lambda = true +// ..requiredParameters.addAll([ +// Parameter((p) => p +// ..type = refer("String") +// ..name = "key") +// ]) +// ..body = const Code("_defaultConfigs[key]") +// ..static = true)); +// +// return Class((clazz) { +// clazz +// ..name = 'RemoteConfigConstants' +// ..fields.addAll(fieldList) +// ..methods.addAll(methodList); +// // ..methods.addAll([_buildCreator(element, blocks)]) +// // ..methods.addAll(designSpec.nestedSpec ? [] : [_buildGetter(element)]) +// // ..extend = refer(className); +// }); +// } +// +// // => Field((field) => field +// +// } \ No newline at end of file diff --git a/guru_app/packages/guru_spec/lib/src/guru_spec_generator.dart b/guru_app/packages/guru_spec/lib/src/guru_spec_generator.dart new file mode 100644 index 0000000..23b1093 --- /dev/null +++ b/guru_app/packages/guru_spec/lib/src/guru_spec_generator.dart @@ -0,0 +1,1683 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:analyzer/dart/element/element.dart'; +import 'package:build/build.dart'; +import 'package:code_builder/code_builder.dart'; +import 'package:dart_style/dart_style.dart'; +import 'package:glob/glob.dart'; +import 'package:guru_spec/guru_spec.dart'; +import 'package:guru_spec/src/tuple.dart'; +import 'package:source_gen/source_gen.dart'; +import 'package:yaml/yaml.dart'; +import 'package:glob/list_local_fs.dart'; + +/// Created by Haoyi on 2023/1/19 +Builder generatorFactoryBuilder(BuilderOptions options) => + SharedPartBuilder([GuruSpecGenerator()], "guru_spec"); + +class FieldProcessor { + final dynamic _annotationType; + final List _validTypeNames; + final Code Function(FieldElement element, ConstantReader annotation) assigner; + + FieldProcessor(this._annotationType, this._validTypeNames, this.assigner); +} + +class GuruSpecGenerator extends GeneratorForAnnotation { + static const flavorField = "flavor"; + + final Set flavors = {}; + + // method -> flavors + final Map> iapIdsCheckMap = {}; + final Map> iapCapIdsCheckMap = {}; + final Map remoteConfigKeys = {}; + + final Map> idsMethodCheckMap = {}; + + final Map> idsMethodParamsMap = {}; + + final Map> idsMethodMap = {}; + + final Map> productSkuParams = {}; + + // final Map + + final Map> idsCheckMap = {}; + final Map> capIdsCheckMap = {}; + + @override + generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) { + if (element is! ExecutableElement) { + final name = element.displayName; + final todo = 'Remove the [GuruSpecCreator] annotation from `$name`.'; + throw InvalidGenerationSourceError( + 'Generator cannot target `$name`.\n[TODO] $todo', + todo: 'Remove the [GuruSpecCreator] annotation from `$name`.', + ); + } + // if (!element.isAbstract) { + // final name = element.displayName; + // final todo = 'Please change class [$name] to `abstract` class.'; + // throw InvalidGenerationSourceError( + // 'Generator cannot target `$name`.\n[TODO] $todo', + // todo: todo, + // ); + // } + // bool isAppSpec = false; + // for (var type in element.allSupertypes) { + // if (type.getDisplayString(withNullability: false) == "AppSpecCreator") { + // isAppSpec = true; + // break; + // } + // } + // if (!isAppSpec) { + // final name = element.displayName; + // const todo = 'Please inherit the `AppSpec` class.'; + // throw InvalidGenerationSourceError( + // 'Generator cannot target `$name`.\n[TODO] $todo', + // todo: todo, + // ); + // } + return _implementClass(element, annotation); + } + + String _implementClass(ExecutableElement element, ConstantReader annotation) { + log.info("_implementClass methods"); + + final guruSpecFiles = Glob("guru/**guru_spec.yaml", recursive: true); + // const className = "GuruSpecFactory"; + final StringBuffer codeString = StringBuffer(); + final list = guruSpecFiles.listSync(); + log.info("listSync list length:${list.length}"); + for (var entity in list) { + log.info("listSync entity:${entity.path}"); + codeString.write(generateGuruSpecCode(File(entity.path))); + } + + codeString.write(_buildGlobalRemoteConfigConstants()); + codeString.write(_buildGlobalProductIds()); + codeString.write(_buildGlobalProductCategoryConstants()); + codeString.write(_buildAppSpecFactory()); + codeString.write(_buildFlavors()); + return codeString.toString(); + } + + String _buildGlobalProductIds() { + final methodList = []; + for (var field in iapIdsCheckMap.entries) { + final fieldName = field.key; + final codeBlock = []; + for (var flavor in flavors) { + if (field.value.contains(flavor)) { + codeBlock.add(Code("if (GuruApp.instance.flavor == '$flavor') {")); + codeBlock.add(Code("return _${capitalize(flavor)}Products.$fieldName;")); + codeBlock.add(const Code("}")); + } + } + codeBlock.add(const Code("return ProductId.invalid;")); + + methodList.add(Method((method) { + method + ..name = fieldName + ..static = true + ..type = MethodType.getter + ..returns = refer("ProductId") + ..body = Block.of(codeBlock); + })); + } + + for (var field in idsMethodCheckMap.entries) { + final fieldName = field.key; + final codeBlock = []; + final params = idsMethodParamsMap[fieldName]; + if (params == null) { + continue; + } + for (var flavor in flavors) { + if (field.value.contains(flavor)) { + codeBlock.add(Code("if (GuruApp.instance.flavor == '$flavor') {")); + codeBlock.add(Code( + "return _${capitalize(flavor)}Products.$fieldName(${params.map((e) => e.name).join(',')});")); + codeBlock.add(const Code("}")); + } + } + codeBlock.add(const Code("return ProductId.invalid;")); + + methodList.add(Method((method) { + method + ..name = fieldName + ..static = true + ..requiredParameters.addAll(params) + ..returns = refer("ProductId") + ..body = Block.of(codeBlock); + })); + } + + for (var capability in _iapProductIdsCapabilities) { + final fieldName = buildCapIds(capability); + final codeBlock = []; + for (var flavor in flavors) { + if (iapCapIdsCheckMap[fieldName]?.contains(flavor) == true) { + codeBlock.add(Code("if (GuruApp.instance.flavor == '$flavor') {")); + codeBlock.add(Code("return _${capitalize(flavor)}Products.$fieldName;")); + codeBlock.add(const Code("}")); + } + } + codeBlock.add(const Code("return {};")); + + methodList.add(Method((method) { + method + ..name = fieldName + ..static = true + ..type = MethodType.getter + ..returns = refer("Set") + ..body = Block.of(codeBlock); + })); + } + + final codeBlock = []; + // for (var flavor in flavors) { + // codeBlock.add(Code("if (GuruApp.instance.flavor == '$flavor') {")); + // codeBlock.add(Code("return _${capitalize(flavor)}ProductIds.oneOffChargeIapIds;")); + // codeBlock.add(const Code("}")); + // } + + codeBlock.clear(); + + codeBlock.clear(); + codeBlock.add(const Code("GuruApp.instance.productProfile.iapIds")); + methodList.add(Method((method) { + method + ..name = "iapIds" + ..static = true + ..lambda = true + ..type = MethodType.getter + ..returns = refer("Set") + ..body = Block.of(codeBlock); + })); + + codeBlock.clear(); + codeBlock.add(const Code("GuruApp.instance.productProfile.oneOffChargeIapIds")); + // codeBlock.add(const Code("return [];")); + methodList.add(Method((method) { + method + ..name = "oneOffChargeIapIds" + ..static = true + ..lambda = true + ..type = MethodType.getter + ..returns = refer("Set") + ..body = Block.of(codeBlock); + })); + + codeBlock.clear(); + // for (var flavor in flavors) { + // codeBlock.add(Code("if (GuruApp.instance.flavor == '$flavor') {")); + // codeBlock.add(Code("return _${capitalize(flavor)}ProductIds.subscriptionsIapIds;")); + // codeBlock.add(const Code("}")); + // } + // codeBlock.add(const Code("return [];")); + codeBlock.add(const Code("GuruApp.instance.productProfile.subscriptionsIapIds")); + + methodList.add(Method((method) { + method + ..name = "subscriptionsIapIds" + ..static = true + ..lambda = true + ..type = MethodType.getter + ..returns = refer("Set") + ..body = Block.of(codeBlock); + })); + + codeBlock.clear(); + codeBlock.add(const Code("GuruApp.instance.productProfile.pointsIapIds")); + methodList.add(Method((method) { + method + ..name = "pointsIapIds" + ..static = true + ..lambda = true + ..type = MethodType.getter + ..returns = refer("Set") + ..body = Block.of(codeBlock); + })); + + codeBlock.clear(); + codeBlock.add(const Code("GuruApp.instance.productProfile.rewardIds")); + methodList.add(Method((method) { + method + ..name = "rewardIds" + ..static = true + ..lambda = true + ..type = MethodType.getter + ..returns = refer("Set") + ..body = Block.of(codeBlock); + })); + + final classBuilder = Class((clazz) { + clazz + ..name = "ProductIds" + ..methods.addAll(methodList); + }); + + final emitter = DartEmitter(); + return DartFormatter().format([classBuilder.accept(emitter)].join('\n\n')); + } + + String _buildGlobalProductCategoryConstants() { + final categoryClassBuilder = Class((clazz) { + clazz + ..name = 'ProductCategory' + ..fields.addAll([...categoryFieldMap.values]) + ..methods.addAll([...categoryMethodMap.values]); + }); + final emitter = DartEmitter(); + return DartFormatter().format([categoryClassBuilder.accept(emitter)].join('\n\n')); + } + + String _buildGlobalRemoteConfigConstants() { + final List fieldList = []; + final codeBlocks = [const Code("{")]; + for (var fieldEntry in remoteConfigKeys.entries) { + final fieldValue = fieldEntry.key; + final fieldName = fieldEntry.value; + + fieldList.add(Field((field) => field + ..name = fieldName + ..static = true + ..assignment = Code("'$fieldValue'") + ..modifier = FieldModifier.constant)); + } + + final classBuilder = Class((clazz) { + clazz + ..name = 'RemoteConfigConstants' + ..fields.addAll(fieldList); + }); + final emitter = DartEmitter(); + return DartFormatter().format([classBuilder.accept(emitter)].join('\n\n')); + } + + String _buildAppSpecFactory() { + final creator = []; + for (var flavor in flavors) { + creator.add(Code("if (flavor == '$flavor') {")); + creator.add(Code("return _${capitalize(flavor)}AppSpec._instance;")); + creator.add(const Code("}")); + } + + creator.add(const Code("throw NotImplementationAppSpecCreatorException();")); + + final classBuilder = Class((clazz) { + clazz + ..name = "_GuruSpecFactory" + ..methods.add(Method((method) { + method + ..name = "create" + ..static = true + ..requiredParameters.addAll([ + Parameter((p) => p + ..type = refer("String") + ..name = "flavor") + ]) + ..returns = refer("AppSpec") + ..body = Block.of(creator); + })); + }); + + final emitter = DartEmitter(); + return DartFormatter().format([classBuilder.accept(emitter)].join('\n\n')); + } + + String _buildFlavors() { + final List fieldList = []; + for (var flavor in flavors) { + fieldList.add(Field((field) => field + ..name = camelCase(flavor) + ..static = true + ..assignment = Code("'$flavor'") + ..modifier = FieldModifier.constant)); + } + final classBuilder = Class((clazz) { + clazz + ..name = "Flavors" + ..fields.addAll(fieldList); + }); + + final emitter = DartEmitter(); + return DartFormatter().format([classBuilder.accept(emitter)].join('\n\n')); + } + + String generateGuruSpecCode(File file) { + final yaml = file.readAsStringSync(); + final jsonStr = json.encode(loadYaml(yaml)); + final yamlMap = json.decode(jsonStr); + + log.info("doc:$yamlMap"); + + final flavor = yamlMap['flavor'] ?? "Main"; + + final className = "${capitalize(flavor)}AppSpec"; + + flavors.add(flavor); + + // final iapClassBuilder = buildIapProductIds(yamlMap['iap_profile'] ?? {}, flavor: flavor); + + try { + final productsClassBuilders = buildProducts(yamlMap['products'] ?? {}, flavor: flavor); + + // final productManifestClassBuilder = + // _buildProductManifest(flavor, yamlMap['product_manifest'] ?? {}); + + final remoteConfigConstantsBuilder = + buildRemoteConfigConstantsClass(yamlMap['remote_config'], flavor: flavor); + + final classBuilder = Class((clazz) { + clazz + ..name = '_$className' + ..constructors.add(buildAppSpecConstructor()) + ..fields.addAll([ + Field((field) => field + ..name = "_instance" + ..static = true + ..assignment = Code("_$className._()") + ..modifier = FieldModifier.final$), + _buildAppName(yamlMap['app_name']), + _buildFlavor(yamlMap['flavor'] ?? "Main"), + _buildAppDetails(yamlMap['details']), + _buildDeployment(yamlMap['deployment']), + _buildAdsProfile(yamlMap['ads_profile'], yamlMap['details'] ?? {}), + _buildProductProfile(flavor: flavor), + _buildAdjustProfile(yamlMap['adjust_profile'] ?? {}), + _buildDefaultRemoteConfig(flavor: flavor) + ]) + ..extend = refer("AppSpec"); + }); + final emitter = DartEmitter(); + + final StringBuffer codeString = StringBuffer(); + codeString.write( + DartFormatter().format([remoteConfigConstantsBuilder.accept(emitter)].join('\n\n'))); + // codeString.write(DartFormatter().format([iapClassBuilder.accept(emitter)].join('\n\n'))); + codeString.write(DartFormatter().format([classBuilder.accept(emitter)].join('\n\n'))); + // codeString + // .write(DartFormatter().format([productManifestClassBuilder.accept(emitter)].join('\n\n'))); + for (var productsClassBuilder in productsClassBuilders) { + codeString + .write(DartFormatter().format([productsClassBuilder.accept(emitter)].join('\n\n'))); + } + return codeString.toString(); + } catch (error, stacktrace) { + log.info("error:$stacktrace"); + rethrow; + } + } + + Constructor buildAppSpecConstructor() => Constructor((c) => c..name = "_"); + + Field _buildAppName(String name) { + return Field((field) => field + ..name = "appName" + ..static = false + ..assignment = Code("'$name'") + ..annotations.add(const CodeExpression(Code('override'))) + ..modifier = FieldModifier.final$); + } + + Field _buildFlavor(String flavor) { + return Field((field) => field + ..name = "flavor" + ..static = false + ..assignment = Code("'$flavor'") + ..annotations.add(const CodeExpression(Code('override'))) + ..modifier = FieldModifier.final$); + } + + static const _appDetailsFieldMapping = { + "saas_app_id": "saasAppId", + "authority": "authority", + "storage_prefix": "storagePrefix", + "default_cdn_prefix": "defaultCdnPrefix", + "android_gp_url": "androidGooglePlayUrl", + "ios_spp_store_url": "iosAppStoreUrl", + "policy_url": "policyUrl", + "terms_url": "termsUrl", + "email_url": "emailUrl", + "package_name": "packageName", + "bundle_id": "bundleId", + "facebook_app_id": "facebookAppId" + }; + + static final appDetails = {}; + + Field _buildAppDetails(Map detailsMap) { + final codeBlocks = [const Code("AppDetails(")]; + for (var fieldEntry in _appDetailsFieldMapping.entries) { + final fieldName = _appDetailsFieldMapping[fieldEntry.key]; + final fieldData = detailsMap[fieldEntry.key]; + if (fieldName != null && fieldData != null) { + codeBlocks.add(Code("$fieldName: '$fieldData',")); + } + } + codeBlocks.add(const Code(")")); + + return Field((field) => field + ..name = "details" + ..static = false + ..assignment = Block.of(codeBlocks) + ..annotations.add(const CodeExpression(Code('override'))) + ..modifier = FieldModifier.final$); + } + + static const _deploymentFieldMapping = { + "property_cache_size": "propertyCacheSize", + "enable_dithering": "enableDithering", + "disable_rewards_ads": "disableRewardsAds", + "enable_analytics_statistic": "enableAnalyticsStatistic", + "auto_restore_iap": "autoRestoreIap", + "igc_balance_secret": "igcBalanceSecret", + "init_igc": "initIgc", + "auto_request_notification_permission": "autoRequestNotificationPermission", + "log_file_size_limit": "logFileSizeLimit", + "log_file_count": "logFileCount", + "persistent_log_level": "persistentLogLevel", + "ios_validate_receipt_password": "iosValidateReceiptPassword", + "conversion_events": "conversionEvents", + "api_connect_timeout": "apiConnectTimeout", + "api_receive_timeout": "apiReceiveTimeout", + "ios_sandbox_subs_renewal_speed": "iosSandboxSubsRenewalSpeed", + "ads_compliant_initialization": "adsCompliantInitialization", + "notification_permission_prompt_trigger": "notificationPermissionPromptTrigger", + "tracking_notification_permission_pass": "trackingNotificationPermissionPass", + "tracking_notification_permission_pass_limit_times": "trackingNotificationPermissionPassLimitTimes", + "allow_interstitial_as_alternative_reward": "allowInterstitialAsAlternativeReward", + "show_internal_ads_when_banner_unavailable": "showInternalAdsWhenBannerUnavailable" + }; + + final RegExp numericOrBoolRegex = RegExp(r'^(true|false|-?\d+)$'); + final enumFieldMap = >>{ + "notificationPermissionPromptTrigger": + const Tuple2("PromptTrigger", {"rationale", "request"}), + }; + + Field _buildDeployment(Map? deploymentMap) { + late List codeBlocks; + if (deploymentMap == null || deploymentMap.isEmpty) { + codeBlocks = [const Code("Deployment.fromJson({})")]; + } else { + codeBlocks = [const Code("Deployment(")]; + for (var fieldEntry in _deploymentFieldMapping.entries) { + final fieldName = _deploymentFieldMapping[fieldEntry.key]; + final fieldData = deploymentMap[fieldEntry.key]; + if (fieldName != null && fieldData != null) { + if (enumFieldMap.containsKey(fieldName)) { + final tuple = enumFieldMap[fieldName]!; + if (tuple.item2.contains(fieldData)) { + codeBlocks.add(Code("$fieldName: ${enumFieldMap[fieldName]!.item1}.$fieldData,")); + } + } else if (fieldData is String && !numericOrBoolRegex.hasMatch(fieldData)) { + codeBlocks.add(Code("$fieldName: '$fieldData',")); + } else if (fieldEntry.key == "conversion_events" && fieldData is List) { + codeBlocks.add(Code("$fieldName: {")); + codeBlocks.addAll(fieldData.map((value) => Code("'$value',"))); + codeBlocks.add(const Code("},")); + } else { + codeBlocks.add(Code("$fieldName: $fieldData,")); + } + } + } + codeBlocks.add(const Code(")")); + } + return Field((field) => field + ..name = "deployment" + ..static = false + ..assignment = Block.of(codeBlocks) + ..annotations.add(const CodeExpression(Code('override'))) + ..modifier = FieldModifier.final$); + } + + static const _adsProfileFieldMapping = { + "banner_ad_unit_id": "bannerId", + "interstitial_ad_unit_id": "interstitialId", + "rewards_ad_unit_id": "rewardsId", + "amz_app_id": "amazonAppId", + "banner_amz_slot_id": "amazonBannerSlotId", + "interstitial_amz_slot_id": "amazonInterstitialSlotId", + "rewarded_amz_slot_id": "amazonRewardedSlotId" + }; + + static const _adsIdsMapping = { + "banner_ad_unit_id": "AdUnitId", + "interstitial_ad_unit_id": "AdUnitId", + "rewards_ad_unit_id": "AdUnitId", + "amz_app_id": "AdAppId", + "banner_amz_slot_id": "AdSlotId", + "interstitial_amz_slot_id": "AdSlotId", + "rewarded_amz_slot_id": "AdSlotId" + }; + + static const _iapProductIdsCapabilities = {"noAds"}; + static const productIdsCapabilities = {"noAds"}; + + static String buildCapIds(String cap) => "${cap}CapIds"; + + Field _buildAdsProfile(Map? map, Map detailsMap) { + late List codeBlocks = [const Code("AdsProfile(")]; + if (map == null || map.isEmpty) { + codeBlocks = [const Code("AdsProfile.invalid")]; + } else { + codeBlocks = [const Code("AdsProfile(")]; + for (var fieldEntry in _adsProfileFieldMapping.entries) { + final fieldName = _adsProfileFieldMapping[fieldEntry.key]; + final fieldType = _adsIdsMapping[fieldEntry.key]; + final fieldData = map[fieldEntry.key]; + + if (fieldName != null && fieldType != null && fieldData != null) { + final android = fieldData['android'] ?? ''; + final ios = fieldData['ios'] ?? ''; + codeBlocks.add(Code("$fieldName: const $fieldType(android: '$android', ios: '$ios'),")); + } + } + codeBlocks.add(Code( + "pubmaticAppStoreUrl: Platform.isAndroid ? '${detailsMap["android_gp_url"] ?? ""}' : '${detailsMap["ios_spp_store_url"] ?? ""}'")); + codeBlocks.add(const Code(")")); + } + + return Field((field) => field + ..name = "adsProfile" + ..static = false + ..assignment = Block.of(codeBlocks) + ..annotations.add(const CodeExpression(Code('override'))) + ..modifier = FieldModifier.final$); + } + + // Field _buildProductProfile({String flavor = ""}) { + // final idsFieldName = "_${capitalize(flavor)}IapProductIds"; + // final codeBlocks = [const Code("IapProfile(")]; + // codeBlocks.add(Code("oneOffChargeIapIds: $idsFieldName.oneOffChargeIapIds,")); + // codeBlocks.add(Code("subscriptionsIapIds: $idsFieldName.subscriptionsIapIds,")); + // for (var cap in _iapProductIdsCapabilities) { + // final fieldName = buildCapIds(cap); + // if (iapCapIdsCheckMap.containsKey(fieldName)) { + // codeBlocks.add(Code("$fieldName: $idsFieldName.$fieldName,")); + // } + // } + // codeBlocks.add(const Code(")")); + // + // return Field((field) => field + // ..name = "iapProfile" + // ..static = false + // ..assignment = Block.of(codeBlocks) + // ..annotations.add(const CodeExpression(Code('override'))) + // ..modifier = FieldModifier.final$); + // } + + Field _buildProductProfile({String flavor = ""}) { + final idsFieldName = "_${capitalize(flavor)}Products"; + final codeBlocks = [const Code("ProductProfile(")]; + codeBlocks.add(Code("oneOffChargeIapIds: $idsFieldName.oneOffChargeIapIds,")); + codeBlocks.add(Code("subscriptionsIapIds: $idsFieldName.subscriptionsIapIds,")); + codeBlocks.add(Code("pointsIapIds: $idsFieldName.pointsIapIds,")); + codeBlocks.add(Code("rewardIds: $idsFieldName.rewardIds,")); + codeBlocks.add(Code("igcIds: $idsFieldName.igcIds,")); + codeBlocks.add(Code("groupMap: $idsFieldName.groupMap,")); + codeBlocks.add(Code("manifestBuilders: $idsFieldName.manifestBuilders,")); + for (var cap in _iapProductIdsCapabilities) { + final fieldName = buildCapIds(cap); + if (iapCapIdsCheckMap.containsKey(fieldName)) { + codeBlocks.add(Code("$fieldName: $idsFieldName.$fieldName,")); + } + } + codeBlocks.add(const Code(")")); + + return Field((field) => field + ..name = "productProfile" + ..static = false + ..assignment = Block.of(codeBlocks) + ..annotations.add(const CodeExpression(Code('override'))) + ..modifier = FieldModifier.final$); + } + + Field _buildDefaultRemoteConfig({String flavor = ""}) { + return Field((field) => field + ..name = "defaultRemoteConfig" + ..static = false + ..assignment = Code("_${capitalize(flavor)}RemoteConfigConstants._defaultConfigs") + ..annotations.add(const CodeExpression(Code('override'))) + ..modifier = FieldModifier.final$); + } + + Field _buildAdjustProfile(Map map) { + final codeBlocks = [const Code("AdjustProfile(")]; + final appTokenMap = map['app_token']; + if (appTokenMap != null) { + codeBlocks.add(Code( + "appToken: Platform.isAndroid ? '${appTokenMap['android'] ?? ''}' : '${appTokenMap['ios'] ?? ''}',")); + } else { + codeBlocks.add(const Code("appToken: '',")); + } + final eventMap = map['event_map'] ?? {}; + + final androidMapCodeBlocks = []; + final iosMapCodeBlocks = []; + + Code? generateCode(String event, dynamic token, bool hasParams) { + if (token != null && token != '') { + if (hasParams == true) { + return Code( + "\"$event\": (params)=> AdjustProfile.createAdjustEvent(\"$token\", params),"); + } + return Code("\"$event\": (_) => AdjustEvent(\"$token\"),"); + } + return null; + } + + if (eventMap is Map) { + for (var entry in eventMap.entries) { + final map = entry.value; + final androidToken = map['android']; + final iosToken = map['ios']; + final hasParams = (map['revenue'] == true) || map["params"] == true; + + final androidCode = generateCode(entry.key, androidToken, hasParams); + if (androidCode != null) { + androidMapCodeBlocks.add(androidCode); + } + + final iosCode = generateCode(entry.key, iosToken, hasParams); + if (iosCode != null) { + iosMapCodeBlocks.add(iosCode); + } + } + } + codeBlocks + .add(const Code("eventNameMapping: Platform.isAndroid ? {")); + codeBlocks.addAll(androidMapCodeBlocks); + codeBlocks.add(const Code("} : {")); + codeBlocks.addAll(iosMapCodeBlocks); + codeBlocks.add(const Code("})")); + + return Field((field) => field + ..name = "adjustProfile" + ..static = false + ..assignment = Block.of(codeBlocks) + ..annotations.add(const CodeExpression(Code('override'))) + ..modifier = FieldModifier.final$); + } + + static String camelCase(String value) { + if (value == '') { + return ''; + } + + final separatedWords = value.split(RegExp(r'[!@#<>?":`~;[\]\\|=+)(*&^%-\s_]+')); + var newString = ''; + + for (final word in separatedWords) { + newString += word[0].toUpperCase() + word.substring(1).toLowerCase(); + } + + return newString[0].toLowerCase() + newString.substring(1); + } + + // + String capitalize(String text, {bool useCamelCase = false}) { + final value = useCamelCase ? camelCase(text) : text; + switch (value.length) { + case 0: + return value; + case 1: + return value.toUpperCase(); + default: + return value.substring(0, 1).toUpperCase() + value.substring(1); + } + } + + // + // List _buildIapProductField(Map map, String flavor) { + // final List fieldList = []; + // // String? noAds; + // final List oneOffChargeIapIds = []; + // final List subscriptionsIapIds = []; + // final Map> capabilitiesIapIds = {}; + // for (var fieldEntry in map.entries) { + // String fieldName = camelCase(fieldEntry.key); + // final fieldData = fieldEntry.value; + // if (fieldName != '' && fieldData != null) { + // final android = fieldData['android'] ?? ''; + // final ios = fieldData['ios'] ?? ''; + // final attr = fieldData['attr'] ?? ''; + // final capabilities = (fieldData["capabilities"] ?? '') as String; + // + // if (capabilities != '') { + // final segments = capabilities.split('|'); + // final capabilitiesList = + // segments.where((segment) => _iapProductIdsCapabilities.contains(segment)).toList(); + // for (var capability in capabilitiesList) { + // capabilitiesIapIds[capability] = (capabilitiesIapIds[capability] ??= {}) + // ..add(fieldName); + // } + // } + // + // if (attr == 'subscriptions') { + // subscriptionsIapIds.add(fieldName); + // } else { + // oneOffChargeIapIds.add(fieldName); + // } + // + // fieldList.add(Field((field) => field + // ..name = fieldName + // ..static = true + // ..assignment = + // Code("ProductId(android: '$android', ios: '$ios', attr: TransactionAttributes.$attr)") + // ..modifier = FieldModifier.constant)); + // + // iapIdsCheckMap[fieldName] = (iapIdsCheckMap[fieldName] ??= {})..add(flavor); + // } + // } + // + // for (var entry in capabilitiesIapIds.entries) { + // final fieldName = buildCapIds(entry.key); + // if (entry.value.isNotEmpty) { + // fieldList.add(Field((field) => field + // ..name = fieldName + // ..static = true + // ..assignment = Code("[${entry.value.join(',')}]") + // ..modifier = FieldModifier.constant)); + // iapCapIdsCheckMap[fieldName] = (iapCapIdsCheckMap[fieldName] ??= {})..add(flavor); + // } + // } + // + // fieldList.add(Field((field) => field + // ..name = "oneOffChargeIapIds" + // ..static = true + // ..assignment = Code("[${oneOffChargeIapIds.join(',')}]") + // ..modifier = FieldModifier.constant)); + // + // fieldList.add(Field((field) => field + // ..name = "subscriptionsIapIds" + // ..static = true + // ..assignment = Code("[${subscriptionsIapIds.join(',')}]") + // ..modifier = FieldModifier.constant)); + // + // return fieldList; + // } + + List buildIapProfileMethods() { + final List methodList = []; + methodList.add(Method((method) => method + ..name = 'iapIds' + ..type = MethodType.getter + ..returns = refer('Set') + ..lambda = true + ..body = const Code("{...oneOffChargeIapIds, ...subscriptionsIapIds}") + ..static = true)); + return methodList; + } + + static final Map categoryFieldMap = {}; + static final Map categoryMethodMap = {}; + static final Map categoryFieldNameMap = {}; + + List buildProducts(Map map, {String flavor = ""}) { + final List regExpField = []; + final List fieldList = []; + + final List methodList = []; + final List manifestBuilders = []; + // String? noAds; + final List oneOffChargeIapIds = []; + final List subscriptionsIapIds = []; + final List pointsIapIds = []; + final List rewardIds = []; + final List igcIds = []; + final Map> capabilitiesIds = {}; + final Set groups = {}; + final Map> groupSubsIds = {}; // group key + final Map> productOfferIds = {}; + + for (var fieldEntry in map.entries) { + String fieldName = camelCase(fieldEntry.key); + final fieldData = fieldEntry.value; + if (fieldName == '' || fieldData == null) { + continue; + } + + final attr = fieldData['attr'] ?? ''; + final capabilities = (fieldData["capabilities"] ?? '') as String; + final android = fieldData['android'] ?? ''; + final ios = fieldData['ios'] ?? ''; + final sku = (fieldData['sku'] ?? '') as String; + final methods = ((fieldData['method'] ?? '') as String).split(","); + final manifest = (fieldData['manifest'] ?? {}) as Map; + final offers = fieldData['offers'] ?? []; + final basePlan = fieldData['base_plan'] ?? ''; + final group = fieldData['group'] ?? ''; + final points = ((fieldData['points'] ?? false) as bool) && (attr == 'consumable') ? true : false; + log.info("fieldName:$fieldName fieldData:${fieldData['attr']} points:$points(${fieldData['points']})"); + if (attr == 'subscriptions') { + if (group.isEmpty == true) { + final todo = + 'Please define `group` field to subscriptions [$fieldName] in guru_spec.yaml.'; + throw InvalidGenerationSourceError( + 'Generator cannot target `$fieldName`.\n[TODO] $todo', + todo: todo, + ); + } + groups.add(group); + } + + void addCapabilities(String name) { + final segments = capabilities.split('|'); + final capabilitiesList = + segments.where((segment) => productIdsCapabilities.contains(segment)).toList(); + for (var capability in capabilitiesList) { + capabilitiesIds[capability] = (capabilitiesIds[capability] ??= {})..add(name); + } + } + + final hasOfferSubs = + attr == 'subscriptions' && group.isNotEmpty && basePlan.isNotEmpty && offers.isNotEmpty; + + if (capabilities != '') { + addCapabilities(fieldName); + } + + final List params = []; + final List skuParams = []; + final List ownParams = []; + // methodList.add(_buildProductManifestMethod(sku, fieldName, manifest)); + if (paramsExp.hasMatch(sku)) { + final str = sku.replaceAllMapped(paramsExp, (match) { + final param = match.group(1) ?? ""; + final paramName = camelCase(param); + params.add(Parameter((p) => p + ..type = refer("String") + ..name = paramName)); + skuParams.add(param); + return "\${$paramName}"; + }); + + final regExp = sku.replaceAll(paramsExp, "(.*)"); + + regExpField.add(Field((field) => field + ..name = "${fieldName}RegExp" + ..static = true + ..assignment = Code("RegExp(r\"^$regExp\$\")") + ..modifier = FieldModifier.final$)); + + ownParams.addAll(params); + + if (methods.length > 1) { + params.add(Parameter((p) => p + ..type = refer("TransactionMethod") + ..name = 'method')); + } + + methodList.add(Method((method) => method + ..name = fieldName + ..returns = refer('ProductId') + ..lambda = true + ..requiredParameters.addAll(params) + ..static = true + ..body = Code( + "GuruApp.instance.defineProductId('$str', TransactionAttributes.$attr, ${methods.length > 1 ? 'method' : 'TransactionMethod.${methods.first}'})"))); + idsMethodCheckMap[fieldName] = (idsMethodCheckMap[fieldName] ??= {})..add(flavor); + idsMethodParamsMap[fieldName] = params; + } else { + for (var method in methods) { + if (method == 'iap') { + if (attr == 'subscriptions') { + // if (!hasOfferSubs) { + subscriptionsIapIds.add(fieldName); + groupSubsIds[group] = (groupSubsIds[group] ?? {})..add(fieldName); + // } + } else { + oneOffChargeIapIds.add(fieldName); + } + } else if (method == 'reward') { + rewardIds.add(fieldName); + } else if (method == 'igc') { + igcIds.add(fieldName); + } + } + + if (hasOfferSubs) { + final offerIds = []; + for (var offer in offers) { + final offerProductName = camelCase("${group}_${basePlan}_$offer"); + offerIds.add(offerProductName); + fieldList.add(Field((field) => field + ..name = offerProductName + ..static = true + ..assignment = Code( + "ProductId(android: '$android', ios: '$ios', attr: TransactionAttributes.$attr, basePlan: '$basePlan', offerId: '$offer', originId: $fieldName)") + ..modifier = FieldModifier.constant)); + iapIdsCheckMap[offerProductName] = (iapIdsCheckMap[offerProductName] ??= {}) + ..add(flavor); + addCapabilities(offerProductName); + groupSubsIds[group] = (groupSubsIds[group] ?? {})..add(offerProductName); + subscriptionsIapIds.add(offerProductName); + productOfferIds[fieldName] = (productOfferIds[fieldName] ?? {}) + ..add(offerProductName); + } + + fieldList.add(Field((field) => field + ..name = "${fieldName}OfferIds" + ..static = true + ..assignment = Code("{${offerIds.join(',')}}") + ..modifier = FieldModifier.final$)); + } + fieldList.add(Field((field) => field + ..name = fieldName + ..static = true + ..assignment = sku.isNotEmpty + ? Code("ProductId.fromSku(sku: '$sku', attr: TransactionAttributes.$attr, points: $points)") + : Code( + "ProductId(android: '$android', ios: '$ios',${basePlan.toString().isNotEmpty ? "basePlan: '$basePlan', " : ''}attr: TransactionAttributes.$attr, points: $points)") + ..modifier = FieldModifier.constant)); + if (points) { + pointsIapIds.add(fieldName); + } + iapIdsCheckMap[fieldName] = (iapIdsCheckMap[fieldName] ??= {})..add(flavor); + } + + if (manifest.isNotEmpty) { + String category = (manifest.remove("category") ?? '') as String; + if (category.isNotEmpty != true) { + final todo = 'Please define `category` field to [$fieldName] in guru_spec.yaml.'; + throw InvalidGenerationSourceError( + 'Generator cannot target `$fieldName`.\n[TODO] $todo', + todo: todo, + ); + } + if (fillParamsExp.hasMatch(category)) { + // skuParams + final Map categoryParams = {}; + final oldCategory = category; + category = category.replaceAllMapped(fillParamsExp, (match) { + // categoryParams = + final skuParamsIdx = int.parse(match.group(1)!) - 1; + log.info("skuParamsIdx:$skuParamsIdx skuParams:${skuParams.length}"); + final categoryParamName = skuParams[skuParamsIdx]; + if (!categoryParams.containsKey(categoryParamName)) { + categoryParams[categoryParamName] = Parameter((p) => p + ..type = refer("String") + ..name = camelCase(categoryParamName)); + } + return "\${matches.first.group(${match.group(1)!})!}"; + }); + + if (!categoryMethodMap.containsKey(fieldName)) { + final categoryCode = oldCategory.replaceAllMapped(fillParamsExp, (match) { + final skuParamsIdx = int.parse(match.group(1)!) - 1; + final categoryParamName = camelCase(skuParams[skuParamsIdx]); + return "\${$categoryParamName}"; + }); + log.info("[$fieldName] categoryParams:${categoryParams.length}"); + categoryMethodMap[fieldName] = Method((method) => method + ..name = fieldName + ..static = true + ..requiredParameters.addAll(categoryParams.values.toList()) + ..body = Code("\"$categoryCode\";")); + } + } else { + if (!categoryFieldMap.containsKey(category)) { + final categoryFieldName = camelCase(category); + if (categoryFieldNameMap.containsKey(categoryFieldName.toLowerCase())) { + final todo = + '"`$category` Category and `${categoryFieldNameMap[categoryFieldName]}` Category are defined too similarly. In order to facilitate code comprehension, please differentiate the two categories by adding more specific subcategories, such as Category`${category}1`."'; + throw InvalidGenerationSourceError( + 'Generator cannot target `$fieldName`.\n[TODO] $todo', + todo: todo, + ); + } + categoryFieldNameMap[categoryFieldName.toLowerCase()] = category; + categoryFieldMap[category] = Field((field) => field + ..name = categoryFieldName + ..static = true + ..assignment = Code("'$category'") + ..modifier = FieldModifier.constant); + } + } + + final method = _buildProductManifestMethod( + flavor, fieldName, category, sku, hasOfferSubs, skuParams, manifest); + manifestBuilders.add(method.name!); + methodList.add(method); + + if (attr == "possessive" || attr == 'asset') { + final ownCodeBlock = []; + ownCodeBlock.add(const Code("if (entity.state == TransactionState.success &&")); + ownCodeBlock.add(Code("entity.category == '${fieldEntry.key}') {")); + if (ownParams.isNotEmpty) { + ownCodeBlock.add(Code("final match = ${fieldName}RegExp.firstMatch(entity.sku);")); + for (var idx = 0; idx < ownParams.length; ++idx) { + ownCodeBlock.add(Code( + "${idx == 0 ? 'return' : ''} match?.group(${idx + 1}) == ${ownParams[idx].name} ${idx < ownParams.length - 1 ? "&&" : ";"}")); + } + } else { + ownCodeBlock.add(Code("return $fieldName.sku == entity.sku;")); + } + ownCodeBlock.add(const Code("}")); + ownCodeBlock.add(const Code("return false;")); + + methodList.add(Method((method) => method + ..name = "isOwn${capitalize(fieldName)}" + ..returns = refer('bool') + ..body = Block.of(ownCodeBlock) + ..requiredParameters.add(Parameter((p) => p + ..type = refer("OrderEntity") + ..name = 'entity')) + ..requiredParameters.addAll(ownParams) + ..static = true)); + } + } + } + for (var entry in capabilitiesIds.entries) { + final fieldName = buildCapIds(entry.key); + if (entry.value.isNotEmpty) { + fieldList.add(Field((field) => field + ..name = fieldName + ..static = true + ..assignment = Code("{${entry.value.join(',')}}") + ..modifier = FieldModifier.final$)); + iapCapIdsCheckMap[fieldName] = (iapCapIdsCheckMap[fieldName] ??= {})..add(flavor); + } + } + + for (var group in groups) { + final fieldName = camelCase("${group}_group"); + + final commonIds = groupSubsIds[group] ?? {}; + + fieldList.add(Field((field) => field + ..name = fieldName + ..static = true + ..assignment = Code("{${commonIds.join(',')}}") + ..modifier = FieldModifier.final$)); + iapCapIdsCheckMap[fieldName] = (iapCapIdsCheckMap[fieldName] ??= {})..add(flavor); + } + + final groupSkuPairs = []; + for (var groupEntry in groupSubsIds.entries) { + final group = groupEntry.key; + final ids = groupEntry.value; + for (var id in ids) { + groupSkuPairs.add("$id.sku : '$group'"); + } + } + fieldList.add(Field((field) => field + ..name = "groupMap" + ..static = true + ..assignment = Code("{${groupSkuPairs.join(',')}}") + ..modifier = FieldModifier.final$)); + + fieldList.add(Field((field) => field + ..name = "oneOffChargeIapIds" + ..static = true + ..assignment = Code("{${oneOffChargeIapIds.join(',')}}") + ..modifier = FieldModifier.final$)); + + fieldList.add(Field((field) => field + ..name = "subscriptionsIapIds" + ..static = true + ..assignment = Code("{${subscriptionsIapIds.join(',')}}") + ..modifier = FieldModifier.final$)); + + fieldList.add(Field((field) => field + ..name = "pointsIapIds" + ..static = true + ..assignment = Code("{${pointsIapIds.join(',')}}") + ..modifier = FieldModifier.final$)); + + fieldList.add(Field((field) => field + ..name = "rewardIds" + ..static = true + ..assignment = Code("{${rewardIds.join(',')}}") + ..modifier = FieldModifier.final$)); + + fieldList.add(Field((field) => field + ..name = "igcIds" + ..static = true + ..assignment = Code("{${igcIds.join(',')}}") + ..modifier = FieldModifier.final$)); + + fieldList.add(Field((field) => field + ..name = "manifestBuilders" + ..static = true + ..assignment = Code("[${manifestBuilders.join(',')}]") + ..modifier = FieldModifier.constant)); + + return [ + Class((clazz) { + clazz + ..name = '_${capitalize(flavor)}Products' + ..fields.addAll([...regExpField, ...fieldList]) + ..methods.addAll([...methodList, ...buildIapProfileMethods()]); + }), + ]; + } + + static final Map> validateSameCategoryManifestMap = {}; + + Method _buildProductManifestMethod(String flavor, String productName, String category, String sku, + bool hasOfferSubs, List skuParamsList, Map map) { + final List codeBlocks = []; + final List extrasBlocks = []; + final List detailsBlocks = []; + final Set productValidate = {}; + if (paramsExp.hasMatch(sku)) { + codeBlocks + .add(Code("final matches = ${productName}RegExp.allMatches(intent.productId.sku);")); + codeBlocks.add(const Code("if (matches.isEmpty) {")); + codeBlocks.add(const Code("return null;")); + codeBlocks.add(const Code("}")); + } else { + if (hasOfferSubs) { + final offerIdsField = "${productName}OfferIds"; + codeBlocks.add(Code("if (!$offerIdsField.contains(intent.productId)) {")); + } else { + codeBlocks.add(Code("if (intent.productId != $productName) {")); + } + codeBlocks.add(const Code("return null;")); + codeBlocks.add(const Code("}")); + } + detailsBlocks.add(const Code("final details =
[];")); + extrasBlocks.add(const Code("final extras = {")); + extrasBlocks.add(const Code("ExtraReservedField.scene: intent.scene,")); + extrasBlocks.add(const Code("ExtraReservedField.rate: intent.rate,")); + extrasBlocks.add(const Code("ExtraReservedField.sales: intent.sales,")); + + for (var data in map.entries) { + if (detailsExp.hasMatch(data.key)) { + final details = data.value as Map; + final type = details.remove("type"); + final amount = details.remove("amount"); + if (type == null || amount == null) { + const todo = 'Please define `type` or `amount` field to guru_spec.yaml.'; + throw InvalidGenerationSourceError( + 'Generator cannot target `$productName`.\n[TODO] $todo', + todo: todo, + ); + } + final ignoreSales = details.remove("ignore_sales"); + if (ignoreSales != null && ignoreSales is! bool) { + final todo = + '[$productName] ignoreSales:$ignoreSales ${ignoreSales.runtimeType} field is not `bool`'; + throw InvalidGenerationSourceError( + 'Generator cannot target `$productName`.\n[TODO] $todo', + todo: todo, + ); + } + + if (ignoreSales == true) { + detailsBlocks.add(Code("details.add(Details.define('$type', $amount)")); + } else { + detailsBlocks.add(Code( + "details.add(Details.define('$type', intent.sales ? max($amount, (intent.rate * $amount).toInt()) : $amount)")); + } + for (var detail in details.entries) { + final fieldData = detail.value; + detailsBlocks.add(Code("..${buildSetParams(detail.key, fieldData, skuParamsList)}")); + } + detailsBlocks.add(const Code(");")); + } else { + final pairCode = buildParamsPair(data.key, data.value, skuParamsList); + log.info("data.key:${data.key} data.value:${data.value} = $pairCode"); + productValidate.add(data.key); + if (pairCode != null) { + extrasBlocks.add(Code("$pairCode,")); + } + } + } + + final validateData = (validateSameCategoryManifestMap[productName] ??= productValidate.toSet()); + final checkedData = validateData.toSet()..removeAll(productValidate); + final checkedData2 = productValidate.toSet()..removeAll(validateData); + + if (checkedData.isNotEmpty || checkedData2.isNotEmpty) { + final todo = + '[$flavor]The manifests of the same `$productName` category have mismatched parameters. Please check if the manifest in the `$productName` matches ${validateData..removeAll(skuParamsList)}.'; + throw InvalidGenerationSourceError( + 'Generator cannot target `$productName`.\n[TODO] $todo', + todo: todo, + ); + } + + final List manifestParams = ["'$category'"]; + // bool isConst = false; + if (extrasBlocks.length > 1) { + extrasBlocks.add(const Code("};")); + codeBlocks.addAll(extrasBlocks); + manifestParams.add("extras: extras"); + // isConst = false; + } + + if (hasOfferSubs) { + codeBlocks.add(const Code("if (Platform.isAndroid && intent.productId.hasOffer) {")); + codeBlocks + .add(const Code("extras[ExtraReservedField.basePlanId] = intent.productId.basePlan;")); + codeBlocks.add(const Code("extras[ExtraReservedField.offerId] = intent.productId.offerId;")); + codeBlocks.add(const Code("}")); + } + + if (detailsBlocks.length > 1) { + codeBlocks.addAll(detailsBlocks); + manifestParams.add("details: details"); + // isConst = false; + } + + codeBlocks.add(Code("return Manifest(${manifestParams.join(", ")});")); + + return Method((method) => method + ..name = "build${capitalize(productName)}Manifest" + ..returns = refer('Future') + ..body = Block.of(codeBlocks) + ..modifier = MethodModifier.async + ..requiredParameters.add(Parameter((p) => p + ..type = refer("TransactionIntent") + ..name = 'intent')) + ..static = true); + } + + String? buildSetParams(String fieldName, dynamic fieldData, List skuParamsList) { + if (fieldData is int) { + return "setInt('$fieldName', $fieldData)"; + } else if (fieldData is double) { + return "setDouble('$fieldName', $fieldData})"; + } else if (fieldData is bool) { + return "setBool('$fieldName', ${fieldData ? "true" : "false"})"; + } else { + final filledMatches = fillParamsExp.allMatches(fieldData).toList(); + if (filledMatches.isNotEmpty) { + for (var idx = 0; idx < filledMatches.length; ++idx) { + final match = filledMatches[idx]; + return "setString('${skuParamsList[idx]}', matches.first.group(${match.group(1)})!)"; + } + } else { + final matches = typeParamsExp.allMatches(fieldData); + if (matches.isNotEmpty) { + final fieldName = matches.first.group(1); + final fieldType = matches.first.group(2)?.toLowerCase(); + if (fieldType == 'int') { + return "setInt('$fieldName', $fieldName)"; + } else if (fieldData == "double") { + return "setDouble('$fieldName', $fieldName})"; + } else if (fieldData == "bool") { + return "setBool('$fieldName', $fieldName)"; + } else { + return "setString('$fieldName', $fieldData)"; + } + } else { + return "setString('$fieldName', '$fieldData')"; + } + } + } + return null; + } + + static const _manifestExtraReservedMap = { + "icon": "ExtraReservedField.icon", + "name": "ExtraReservedField.name" + }; + + String? buildParamsPair(String fieldName, dynamic fieldData, List skuParamsList) { + final reservedExtraKey = _manifestExtraReservedMap[fieldName.toLowerCase()]; + if (reservedExtraKey != null) { + // if (fieldData !is String) { + // final todo = 'fieldName `$fieldName` is Reserved field! it need `String` type!! Please check it!!'; + // throw InvalidGenerationSourceError( + // 'Generator cannot target buildParamsPair `$fieldName`.\n[TODO] $todo', + // todo: todo, + // ); + // } + // if (fieldName.toLowerCase() == "scene") { + final todo = 'fieldName `$fieldName` is reserved field! Cannot be defined by guru_spec'; + throw InvalidGenerationSourceError( + 'Generator cannot target buildParamsPair `$fieldName`.\n[TODO] $todo', + todo: todo, + ); + // } + return "$reservedExtraKey : $fieldData"; + } + if (fieldData is int) { + return "'$fieldName': $fieldData"; + } else if (fieldData is double) { + return "'$fieldName': $fieldData"; + } else if (fieldData is bool) { + return "'$fieldName': ${fieldData ? "true" : "false"}"; + } else { + final filledMatches = fillParamsExp.allMatches(fieldData).toList(); + if (filledMatches.isNotEmpty) { + for (var idx = 0; idx < filledMatches.length; ++idx) { + final match = filledMatches[idx]; + final paramsIdx = int.parse(match.group(1)!); + return "'$fieldName': matches.first.group($paramsIdx)!"; + } + } else { + final matches = typeParamsExp.allMatches(fieldData); + if (matches.isNotEmpty) { + final fieldName = matches.first.group(1); + final fieldType = matches.first.group(2)?.toLowerCase(); + if (fieldType == 'int') { + return "'$fieldName': $fieldName"; + } else if (fieldData == "double") { + return "'$fieldName': $fieldName"; + } else if (fieldData == "bool") { + return "'$fieldName': $fieldName"; + } else { + return "'$fieldName': $fieldData"; + } + } else { + return "'$fieldName': '$fieldData'"; + } + } + } + return null; + } + + // Class _buildProductManifest(String flavor, Map map) { + // final methodList = []; + // final fieldList = []; + // for (var product in map.entries) { + // final manifestName = product.key; + // final manifestData = product.value; + // + // final sku = (manifestData.remove("sku") ?? '') as String; + // if (sku.isNotEmpty != true) { + // final todo = 'Please define `sku` field to [$manifestName] in guru_spec.yaml.'; + // throw InvalidGenerationSourceError( + // 'Generator cannot target `$manifestName`.\n[TODO] $todo', + // todo: todo, + // ); + // } + // + // final category = (manifestData.remove("category") ?? '') as String; + // if (category.isNotEmpty != true) { + // final todo = 'Please define `category` field to [$manifestName] in guru_spec.yaml.'; + // throw InvalidGenerationSourceError( + // 'Generator cannot target `$manifestName`.\n[TODO] $todo', + // todo: todo, + // ); + // } + // + // fieldList.add(Field((field) => field + // ..name = "${capitalize(manifestName, useCamelCase: true)}Matcher" + // ..static = true + // ..assignment = Code("RegExp($sku)") + // ..modifier = FieldModifier.final$)); + // + // final List codeBlocks = []; + // codeBlocks.add(const Code("final details =
[];")); + // for (var data in manifestData.entries) { + // if (detailsExp.hasMatch(data.key)) { + // final details = data.value as Map; + // final type = details.remove("type"); + // final amount = details.remove("amount"); + // if (type == null || amount == null) { + // const todo = 'Please define `type` or `amount` field to guru_spec.yaml.'; + // throw InvalidGenerationSourceError( + // 'Generator cannot target `$manifestName`.\n[TODO] $todo', + // todo: todo, + // ); + // } + // final ignoreSales = details.remove("ignore_sales"); + // if (ignoreSales != null && ignoreSales is! bool) { + // final todo = + // '[$manifestName] ignoreSales:$ignoreSales ${ignoreSales.runtimeType} field is not `bool`'; + // throw InvalidGenerationSourceError( + // 'Generator cannot target `$manifestName`.\n[TODO] $todo', + // todo: todo, + // ); + // } + // + // if (ignoreSales == true) { + // codeBlocks.add(Code("details.add(Details.define('$type', $amount)")); + // } else { + // codeBlocks.add(Code( + // "details.add(Details.define('$type', intent.sales ? max($amount, (intent.rate * $amount).toInt()) : $amount)")); + // } + // for (var detail in details.entries) { + // final fieldData = detail.value; + // if (fieldData is int) { + // codeBlocks.add(Code("..setInt('${detail.key}', $fieldData)")); + // } else if (fieldData is double) { + // codeBlocks.add(Code("..setDouble('${detail.key}', $fieldData})")); + // } else if (fieldData is bool) { + // codeBlocks.add(Code("..setBool('${detail.key}', ${fieldData ? "true" : "false"})")); + // } else { + // final matches = typeParamsExp.allMatches(fieldData); + // if (matches.isNotEmpty) { + // final fieldName = matches.first.group(1); + // final fieldType = matches.first.group(2)?.toLowerCase(); + // if (fieldType == 'int') { + // codeBlocks.add(Code("..setInt('${detail.key}', $fieldName)")); + // } else if (fieldData == "double") { + // codeBlocks.add(Code("..setDouble('${detail.key}', $fieldName})")); + // } else if (fieldData == "bool") { + // codeBlocks.add(Code("..setBool('${detail.key}', $fieldName)")); + // } else { + // codeBlocks.add(Code("..setString('${detail.key}', $fieldData)")); + // } + // } else { + // codeBlocks.add(Code("..setString('${detail.key}', '$fieldData')")); + // } + // } + // } + // codeBlocks.add(const Code(");")); + // } + // } + // codeBlocks.add(Code("return Manifest('$category', details: details);")); + // + // methodList.add(Method((method) => method + // ..name = "build${capitalize(manifestName)}Manifest" + // ..returns = refer('Manifest') + // ..body = Block.of(codeBlocks) + // ..requiredParameters.add(Parameter((p) => p + // ..type = refer("TransactionIntent") + // ..name = 'intent')) + // ..static = true)); + // } + // return Class((clazz) { + // clazz + // ..name = '_${capitalize(flavor)}ProductManifest' + // ..fields.addAll(fieldList) + // ..methods.addAll(methodList); + // // ..methods.addAll([_buildCreator(element, blocks)]) + // // ..methods.addAll(designSpec.nestedSpec ? [] : [_buildGetter(element)]) + // // ..extend = refer(className); + // }); + // return Class((clazz) => clazz..name = "Hello"); + // } + + static final RegExp detailsExp = RegExp(r"details\d*$"); + static final RegExp paramsExp = RegExp(r"\{([^}]+)\}"); + static final RegExp typeParamsExp = RegExp(r"\{([^}]+)\}:(string|int|double|bool)$"); + static final RegExp fillParamsExp = RegExp(r"\{(\d)\}"); + + // List _buildProductField(Map map, String flavor) { + // final List fieldList = []; + // final List methodList = []; + // // String? noAds; + // final List oneOffChargeIapIds = []; + // final List subscriptionsIapIds = []; + // final List rewardIds = []; + // final List igcIds = []; + // final Map> capabilitiesIds = {}; + // for (var fieldEntry in map.entries) { + // String fieldName = camelCase(fieldEntry.key); + // final fieldData = fieldEntry.value; + // if (fieldName != '' && fieldData != null) { + // final attr = fieldData['attr'] ?? ''; + // final capabilities = (fieldData["capabilities"] ?? '') as String; + // final android = fieldData['android'] ?? ''; + // final ios = fieldData['ios'] ?? ''; + // final sku = (fieldData['sku'] ?? '') as String; + // final methods = ((fieldData['method'] ?? '') as String).split(","); + // for (var method in methods) { + // if (method == 'iap') { + // if (attr == 'subscriptions') { + // subscriptionsIapIds.add(fieldName); + // } else { + // oneOffChargeIapIds.add(fieldName); + // } + // } else if (method == 'reward') { + // rewardIds.add(fieldName); + // } else if (method == 'igc') { + // igcIds.add(fieldName); + // } + // } + // if (capabilities != '') { + // final segments = capabilities.split('|'); + // final capabilitiesList = + // segments.where((segment) => productIdsCapabilities.contains(segment)).toList(); + // for (var capability in capabilitiesList) { + // capabilitiesIds[capability] = (capabilitiesIds[capability] ??= {}) + // ..add(fieldName); + // } + // } + // + // if (paramsExp.hasMatch(sku)) { + // final List params = []; + // final str = sku.replaceAllMapped(paramsExp, (match) { + // final param = match.group(1) ?? ""; + // params.add(Parameter((p) => p + // ..type = refer("String") + // ..name = param)); + // return "\${${match.group(1)}}"; + // }); + // + // methodList.add(Method((method) => method + // ..name = fieldName + // ..returns = refer('ProductId') + // ..lambda = true + // ..optionalParameters.addAll(params) + // ..body = Code(str) + // ..static = true)); + // } else { + // fieldList.add(Field((field) => field + // ..name = fieldName + // ..static = true + // ..assignment = sku.isNotEmpty + // ? Code("ProductId.fromSku(sku: '$sku', attr: TransactionAttributes.$attr)") + // : Code( + // "ProductId(android: '$android', ios: '$ios', attr: TransactionAttributes.$attr)") + // ..modifier = FieldModifier.constant)); + // } + // + // iapIdsCheckMap[fieldName] = (iapIdsCheckMap[fieldName] ??= {})..add(flavor); + // } + // } + // + // List buildIapProfileMethods() { + // final List methodList = []; + // methodList.add(Method((method) => method + // ..name = 'ids' + // ..type = MethodType.getter + // ..returns = refer('List') + // ..lambda = true + // ..body = const Code("[...oneOffChargeIapIds, ...subscriptionsIapIds]") + // ..static = true)); + // return methodList; + // } + // + // for (var entry in capabilitiesIds.entries) { + // final fieldName = buildCapIds(entry.key); + // if (entry.value.isNotEmpty) { + // fieldList.add(Field((field) => field + // ..name = fieldName + // ..static = true + // ..assignment = Code("[${entry.value.join(',')}]") + // ..modifier = FieldModifier.constant)); + // iapCapIdsCheckMap[fieldName] = (iapCapIdsCheckMap[fieldName] ??= {})..add(flavor); + // } + // } + // + // fieldList.add(Field((field) => field + // ..name = "oneOffChargeIapIds" + // ..static = true + // ..assignment = Code("[${oneOffChargeIapIds.join(',')}]") + // ..modifier = FieldModifier.constant)); + // + // fieldList.add(Field((field) => field + // ..name = "subscriptionsIapIds" + // ..static = true + // ..assignment = Code("[${subscriptionsIapIds.join(',')}]") + // ..modifier = FieldModifier.constant)); + // + // fieldList.add(Field((field) => field + // ..name = "rewardIds" + // ..static = true + // ..assignment = Code("[${rewardIds.join(',')}]") + // ..modifier = FieldModifier.constant)); + // + // fieldList.add(Field((field) => field + // ..name = "igcIds" + // ..static = true + // ..assignment = Code("[${igcIds.join(',')}]") + // ..modifier = FieldModifier.constant)); + // return fieldList; + // } + + // Class buildIapProductIds(Map map, {String flavor = ""}) { + // return Class((clazz) { + // clazz + // ..name = '_${capitalize(flavor)}IapProductIds' + // ..fields.addAll(_buildIapProductField(map, flavor)) + // ..methods.addAll(buildIapProfileMethods()); + // // ..methods.addAll([_buildCreator(element, blocks)]) + // // ..methods.addAll(designSpec.nestedSpec ? [] : [_buildGetter(element)]) + // // ..extend = refer(className); + // }); + // } + + Class buildRemoteConfigConstantsClass(Map map, {String flavor = ""}) { + final List fieldList = []; + final codeBlocks = [const Code("{")]; + for (var fieldEntry in map.entries) { + final fieldName = camelCase(fieldEntry.key); + final fieldData = fieldEntry.value; + if (fieldName != '' && fieldData != null) { + fieldList.add(Field((field) => field + ..name = fieldName + ..static = true + ..assignment = Code("'${fieldEntry.key}'") + ..modifier = FieldModifier.constant)); + codeBlocks.add(Code("$fieldName: '$fieldData',")); + remoteConfigKeys[fieldEntry.key] = fieldName; + } + } + codeBlocks.add(const Code("}")); + + fieldList.add(Field((field) => field + ..name = '_defaultConfigs' + ..static = true + ..type = refer("Map") + ..assignment = Block.of(codeBlocks) + ..modifier = FieldModifier.constant)); + + final List methodList = []; + methodList.add(Method((method) => method + ..name = 'getDefaultConfigString' + ..returns = refer('String') + ..lambda = true + ..requiredParameters.addAll([ + Parameter((p) => p + ..type = refer("String") + ..name = "key") + ]) + ..body = const Code("_defaultConfigs[key]") + ..static = true)); + + return Class((clazz) { + clazz + ..name = '_${capitalize(flavor)}RemoteConfigConstants' + ..fields.addAll(fieldList) + ..methods.addAll(methodList); + }); + } + +// => Field((field) => field + +} diff --git a/guru_app/packages/guru_spec/lib/src/hash.dart b/guru_app/packages/guru_spec/lib/src/hash.dart new file mode 100644 index 0000000..252e689 --- /dev/null +++ b/guru_app/packages/guru_spec/lib/src/hash.dart @@ -0,0 +1,40 @@ +library hash; + +/// Created by @Haoyi on 2021/7/7 + +/// Generates a hash code for multiple [objects]. +int hashObjects(Iterable objects) => _finish(objects.fold(0, (h, i) => _combine(h, i.hashCode))); + +/// Generates a hash code for two objects. +int hash2(a, b) => _finish(_combine(_combine(0, a.hashCode), b.hashCode)); + +/// Generates a hash code for three objects. +int hash3(a, b, c) => _finish(_combine(_combine(_combine(0, a.hashCode), b.hashCode), c.hashCode)); + +/// Generates a hash code for four objects. +int hash4(a, b, c, d) => _finish( + _combine(_combine(_combine(_combine(0, a.hashCode), b.hashCode), c.hashCode), d.hashCode)); + +int hashIntList(List list) => _finish(list.fold(0x9E370001, (h, i) => _combine(h, i))); + +// Jenkins hash functions + +int _combine(int hash, int value) { + hash = 0x1fffffff & (hash + value); + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); +} + +int _finish(int hash) { + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); +} + +int hashCombine(int hash, int value) { + return _combine(hash, value); +} + +int hashFinish(int hash) { + return _finish(hash); +} diff --git a/guru_app/packages/guru_spec/lib/src/intelligent_keys_generator.dart b/guru_app/packages/guru_spec/lib/src/intelligent_keys_generator.dart new file mode 100644 index 0000000..a48c93d --- /dev/null +++ b/guru_app/packages/guru_spec/lib/src/intelligent_keys_generator.dart @@ -0,0 +1,35 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:analyzer/dart/element/element.dart'; +import 'package:build/build.dart'; +import 'package:code_builder/code_builder.dart'; +import 'package:dart_style/dart_style.dart'; +import 'package:glob/glob.dart'; +import 'package:guru_spec/guru_spec.dart'; +import 'package:guru_spec/src/tuple.dart'; +import 'package:source_gen/source_gen.dart'; +import 'package:yaml/yaml.dart'; +import 'package:glob/list_local_fs.dart'; + +/// Created by Haoyi on 2023/3/1 + +Builder intelligentKeysGeneratorFactoryBuilder(BuilderOptions options) => + SharedPartBuilder([IntelligentKeysGenerator()], "intelligent_keys"); + +class IntelligentKeysGenerator extends GeneratorForAnnotation { + @override + generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) { + if (element is! ClassElement) { + final name = element.displayName; + final todo = 'Remove the [GuruSpecCreator] annotation from `$name`.'; + throw InvalidGenerationSourceError( + 'Generator cannot target `$name`.\n[TODO] $todo', + todo: 'Remove the [GuruSpecCreator] annotation from `$name`.', + ); + } + log.info("IntelligentKeysGenerator fields:${element.fields.map((e) => e.name)}"); + log.info("doc:${element.displayName} "); + + } +} diff --git a/guru_app/packages/guru_spec/lib/src/tuple.dart b/guru_app/packages/guru_spec/lib/src/tuple.dart new file mode 100644 index 0000000..5dc05e0 --- /dev/null +++ b/guru_app/packages/guru_spec/lib/src/tuple.dart @@ -0,0 +1,413 @@ +import 'hash.dart'; + +/// Created by Haoyi on 2020/12/18 + +/// Represents a 2-tuple, or pair. +class Tuple2 { + /// Returns the first item of the tuple + final T1 item1; + + /// Returns the second item of the tuple + final T2 item2; + + /// Creates a new tuple value with the specified items. + const Tuple2(this.item1, this.item2); + + /// Create a new tuple value with the specified list [items]. + factory Tuple2.fromList(List items) { + if (items.length != 2) { + throw ArgumentError('items must have length 2'); + } + + return Tuple2(items[0] as T1, items[1] as T2); + } + + /// Returns a tuple with the first item set to the specified value. + Tuple2 withItem1(T1 v) => Tuple2(v, item2); + + /// Returns a tuple with the second item set to the specified value. + Tuple2 withItem2(T2 v) => Tuple2(item1, v); + + /// Creates a [List] containing the items of this [Tuple2]. + /// + /// The elements are in item order. The list is variable-length + /// if [growable] is true. + List toList({bool growable = false}) => List.from([item1, item2], growable: growable); + + @override + String toString() => '[$item1, $item2]'; + + @override + bool operator ==(Object other) => other is Tuple2 && other.item1 == item1 && other.item2 == item2; + + @override + int get hashCode => hash2(item1.hashCode, item2.hashCode); +} + +/// Represents a 3-tuple, or triple. +class Tuple3 { + /// Returns the first item of the tuple + final T1 item1; + + /// Returns the second item of the tuple + final T2 item2; + + /// Returns the third item of the tuple + final T3 item3; + + /// Creates a new tuple value with the specified items. + const Tuple3(this.item1, this.item2, this.item3); + + /// Create a new tuple value with the specified list [items]. + factory Tuple3.fromList(List items) { + if (items.length != 3) { + throw ArgumentError('items must have length 3'); + } + + return Tuple3(items[0] as T1, items[1] as T2, items[2] as T3); + } + + /// Returns a tuple with the first item set to the specified value. + Tuple3 withItem1(T1 v) => Tuple3(v, item2, item3); + + /// Returns a tuple with the second item set to the specified value. + Tuple3 withItem2(T2 v) => Tuple3(item1, v, item3); + + /// Returns a tuple with the third item set to the specified value. + Tuple3 withItem3(T3 v) => Tuple3(item1, item2, v); + + /// Creates a [List] containing the items of this [Tuple3]. + /// + /// The elements are in item order. The list is variable-length + /// if [growable] is true. + List toList({bool growable = false}) => List.from([item1, item2, item3], growable: growable); + + @override + String toString() => '[$item1, $item2, $item3]'; + + @override + bool operator ==(Object other) => + other is Tuple3 && other.item1 == item1 && other.item2 == item2 && other.item3 == item3; + + @override + int get hashCode => hash3(item1.hashCode, item2.hashCode, item3.hashCode); +} + +/// Represents a 4-tuple, or quadruple. +class Tuple4 { + /// Returns the first item of the tuple + final T1 item1; + + /// Returns the second item of the tuple + final T2 item2; + + /// Returns the third item of the tuple + final T3 item3; + + /// Returns the fourth item of the tuple + final T4 item4; + + /// Creates a new tuple value with the specified items. + const Tuple4(this.item1, this.item2, this.item3, this.item4); + + /// Create a new tuple value with the specified list [items]. + factory Tuple4.fromList(List items) { + if (items.length != 4) { + throw ArgumentError('items must have length 4'); + } + + return Tuple4(items[0] as T1, items[1] as T2, items[2] as T3, items[3] as T4); + } + + /// Returns a tuple with the first item set to the specified value. + Tuple4 withItem1(T1 v) => Tuple4(v, item2, item3, item4); + + /// Returns a tuple with the second item set to the specified value. + Tuple4 withItem2(T2 v) => Tuple4(item1, v, item3, item4); + + /// Returns a tuple with the third item set to the specified value. + Tuple4 withItem3(T3 v) => Tuple4(item1, item2, v, item4); + + /// Returns a tuple with the fourth item set to the specified value. + Tuple4 withItem4(T4 v) => Tuple4(item1, item2, item3, v); + + /// Creates a [List] containing the items of this [Tuple4]. + /// + /// The elements are in item order. The list is variable-length + /// if [growable] is true. + List toList({bool growable = false}) => + List.from([item1, item2, item3, item4], growable: growable); + + @override + String toString() => '[$item1, $item2, $item3, $item4]'; + + @override + bool operator ==(Object other) => + other is Tuple4 && + other.item1 == item1 && + other.item2 == item2 && + other.item3 == item3 && + other.item4 == item4; + + @override + int get hashCode => hash4(item1.hashCode, item2.hashCode, item3.hashCode, item4.hashCode); +} + +/// Represents a 5-tuple, or quintuple. +class Tuple5 { + /// Returns the first item of the tuple + final T1 item1; + + /// Returns the second item of the tuple + final T2 item2; + + /// Returns the third item of the tuple + final T3 item3; + + /// Returns the fourth item of the tuple + final T4 item4; + + /// Returns the fifth item of the tuple + final T5 item5; + + /// Creates a new tuple value with the specified items. + const Tuple5(this.item1, this.item2, this.item3, this.item4, this.item5); + + /// Create a new tuple value with the specified list [items]. + factory Tuple5.fromList(List items) { + if (items.length != 5) { + throw ArgumentError('items must have length 5'); + } + + return Tuple5( + items[0] as T1, items[1] as T2, items[2] as T3, items[3] as T4, items[4] as T5); + } + + /// Returns a tuple with the first item set to the specified value. + Tuple5 withItem1(T1 v) => + Tuple5(v, item2, item3, item4, item5); + + /// Returns a tuple with the second item set to the specified value. + Tuple5 withItem2(T2 v) => + Tuple5(item1, v, item3, item4, item5); + + /// Returns a tuple with the third item set to the specified value. + Tuple5 withItem3(T3 v) => + Tuple5(item1, item2, v, item4, item5); + + /// Returns a tuple with the fourth item set to the specified value. + Tuple5 withItem4(T4 v) => + Tuple5(item1, item2, item3, v, item5); + + /// Returns a tuple with the fifth item set to the specified value. + Tuple5 withItem5(T5 v) => + Tuple5(item1, item2, item3, item4, v); + + /// Creates a [List] containing the items of this [Tuple5]. + /// + /// The elements are in item order. The list is variable-length + /// if [growable] is true. + List toList({bool growable = false}) => + List.from([item1, item2, item3, item4, item5], growable: growable); + + @override + String toString() => '[$item1, $item2, $item3, $item4, $item5]'; + + @override + bool operator ==(Object other) => + other is Tuple5 && + other.item1 == item1 && + other.item2 == item2 && + other.item3 == item3 && + other.item4 == item4 && + other.item5 == item5; + + @override + int get hashCode => + hashObjects([item1.hashCode, item2.hashCode, item3.hashCode, item4.hashCode, item5.hashCode]); +} + +/// Represents a 6-tuple, or sextuple. +class Tuple6 { + /// Returns the first item of the tuple + final T1 item1; + + /// Returns the second item of the tuple + final T2 item2; + + /// Returns the third item of the tuple + final T3 item3; + + /// Returns the fourth item of the tuple + final T4 item4; + + /// Returns the fifth item of the tuple + final T5 item5; + + /// Returns the sixth item of the tuple + final T6 item6; + + /// Creates a new tuple value with the specified items. + const Tuple6(this.item1, this.item2, this.item3, this.item4, this.item5, this.item6); + + /// Create a new tuple value with the specified list [items]. + factory Tuple6.fromList(List items) { + if (items.length != 6) { + throw ArgumentError('items must have length 6'); + } + + return Tuple6(items[0] as T1, items[1] as T2, items[2] as T3, + items[3] as T4, items[4] as T5, items[5] as T6); + } + + /// Returns a tuple with the first item set to the specified value. + Tuple6 withItem1(T1 v) => + Tuple6(v, item2, item3, item4, item5, item6); + + /// Returns a tuple with the second item set to the specified value. + Tuple6 withItem2(T2 v) => + Tuple6(item1, v, item3, item4, item5, item6); + + /// Returns a tuple with the third item set to the specified value. + Tuple6 withItem3(T3 v) => + Tuple6(item1, item2, v, item4, item5, item6); + + /// Returns a tuple with the fourth item set to the specified value. + Tuple6 withItem4(T4 v) => + Tuple6(item1, item2, item3, v, item5, item6); + + /// Returns a tuple with the fifth item set to the specified value. + Tuple6 withItem5(T5 v) => + Tuple6(item1, item2, item3, item4, v, item6); + + /// Returns a tuple with the sixth item set to the specified value. + Tuple6 withItem6(T6 v) => + Tuple6(item1, item2, item3, item4, item5, v); + + /// Creates a [List] containing the items of this [Tuple5]. + /// + /// The elements are in item order. The list is variable-length + /// if [growable] is true. + List toList({bool growable = false}) => + List.from([item1, item2, item3, item4, item5, item6], growable: growable); + + @override + String toString() => '[$item1, $item2, $item3, $item4, $item5, $item6]'; + + @override + bool operator ==(Object other) => + other is Tuple6 && + other.item1 == item1 && + other.item2 == item2 && + other.item3 == item3 && + other.item4 == item4 && + other.item5 == item5 && + other.item6 == item6; + + @override + int get hashCode => hashObjects([ + item1.hashCode, + item2.hashCode, + item3.hashCode, + item4.hashCode, + item5.hashCode, + item6.hashCode + ]); +} + +/// Represents a 7-tuple, or septuple. +class Tuple7 { + /// Returns the first item of the tuple + final T1 item1; + + /// Returns the second item of the tuple + final T2 item2; + + /// Returns the third item of the tuple + final T3 item3; + + /// Returns the fourth item of the tuple + final T4 item4; + + /// Returns the fifth item of the tuple + final T5 item5; + + /// Returns the sixth item of the tuple + final T6 item6; + + /// Returns the seventh item of the tuple + final T7 item7; + + /// Creates a new tuple value with the specified items. + const Tuple7(this.item1, this.item2, this.item3, this.item4, this.item5, this.item6, this.item7); + + /// Create a new tuple value with the specified list [items]. + factory Tuple7.fromList(List items) { + if (items.length != 7) { + throw ArgumentError('items must have length 7'); + } + + return Tuple7(items[0] as T1, items[1] as T2, items[2] as T3, + items[3] as T4, items[4] as T5, items[5] as T6, items[6] as T7); + } + + /// Returns a tuple with the first item set to the specified value. + Tuple7 withItem1(T1 v) => + Tuple7(v, item2, item3, item4, item5, item6, item7); + + /// Returns a tuple with the second item set to the specified value. + Tuple7 withItem2(T2 v) => + Tuple7(item1, v, item3, item4, item5, item6, item7); + + /// Returns a tuple with the third item set to the specified value. + Tuple7 withItem3(T3 v) => + Tuple7(item1, item2, v, item4, item5, item6, item7); + + /// Returns a tuple with the fourth item set to the specified value. + Tuple7 withItem4(T4 v) => + Tuple7(item1, item2, item3, v, item5, item6, item7); + + /// Returns a tuple with the fifth item set to the specified value. + Tuple7 withItem5(T5 v) => + Tuple7(item1, item2, item3, item4, v, item6, item7); + + /// Returns a tuple with the sixth item set to the specified value. + Tuple7 withItem6(T6 v) => + Tuple7(item1, item2, item3, item4, item5, v, item7); + + /// Returns a tuple with the seventh item set to the specified value. + Tuple7 withItem7(T7 v) => + Tuple7(item1, item2, item3, item4, item5, item6, v); + + /// Creates a [List] containing the items of this [Tuple5]. + /// + /// The elements are in item order. The list is variable-length + /// if [growable] is true. + List toList({bool growable = false}) => + List.from([item1, item2, item3, item4, item5, item6, item7], growable: growable); + + @override + String toString() => '[$item1, $item2, $item3, $item4, $item5, $item6, $item7]'; + + @override + bool operator ==(Object other) => + other is Tuple7 && + other.item1 == item1 && + other.item2 == item2 && + other.item3 == item3 && + other.item4 == item4 && + other.item5 == item5 && + other.item6 == item6 && + other.item7 == item7; + + @override + int get hashCode => hashObjects([ + item1.hashCode, + item2.hashCode, + item3.hashCode, + item4.hashCode, + item5.hashCode, + item6.hashCode, + item7.hashCode + ]); +} diff --git a/guru_app/packages/guru_spec/pubspec.lock b/guru_app/packages/guru_spec/pubspec.lock new file mode 100644 index 0000000..2b2738d --- /dev/null +++ b/guru_app/packages/guru_spec/pubspec.lock @@ -0,0 +1,441 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.flutter-io.cn" + source: hosted + version: "47.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.7.0" + args: + dependency: transitive + description: + name: args + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.9.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + build: + dependency: "direct main" + description: + name: build + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.2" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.9" + build_runner: + dependency: "direct main" + description: + name: build_runner + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.3" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.2.3" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.4.1" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.4.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.16.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.3" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + file: + dependency: transitive + description: + name: file + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.4" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.0" + glob: + dependency: "direct main" + description: + name: glob + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.1" + io: + dependency: transitive + description: + name: io + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.4" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.6.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.12" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.2" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: "direct main" + description: + name: source_gen + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.6" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.12" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + yaml: + dependency: "direct main" + description: + name: yaml + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.1" +sdks: + dart: ">=2.17.0 <3.0.0" + flutter: ">=2.10.5" diff --git a/guru_app/packages/guru_spec/pubspec.yaml b/guru_app/packages/guru_spec/pubspec.yaml new file mode 100644 index 0000000..355a39d --- /dev/null +++ b/guru_app/packages/guru_spec/pubspec.yaml @@ -0,0 +1,60 @@ +name: guru_spec +description: A new Flutter project. +version: 1.1.0 +homepage: + +environment: + sdk: ">=2.16.2 <4.0.0" + flutter: ">=2.10.5" + +dependencies: + flutter: + sdk: flutter + + build: 2.4.1 + build_runner: 2.4.7 + source_gen: 1.5.0 + yaml: 3.1.2 + glob: 2.1.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^1.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + generate: true + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/guru_app/packages/guru_spec/test/guru_spec_test.dart b/guru_app/packages/guru_spec/test/guru_spec_test.dart new file mode 100644 index 0000000..ff940cf --- /dev/null +++ b/guru_app/packages/guru_spec/test/guru_spec_test.dart @@ -0,0 +1,5 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:guru_spec/guru_spec.dart'; + +void main() {} diff --git a/guru_app/packages/guru_utils/.gitignore b/guru_app/packages/guru_utils/.gitignore new file mode 100644 index 0000000..9be145f --- /dev/null +++ b/guru_app/packages/guru_utils/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/guru_app/packages/guru_utils/CHANGELOG.md b/guru_app/packages/guru_utils/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/guru_app/packages/guru_utils/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/guru_app/packages/guru_utils/LICENSE b/guru_app/packages/guru_utils/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/guru_app/packages/guru_utils/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/guru_app/packages/guru_utils/README.md b/guru_app/packages/guru_utils/README.md new file mode 100644 index 0000000..8b55e73 --- /dev/null +++ b/guru_app/packages/guru_utils/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/guru_app/packages/guru_utils/analysis_options.yaml b/guru_app/packages/guru_utils/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/guru_app/packages/guru_utils/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/guru_app/packages/guru_utils/lib/ads/ads.dart b/guru_app/packages/guru_utils/lib/ads/ads.dart new file mode 100644 index 0000000..7deb977 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/ads/ads.dart @@ -0,0 +1,6 @@ +/// Created by Haoyi on 2023/2/9 + +export 'ads_delegate.dart'; +export 'ads_manager_delegate.dart'; +export 'handler/ads_handler.dart'; +export 'data/ads_model.dart'; \ No newline at end of file diff --git a/guru_app/packages/guru_utils/lib/ads/ads_delegate.dart b/guru_app/packages/guru_utils/lib/ads/ads_delegate.dart new file mode 100644 index 0000000..55dfc00 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/ads/ads_delegate.dart @@ -0,0 +1,34 @@ +import 'package:guru_utils/ads/handler/ads_handler.dart'; + +import 'data/ads_model.dart'; + +/// Created by Haoyi on 5/6/21 +/// + +mixin AdsDelegate on AdsLifecycleOwner { + Stream get observableLoaded; + + bool get loaded; + + Future load() async { + return AdCause.ignore; + } + + Future hide() async {} + + Future show({required String scene, bool ignoreCheck = false}) async { + return AdCause.ignore; + } + + bool needReset() { + return false; + } + + Future reset() async { + return false; + } + + Future getState(); +} + +mixin AuditAdsDelegate on AdsDelegate {} diff --git a/guru_app/packages/guru_utils/lib/ads/ads_manager_delegate.dart b/guru_app/packages/guru_utils/lib/ads/ads_manager_delegate.dart new file mode 100644 index 0000000..fc88078 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/ads/ads_manager_delegate.dart @@ -0,0 +1,38 @@ +import 'package:flutter/cupertino.dart'; +import 'package:guru_utils/ads/ads_delegate.dart'; +import 'package:guru_utils/ads/data/ads_model.dart'; +import 'package:guru_utils/ads/handler/ads_handler.dart'; +import 'package:guru_utils/extensions/extensions.dart'; + +/// Created by Haoyi on 2023/2/9 + +abstract class AdsManagerDelegate { + static late AdsManagerDelegate _delegate; + + static AdsManagerDelegate get instance => _delegate; + + @mustCallSuper + AdsManagerDelegate() { + _delegate = this; + } + + Stream get observableInitialized; + + Stream get observableNoAds; + + bool get isPurchasedNoAd; + + Future getInterstitialAds(); + + Future getRewardsAds(); + + Future createBannerAds({String? scene, AdsLifecycleObserver? observer}); + + Future validateBanner(String? scene, {AdsValidator? validator}); + + Future validateInterstitial(String? scene, {AdsValidator? validator}); + + Future validateRewards(String? scene, {AdsValidator? validator}); + + dynamic getConfig(String type); +} diff --git a/guru_app/packages/guru_utils/lib/ads/data/ads_model.dart b/guru_app/packages/guru_utils/lib/ads/data/ads_model.dart new file mode 100644 index 0000000..2e80c23 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/ads/data/ads_model.dart @@ -0,0 +1,381 @@ +import 'dart:io'; + +import 'package:guru_utils/ads/ads_delegate.dart'; +import 'package:guru_utils/converts/converts.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_utils/hash/hash.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'ads_model.g.dart'; + +/// Created by Haoyi on 5/10/21 + +enum AdType { banner, interstitial, rewarded, none } + +enum AdCause { + success, + displayFailed, + alreadyLoaded, + loadFailed, + requestFailed, + disabledScene, + adsDisabled, + invalidRequest, + tooFrequent, + canceled, + internalError, + rewardedFailed, + unknownError, + lifecycleError, + showTimeout, + conflict, + noAds, + ignore, + noResponse +} + +enum AdState { created, loading, failed, loaded } + +String toAdStatusName(AdState status) { + switch (status) { + case AdState.created: + return "CREATED"; + case AdState.loading: + return "LOADING"; + case AdState.loaded: + return "LOADED"; + default: + return "FAILED"; + } +} + +typedef AdsValidator = Future Function(); + +class AdsResult { + final AdType type; + final AdCause cause; + final dynamic internalCause; + + factory AdsResult.success(AdType type) => AdsResult.build(type, AdCause.success); + + @override + String toString() { + return 'AdsResult{type: $type, cause: $cause, internalCause: $internalCause}'; + } + + AdsResult.build(this.type, this.cause, {this.internalCause}); +} + +class AdNetwork { + static const Mopub = "Mopub"; + static const AdManager = "AdManager"; + static const Admob = "Admob"; + static const FAN = "FAN"; + static const Unity = "Unity"; + static const Fyber = "Fyber"; + static const IronSource = "IronSource"; + static const Verizon = "Verizon"; + static const Amazon = "Amazon"; + static const Pangle = "Pangle"; + static const Applovin = "Applovin"; + static const Ogury = "Ogury"; + static const HyBid = "HyBid"; + static const HyperMX = "HyperMX"; +} + +class AdFormat { + static const Banner = "Banner"; + static const Interstitial = "Interstitial"; + static const Rewarded = "Rewarded"; +} + +class AdAppId { + final String android; + final String ios; + + static const invalid = AdAppId(android: "", ios: ""); + + const AdAppId({required this.android, required this.ios}); + + factory AdAppId.fromJson(Map json) => + AdAppId(android: json['android'] as String, ios: json['ios'] as String); + + Map toJson() => {"android": android, "ios": ios}; + + @override + String toString() { + return 'AdAppId{$id}'; + } + + String get id { + if (Platform.isAndroid) { + return android; + } else if (Platform.isIOS) { + return ios; + } + return android; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AdUnitId && + runtimeType == other.runtimeType && + android == other.android && + ios == other.ios; + + @override + int get hashCode => android.hashCode ^ ios.hashCode; +} + +class AdUnitId { + final String android; + final String ios; + + static const invalid = AdUnitId(android: "", ios: ""); + + const AdUnitId({required this.android, required this.ios}); + + factory AdUnitId.fromJson(Map json) => + AdUnitId(android: json['a'] as String, ios: json['i'] as String); + + Map toJson() => {"a": android, "i": ios}; + + @override + String toString() { + return 'AdUnitId{$id}'; + } + + String get id { + if (Platform.isAndroid) { + return android; + } else if (Platform.isIOS) { + return ios; + } + return android; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AdUnitId && + runtimeType == other.runtimeType && + android == other.android && + ios == other.ios; + + @override + int get hashCode => android.hashCode ^ ios.hashCode; +} + +class AdSlotId { + final String android; + final String ios; + + const AdSlotId({required this.android, required this.ios}); + + factory AdSlotId.fromJson(Map json) => + AdSlotId(android: json['a'] as String, ios: json['i'] as String); + + Map toJson() => {"a": android, "i": ios}; + + @override + String toString() { + return 'AdSlotId{$id}'; + } + + String get id { + if (Platform.isAndroid) { + return android; + } else if (Platform.isIOS) { + return ios; + } + return android; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AdUnitId && + runtimeType == other.runtimeType && + android == other.android && + ios == other.ios; + + @override + int get hashCode => hash2(android.hashCode, ios.hashCode); +} + +@JsonSerializable() +class AdId { + @JsonKey(name: "ad_unit_id") + final AdUnitId adUnitId; + + @JsonKey(name: "amz_slot_id") + final AdSlotId? amazonAdSlotId; + + AdId(this.adUnitId, {this.amazonAdSlotId}); + + @override + String toString() { + return 'AdId{adUnitId: $adUnitId, amazonAdSlotId: $amazonAdSlotId}'; + } + + factory AdId.fromJson(Map json) => _$AdIdFromJson(json); + + Map toJson() => _$AdIdToJson(this); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AdId && + runtimeType == other.runtimeType && + adUnitId == other.adUnitId && + amazonAdSlotId == other.amazonAdSlotId; + + @override + int get hashCode => hash2(adUnitId.hashCode, amazonAdSlotId.hashCode); +} + +@JsonSerializable() +class AdsProfile { + @JsonKey(name: "banner_ad_unit_id") + final AdUnitId bannerId; + + @JsonKey(name: "interstitial_ad_unit_id") + final AdUnitId interstitialId; + + @JsonKey(name: "rewards_ad_unit_id") + final AdUnitId rewardsId; + + @JsonKey(name: "amz_app_id") + final AdAppId? amazonAppId; + + @JsonKey(name: "banner_amz_slot_id") + final AdSlotId? amazonBannerSlotId; + + @JsonKey(name: "interstitial_amz_slot_id") + final AdSlotId? amazonInterstitialSlotId; + + @JsonKey(name: "rewarded_amz_slot_id") + final AdSlotId? amazonRewardedSlotId; + + @JsonKey(name: "pubmatic_app_store_url") + final String? pubmaticAppStoreUrl; + + @JsonKey(name: "strategy_interstitial_ids") + final List? strategyInterstitialIds; + + factory AdsProfile.fromJson(Map json) => _$AdsProfileFromJson(json); + + Map toJson() => _$AdsProfileToJson(this); + + AdsProfile( + {required this.bannerId, + required this.interstitialId, + required this.rewardsId, + this.amazonAppId, + this.amazonBannerSlotId, + this.amazonInterstitialSlotId, + this.amazonRewardedSlotId, + this.pubmaticAppStoreUrl, + this.strategyInterstitialIds}); + + AdsProfile copyWith( + {AdAppId? amazonAppId, + AdSlotId? amazonBannerSlotId, + AdSlotId? amazonInterstitialSlotId, + AdSlotId? amazonRewardedSlotId, + String? pubmaticAppStoreUrl, + List? strategyInterstitialIds}) { + return AdsProfile( + bannerId: bannerId, + interstitialId: interstitialId, + rewardsId: rewardsId, + amazonAppId: amazonAppId ?? this.amazonAppId, + amazonBannerSlotId: amazonBannerSlotId ?? this.amazonBannerSlotId, + amazonInterstitialSlotId: amazonInterstitialSlotId ?? this.amazonInterstitialSlotId, + amazonRewardedSlotId: amazonRewardedSlotId ?? this.amazonRewardedSlotId, + pubmaticAppStoreUrl: pubmaticAppStoreUrl ?? this.pubmaticAppStoreUrl, + strategyInterstitialIds: strategyInterstitialIds ?? this.strategyInterstitialIds); + } + + static final AdsProfile invalid = AdsProfile( + bannerId: AdUnitId.invalid, + interstitialId: AdUnitId.invalid, + rewardsId: AdUnitId.invalid, + amazonAppId: null, + amazonBannerSlotId: null, + amazonInterstitialSlotId: null, + amazonRewardedSlotId: null, + pubmaticAppStoreUrl: null, + strategyInterstitialIds: null); +} + +class AdsBundle { + final AdsDelegate ads; + final Map arguments; + + AdsBundle.create(this.ads, {this.arguments = const {}}); + + bool getBool(String key, {bool defValue = false}) { + return arguments[key] ?? defValue; + } + + int getInt(String key, {int defValue = 0}) { + return arguments[key] ?? defValue; + } + + double getDouble(String key, {double defValue = 0.0}) { + return arguments[key] ?? defValue; + } + + String getString(String key, {String defValue = ""}) { + return arguments[key] ?? defValue; + } + + T getValue(String key, {required T defValue}) { + try { + return (arguments[key] as T) ?? defValue; + } catch (_) { + return defValue; + } + } +} + +class AdsPropertyKeys { + static const elapsedTimeInMillisSinceStartLoadAds = "elapsedTimeInMillisSinceStartLoadAds"; + static const adsLoaded = "adsLoaded"; +} + +// @JsonSerializable() +// class AnalyticsConfig { +// @JsonKey(name: "cap", defaultValue: ["firebase", "facebook", "guru"]) +// @joinedStringConvert +// final List capabilities; +// +// @JsonKey(name: "init_delay_s", defaultValue: 10) +// final int delayedInSeconds; +// +// @JsonKey(name: "expired_d", defaultValue: 7) +// final int expiredInDays; +// +// AppEventCapabilities toAppEventCapabilities() { +// int capValue = 0; +// if (capabilities.contains("firebase")) { +// capValue |= AppEventCapabilities.firebase; +// } +// if (capabilities.contains("facebook")) { +// capValue |= AppEventCapabilities.facebook; +// } +// if (capabilities.contains("guru")) { +// capValue |= AppEventCapabilities.guru; +// } +// return AppEventCapabilities(capValue); +// } +// +// AnalyticsConfig(this.capabilities, this.delayedInSeconds, this.expiredInDays); +// +// factory AnalyticsConfig.fromJson(Map json) => _$AnalyticsConfigFromJson(json); +// +// Map toJson() => _$AnalyticsConfigToJson(this); +// } diff --git a/guru_app/packages/guru_utils/lib/ads/data/ads_model.g.dart b/guru_app/packages/guru_utils/lib/ads/data/ads_model.g.dart new file mode 100644 index 0000000..0c8278d --- /dev/null +++ b/guru_app/packages/guru_utils/lib/ads/data/ads_model.g.dart @@ -0,0 +1,61 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'ads_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AdId _$AdIdFromJson(Map json) => AdId( + AdUnitId.fromJson(json['ad_unit_id'] as Map), + amazonAdSlotId: json['amz_slot_id'] == null + ? null + : AdSlotId.fromJson(json['amz_slot_id'] as Map), + ); + +Map _$AdIdToJson(AdId instance) => { + 'ad_unit_id': instance.adUnitId, + 'amz_slot_id': instance.amazonAdSlotId, + }; + +AdsProfile _$AdsProfileFromJson(Map json) => AdsProfile( + bannerId: + AdUnitId.fromJson(json['banner_ad_unit_id'] as Map), + interstitialId: AdUnitId.fromJson( + json['interstitial_ad_unit_id'] as Map), + rewardsId: + AdUnitId.fromJson(json['rewards_ad_unit_id'] as Map), + amazonAppId: json['amz_app_id'] == null + ? null + : AdAppId.fromJson(json['amz_app_id'] as Map), + amazonBannerSlotId: json['banner_amz_slot_id'] == null + ? null + : AdSlotId.fromJson( + json['banner_amz_slot_id'] as Map), + amazonInterstitialSlotId: json['interstitial_amz_slot_id'] == null + ? null + : AdSlotId.fromJson( + json['interstitial_amz_slot_id'] as Map), + amazonRewardedSlotId: json['rewarded_amz_slot_id'] == null + ? null + : AdSlotId.fromJson( + json['rewarded_amz_slot_id'] as Map), + pubmaticAppStoreUrl: json['pubmatic_app_store_url'] as String?, + strategyInterstitialIds: + (json['strategy_interstitial_ids'] as List?) + ?.map((e) => AdId.fromJson(e as Map)) + .toList(), + ); + +Map _$AdsProfileToJson(AdsProfile instance) => + { + 'banner_ad_unit_id': instance.bannerId, + 'interstitial_ad_unit_id': instance.interstitialId, + 'rewards_ad_unit_id': instance.rewardsId, + 'amz_app_id': instance.amazonAppId, + 'banner_amz_slot_id': instance.amazonBannerSlotId, + 'interstitial_amz_slot_id': instance.amazonInterstitialSlotId, + 'rewarded_amz_slot_id': instance.amazonRewardedSlotId, + 'pubmatic_app_store_url': instance.pubmaticAppStoreUrl, + 'strategy_interstitial_ids': instance.strategyInterstitialIds, + }; diff --git a/guru_app/packages/guru_utils/lib/ads/handler/ads_handler.dart b/guru_app/packages/guru_utils/lib/ads/handler/ads_handler.dart new file mode 100644 index 0000000..229437d --- /dev/null +++ b/guru_app/packages/guru_utils/lib/ads/handler/ads_handler.dart @@ -0,0 +1,332 @@ +import 'package:flutter/foundation.dart'; +import 'package:guru_utils/ads/data/ads_model.dart'; +import 'package:guru_utils/log/log.dart'; + +/// Created by Haoyi on 5/7/21 + +abstract class AdsLifecycleObserver { + String get name => "Default"; + + void onRequestShow(AdsBundle adsBundle) {} + + void onRequestHide(AdsBundle adsBundle) {} + + void onRequestLoad(AdsBundle adsBundle) {} + + void onRequestDispose(AdsBundle adsBundle) {} + + void onRequestReset(AdsBundle adsBundle) {} + + void onAdLoaded(AdsBundle adsBundle) {} + + void onAdLoadFailed(AdsBundle adsBundle) {} + + void onAdDisplayFailed(AdsBundle adsBundle) {} + + void onAdDisplayed(AdsBundle adsBundle) {} + + void onAdClicked(AdsBundle adsBundle) {} + + void onAdHidden(AdsBundle adsBundle) {} + + // Only Reward Ads + void onAdRewarded(AdsBundle adsBundle) {} +} + +class AdsLifecycleOwner extends AdsLifecycleObserver { + @override + String get name => "AdsLifecycleOwner{$observerNames}"; + + String get observerNames => + adsLifecycleObservers.map((handler) => handler.name).toList().toString(); + + final List adsLifecycleObservers = []; + + void addObserver(AdsLifecycleObserver observer) { + adsLifecycleObservers.add(observer); + } + + void removeObserver(AdsLifecycleObserver observer) { + adsLifecycleObservers.remove(observer); + } + + @mustCallSuper + Future dispose() async { + adsLifecycleObservers.clear(); + } + + @override + void onRequestShow(AdsBundle adsBundle) { + for (var observer in adsLifecycleObservers) { + try { + // Log.w("onRequestShow ${observer.name} ${adsBundle.arguments}"); + + observer.onRequestShow(adsBundle); + } catch (error, stacktrace) { + Log.w("onRequestShow error:$error", stackTrace: stacktrace, syncFirebase: true); + } + } + } + + @override + void onRequestHide(AdsBundle adsBundle) { + for (var observer in adsLifecycleObservers) { + try { + observer.onRequestHide(adsBundle); + } catch (error, stacktrace) { + Log.w("onRequestHide error:$error", stackTrace: stacktrace, syncFirebase: true); + } + } + } + + @override + void onRequestLoad(AdsBundle adsBundle) { + for (var observer in adsLifecycleObservers) { + try { + observer.onRequestLoad(adsBundle); + } catch (error, stacktrace) { + Log.w("onRequestLoad [$runtimeType] ERROR!", tag: "Ads", error: error); + } + } + } + + @override + void onRequestDispose(AdsBundle adsBundle) { + for (var observer in adsLifecycleObservers) { + try { + observer.onRequestDispose(adsBundle); + } catch (error, stacktrace) { + Log.w("onRequestDispose [$runtimeType] ERROR!", tag: "Ads", error: error); + } + } + } + + @override + void onRequestReset(AdsBundle adsBundle) { + for (var observer in adsLifecycleObservers) { + try { + observer.onRequestReset(adsBundle); + } catch (error, stacktrace) { + Log.w("onRequestReset [$runtimeType] ERROR!", tag: "Ads", error: error); + } + } + } + + @override + void onAdLoaded(AdsBundle adsBundle) { + for (var observer in adsLifecycleObservers) { + try { + observer.onAdLoaded(adsBundle); + } catch (error, stacktrace) { + Log.w("onAdLoaded [$runtimeType] ERROR!", tag: "Ads", error: error); + } + } + } + + @override + void onAdLoadFailed(AdsBundle adsBundle) { + for (var observer in adsLifecycleObservers) { + try { + observer.onAdLoadFailed(adsBundle); + } catch (error, stacktrace) { + Log.w("onAdLoadFailed [$runtimeType] ERROR!", tag: "Ads", error: error); + } + } + } + + @override + void onAdDisplayFailed(AdsBundle adsBundle) { + for (var observer in adsLifecycleObservers) { + try { + observer.onAdDisplayFailed(adsBundle); + } catch (error, stacktrace) { + Log.w("onAdDisplayed [$runtimeType] ERROR!", tag: "Ads", error: error); + } + } + } + + @override + void onAdDisplayed(AdsBundle adsBundle) { + for (var observer in adsLifecycleObservers) { + try { + observer.onAdDisplayed(adsBundle); + } catch (error, stacktrace) { + Log.w("onAdDisplayed [$runtimeType] ERROR!", tag: "Ads", error: error); + } + } + } + + @override + void onAdClicked(AdsBundle adsBundle) { + for (var observer in adsLifecycleObservers) { + try { + observer.onAdClicked(adsBundle); + } catch (error, stacktrace) { + Log.w("onAdClicked [$runtimeType] ERROR!", tag: "Ads", error: error); + } + } + } + + @override + void onAdHidden(AdsBundle adsBundle) { + Log.w("CompositeAdsHandler onAdHidden !", syncFirebase: true); + for (var observer in adsLifecycleObservers) { + try { + observer.onAdHidden(adsBundle); + } catch (error, stacktrace) { + Log.w("onAdHidden [$runtimeType] ERROR!", tag: "Ads", error: error); + } + } + } + + @override + void onAdRewarded(AdsBundle adsBundle) { + for (var observer in adsLifecycleObservers) { + try { + observer.onAdRewarded(adsBundle); + } catch (error, _) { + Log.w("onAdRewarded [$runtimeType] ERROR!", tag: "Ads", error: error); + } + } + } +} + +// +// class CompositeRewardedAdsHandler extends CompositeAdsHandler implements RewardedAdsHandler { +// String get name => "CompositeRewardedAdsHandler{${super.handlerName}}"; +// } +// +typedef AdsCallback = void Function(AdsBundle); + +// +class AdsLifecycleObserverDelegate extends AdsLifecycleObserver { + @override + final String name; + + final AdsCallback? onRequestShowCallback; + + final AdsCallback? onRequestHideCallback; + + final AdsCallback? onRequestLoadCallback; + + final AdsCallback? onRequestDisposeCallback; + + final AdsCallback? onAdLoadedCallback; + + final AdsCallback? onAdLoadFailedCallback; + + final AdsCallback? onAdDisplayFailedCallback; + + final AdsCallback? onAdDisplayedCallback; + + final AdsCallback? onAdClickedCallback; + + final AdsCallback? onAdHiddenCallback; + + final AdsCallback? onAdRewardedCallback; + + AdsLifecycleObserverDelegate( + {this.name = "AdsLifecycleObserverDelegate", + this.onRequestShowCallback, + this.onRequestHideCallback, + this.onRequestLoadCallback, + this.onRequestDisposeCallback, + this.onAdLoadedCallback, + this.onAdLoadFailedCallback, + this.onAdDisplayFailedCallback, + this.onAdDisplayedCallback, + this.onAdClickedCallback, + this.onAdHiddenCallback, + this.onAdRewardedCallback}); + + @override + void onRequestShow(AdsBundle adsBundle) { + onRequestShowCallback?.call(adsBundle); + } + + @override + void onRequestHide(AdsBundle adsBundle) { + onRequestHideCallback?.call(adsBundle); + } + + @override + void onRequestLoad(AdsBundle adsBundle) { + onRequestLoadCallback?.call(adsBundle); + } + + @override + void onRequestDispose(AdsBundle adsBundle) { + onRequestDisposeCallback?.call(adsBundle); + } + + @override + void onAdLoaded(AdsBundle adsBundle) { + onAdLoadedCallback?.call(adsBundle); + } + + @override + void onAdLoadFailed(AdsBundle adsBundle) { + onAdLoadFailedCallback?.call(adsBundle); + } + + @override + void onAdDisplayFailed(AdsBundle adsBundle) { + onAdDisplayFailedCallback?.call(adsBundle); + } + + @override + void onAdDisplayed(AdsBundle adsBundle) { + onAdDisplayedCallback?.call(adsBundle); + } + + @override + void onAdClicked(AdsBundle adsBundle) { + onAdClickedCallback?.call(adsBundle); + } + + @override + void onAdHidden(AdsBundle adsBundle) { + onAdHiddenCallback?.call(adsBundle); + } + + @override + void onAdRewarded(AdsBundle adsBundle) { + onAdRewardedCallback?.call(adsBundle); + } +} +// +// class RewardsAdsHandlerDelegate extends AdsHandlerDelegate implements RewardedAdsHandler { +// final AdsCallback? onAdRewardedCallback; +// +// RewardsAdsHandlerDelegate( +// {String name = "RewardsAdsHandlerDelegate", +// AdsCallback? onRequestShowCallback, +// AdsCallback? onRequestHideCallback, +// AdsCallback? onRequestLoadCallback, +// AdsCallback? onRequestDisposeCallback, +// AdsCallback? onAdLoadedCallback, +// AdsCallback? onAdLoadFailedCallback, +// AdsCallback? onAdDisplayFailedCallback, +// AdsCallback? onAdDisplayedCallback, +// AdsCallback? onAdClickedCallback, +// AdsCallback? onAdHiddenCallback, +// this.onAdRewardedCallback}) +// : super( +// name: name, +// onRequestShowCallback: onRequestShowCallback, +// onRequestHideCallback: onRequestHideCallback, +// onRequestLoadCallback: onRequestLoadCallback, +// onRequestDisposeCallback: onRequestDisposeCallback, +// onAdLoadedCallback: onAdLoadedCallback, +// onAdLoadFailedCallback: onAdLoadFailedCallback, +// onAdDisplayFailedCallback: onAdDisplayFailedCallback, +// onAdDisplayedCallback: onAdDisplayedCallback, +// onAdClickedCallback: onAdClickedCallback, +// onAdHiddenCallback: onAdHiddenCallback); +// +// @override +// void onAdRewarded(AdsBundle adsBundle) { +// Log.d("[$name]onAdRewarded ${this.onAdRewardedCallback == null}"); +// this.onAdRewardedCallback?.call(adsBundle); +// } +// } diff --git a/guru_app/packages/guru_utils/lib/ads/utils/ads_cpm_calibration.dart b/guru_app/packages/guru_utils/lib/ads/utils/ads_cpm_calibration.dart new file mode 100644 index 0000000..db3ba44 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/ads/utils/ads_cpm_calibration.dart @@ -0,0 +1,6 @@ +/// Created by Haoyi on 2021/11/26 + +class AdsCpmCalibration { + static const defaultAndroidFacebookCpmCalibrationData = ''; + static const defaultIOSFacebookCpmCalibrationData = '{"data":{"list":[{"format":"inter","cpm":0.032877,"country":"us"},{"format":"reward","cpm":0.107767,"country":"us"},{"format":"inter","cpm":0.005034,"country":"in"},{"format":"reward","cpm":null,"country":"in"},{"format":"inter","cpm":0.011241,"country":"ca"},{"format":"reward","cpm":0.094526,"country":"ca"},{"format":"inter","cpm":0.003824,"country":"br"},{"format":"reward","cpm":0.009776,"country":"br"},{"format":"inter","cpm":0.049431,"country":"au"},{"format":"reward","cpm":0.056175,"country":"au"},{"format":"inter","cpm":0.012067,"country":"jp"},{"format":"reward","cpm":0.055358,"country":"jp"},{"format":"inter","cpm":0.01472,"country":"de"},{"format":"reward","cpm":0.020876,"country":"de"},{"format":"inter","cpm":0.011296,"country":"gb"},{"format":"reward","cpm":0.027093,"country":"gb"},{"format":"inter","cpm":0.006439,"country":"fr"},{"format":"reward","cpm":0.018981,"country":"fr"},{"format":"inter","cpm":null,"country":"tw"},{"format":"reward","cpm":null,"country":"tw"},{"format":"inter","cpm":null,"country":"kr"},{"format":"reward","cpm":null,"country":"kr"},{"format":"inter","cpm":0.010416,"country":"ph"},{"format":"reward","cpm":null,"country":"ph"},{"format":"inter","cpm":0.00599,"country":"ru"},{"format":"reward","cpm":0.004472,"country":"ru"},{"format":"inter","cpm":null,"country":"it"},{"format":"reward","cpm":null,"country":"it"},{"format":"inter","cpm":0.010365,"country":"es"},{"format":"reward","cpm":null,"country":"es"},{"format":"inter","cpm":null,"country":"hk"},{"format":"reward","cpm":null,"country":"hk"},{"format":"inter","cpm":0.007427,"country":"mx"},{"format":"reward","cpm":0.01035,"country":"mx"},{"format":"inter","cpm":null,"country":"nl"},{"format":"reward","cpm":null,"country":"nl"},{"format":"inter","cpm":null,"country":"sa"},{"format":"reward","cpm":null,"country":"sa"},{"format":"inter","cpm":null,"country":"be"},{"format":"reward","cpm":null,"country":"be"},{"format":"inter","cpm":null,"country":"se"},{"format":"reward","cpm":null,"country":"se"},{"format":"inter","cpm":null,"country":"sg"},{"format":"reward","cpm":null,"country":"sg"},{"format":"inter","cpm":null,"country":"cl"},{"format":"reward","cpm":null,"country":"cl"},{"format":"inter","cpm":0.01166,"country":"ch"},{"format":"reward","cpm":null,"country":"ch"},{"format":"inter","cpm":null,"country":"fi"},{"format":"reward","cpm":null,"country":"fi"},{"format":"inter","cpm":null,"country":"th"},{"format":"reward","cpm":null,"country":"th"},{"format":"inter","cpm":0.00109,"country":"pl"},{"format":"reward","cpm":null,"country":"pl"},{"format":"inter","cpm":null,"country":"dk"},{"format":"reward","cpm":null,"country":"dk"},{"format":"inter","cpm":null,"country":"ae"},{"format":"reward","cpm":null,"country":"ae"},{"format":"inter","cpm":0.009707,"country":"at"},{"format":"reward","cpm":0.011048,"country":"at"},{"format":"inter","cpm":0.004342,"country":"id"},{"format":"reward","cpm":null,"country":"id"},{"format":"inter","cpm":0.005622,"country":"vn"},{"format":"reward","cpm":0.005026,"country":"vn"},{"format":"inter","cpm":0.001684,"country":"tr"},{"format":"reward","cpm":null,"country":"tr"}]}}'; +} diff --git a/guru_app/packages/guru_utils/lib/ads/utils/ads_exception.dart b/guru_app/packages/guru_utils/lib/ads/utils/ads_exception.dart new file mode 100644 index 0000000..9f072bb --- /dev/null +++ b/guru_app/packages/guru_utils/lib/ads/utils/ads_exception.dart @@ -0,0 +1,15 @@ +import "dart:core"; + +/// Created by Haoyi on 2021/11/29 +/// + +class NoAdsException implements Exception { + final String msg; + + NoAdsException(this.msg); + + @override + String toString() { + return 'NoAdsException{msg: $msg}'; + } +} diff --git a/guru_app/packages/guru_utils/lib/aigc/bi/ai_bi.dart b/guru_app/packages/guru_utils/lib/aigc/bi/ai_bi.dart new file mode 100644 index 0000000..09b4078 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/aigc/bi/ai_bi.dart @@ -0,0 +1,110 @@ +import 'package:persistent/log/persistent_log.dart'; +import 'package:intl/intl.dart'; + +/// Created by Haoyi on 2023/3/30 + +class AdsType { + static const String banner = "B"; + static const String interstitial = "I"; + static const String rewarded = "R"; + static const String native = "N"; +} + +class AiBi { + static AiBi instance = AiBi(); + static const logName = "aibi"; + static final standardDateFormat = DateFormat("yyyyMMddTHHmmss"); + + void init() async { + await PersistentLog.createLogger(logName: logName, formatter: Formatter.raw); + } + + void _log(List args) { + PersistentLog.log(logName: logName, message: args.join(",")); + } + + void adsLoad(String adsType, {String adScene = "Unknown"}) { + _log([standardDateFormat.format(DateTime.now()), "ads_load", adsType, adScene]); + } + + void adsLoaded(String adsType, {String adScene = "Unknown", int duration = 0}) { + _log([ + standardDateFormat.format(DateTime.now()), + "ads_loaded", + adsType, + adScene, + duration.toString() + ]); + } + + void adsImp(String adsType, + {String adScene = "Unknown", double adRevenue = 0, String network = "Unknown"}) { + _log([ + standardDateFormat.format(DateTime.now()), + "ads_imp", + adsType, + adScene, + adRevenue.toString(), + '"$network"' + ]); + } + + void adsClk(String adsType, {String adScene = "Unknown"}) { + _log([ + standardDateFormat.format(DateTime.now()), + "ads_clk", + adsType, + adScene, + ]); + } + + void adsFailed(String adsType, {String adScene = "Unknown", String reason = "Unknown"}) { + _log([standardDateFormat.format(DateTime.now()), "ads_failed", adsType, adScene, '"$reason"']); + } + + void adsHide(String adsType, {String adScene = "Unknown"}) { + _log([standardDateFormat.format(DateTime.now()), "ads_hide", adsType, adScene]); + } + + void adsRewarded(String adScene) { + _log([standardDateFormat.format(DateTime.now()), "ads_rwd", adScene]); + } + + void spendVirtualCurrency(int balance, double amount, String scene) { + _log([ + standardDateFormat.format(DateTime.now()), + "svc", + "coin", + balance.toString(), + amount.toString(), + '"$scene"' + ]); + } + + void earnVirtualCurrency(int balance, double amount, String method) { + _log([ + standardDateFormat.format(DateTime.now()), + "evc", + "coin", + balance.toString(), + amount.toString(), + method + ]); + } + + void click(String scene) { + _log([standardDateFormat.format(DateTime.now()), "clk", '"$scene"']); + } + + void dialog(String name) { + _log([standardDateFormat.format(DateTime.now()), "dlg", '"$name"']); + } + + void lifecycle(String name, bool isResumed) { + _log([standardDateFormat.format(DateTime.now()), "lifecycle", name, isResumed ? 'R' : "P"]); + } + + void applife(bool isForeground) { + _log([standardDateFormat.format(DateTime.now()), "applife", isForeground ? 'F' : "B"]); + } +} diff --git a/guru_app/packages/guru_utils/lib/analytics/analytics.dart b/guru_app/packages/guru_utils/lib/analytics/analytics.dart new file mode 100644 index 0000000..91f0654 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/analytics/analytics.dart @@ -0,0 +1,80 @@ +import 'dart:collection'; + +import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_utils/log/log.dart'; + +/// Created by Haoyi on 2023/1/24 +abstract class Analytics { + static final DoubleLinkedQueue> latestEventRecords = DoubleLinkedQueue(); + static final Map userProperties = {}; + + Analytics() { + AnalyticsUtils._analytics = this; + } + + void recordEvents(String name, Map params) async { + latestEventRecords.addFirst(MapEntry("$name [${DateTimeUtils.humanTime}]", params.toString())); + if (latestEventRecords.length > 300) { + latestEventRecords.removeLast(); + } + } + + void recordProperty(String name, String value) async { + userProperties[name] = value; + Log.d("==> set property $name = $value"); + } + + Future getAppInstanceId(); + + void setScreen(String screenName); + + void logEvent(String eventName, Map parameters); + + void logEventEx(String eventName, + {String? itemCategory, + String? itemName, + double? value, + Map parameters = const {}}); + + Future setUserProperty(String key, String value); + + void logException(dynamic exception, {StackTrace? stacktrace}); + + void logFirebase(String msg); +} + +class AnalyticsUtils { + static Analytics? _analytics; + static AnalyticsUtils instance = AnalyticsUtils._(); + + AnalyticsUtils._(); + + void setScreen(String screenName) { + _analytics?.setScreen(screenName); + } + + void logEvent(String eventName, Map parameters) { + _analytics?.logEvent(eventName, parameters); + } + + void logEventEx(String eventName, + {String? itemCategory, + String? itemName, + double? value, + Map parameters = const {}}) { + _analytics?.logEventEx(eventName, + itemCategory: itemCategory, itemName: itemName, parameters: parameters); + } + + Future setUserProperty(String key, String value) async { + return await _analytics?.setUserProperty(key, value); + } + + void logException(dynamic exception, {StackTrace? stacktrace}) { + _analytics?.logException(exception, stacktrace: stacktrace); + } + + void logFirebase(String msg) { + _analytics?.logFirebase(msg); + } +} diff --git a/guru_app/packages/guru_utils/lib/app_ownership/app_ownership_utils.dart b/guru_app/packages/guru_utils/lib/app_ownership/app_ownership_utils.dart new file mode 100644 index 0000000..bd6cb9b --- /dev/null +++ b/guru_app/packages/guru_utils/lib/app_ownership/app_ownership_utils.dart @@ -0,0 +1,169 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:guru_popup/guru_popup.dart'; +import 'package:guru_utils/image/image_utils.dart'; +import 'package:guru_utils/router/router.dart'; +import 'package:guru_widgets/dialog/guru_dialog.dart'; +import 'dart:ui'; +import 'dart:ui' as ui; +import 'package:package_info_plus/package_info_plus.dart'; + +class AppOwnerInfo { + final String logo; + final String webUrl; + final String gpUrl; + final String appName; + final String md5; + final String lanuchYear; + + const AppOwnerInfo(this.logo, { + this.webUrl = '', + this.gpUrl = '', + this.appName = '', + this.md5 = '', + this.lanuchYear = '2022' + }); +} + +class AppOwnershipUtils { + static Future generateAppBitmap(AppOwnerInfo info, {bool waterMark = true}) async { + const canvasRect = Rect.fromLTWH(0.0, 0.0, 1056.0, 640.0); + Paint logoPaint = Paint()..filterQuality = FilterQuality.high; + Paint bgPaint = Paint()..isAntiAlias = true; + + final PictureRecorder recorder = PictureRecorder(); + Canvas canvas = Canvas(recorder, canvasRect); + + bgPaint.color = const Color(0xFF5F3E30); + canvas.drawRRect( + RRect.fromRectAndRadius(canvasRect, const Radius.circular(10.0)), + bgPaint); + + if (waterMark) { + canvas.rotate(-3.14 / 4); + for (var i = 0; i < 4; i++) { + for (var j = 0; j < 6; j++) { + ParagraphBuilder guruParagraphBuilder = + ParagraphBuilder(ParagraphStyle( + fontStyle: FontStyle.normal, + fontSize: 48, + fontFamily: 'EncodeSansExpanded', + strutStyle: ui.StrutStyle(height: 1.8), + )); + guruParagraphBuilder.pushStyle( + ui.TextStyle(color: const Color.fromRGBO(173, 133, 81, 0.6))); + guruParagraphBuilder.addText('Guru Game'); + const content = ParagraphConstraints(width: 400); + canvas.drawParagraph(guruParagraphBuilder.build()..layout(content), + Offset(i * 400 - 400, j * 240)); + } + } + canvas.restore(); + canvas.rotate(3.14 / 4); + } + + final logoImage = await ImageUtils.loadImageFromAsset(info.logo); + final logoSrcRect = Rect.fromLTWH( + 0.0, 0.0, logoImage.width.toDouble(), logoImage.height.toDouble()); + const logoDstRect = Rect.fromLTWH(40.0, 180.0, 240.0, 240.0); + + canvas.drawImageRect(logoImage, logoSrcRect, logoDstRect, logoPaint); + + ParagraphBuilder logoParagraphBuilder = ParagraphBuilder(ParagraphStyle( + textAlign: TextAlign.center, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.bold, + fontSize: 40, + fontFamily: 'EncodeSansExpanded', + strutStyle: ui.StrutStyle(height: 1.8), + )); + logoParagraphBuilder + .pushStyle(ui.TextStyle(color: const Color(0xFFF8C077))); + logoParagraphBuilder.addText(info.appName); + const title = ParagraphConstraints(width: 220); + canvas.drawParagraph( + logoParagraphBuilder.build()..layout(title), const Offset(40, 430)); + + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + final Map textMap = { + 'Official Website': info.webUrl, + 'Google Play': info.gpUrl, + 'Package Name': packageInfo.packageName, + 'MD5 certificate fingerprint': info.md5 + }; + + final textKeys = textMap.keys.toList(); + for (var i = 0; i < textKeys.length; i++) { + final String key = textKeys[i]; + final String value = textMap[key]!; + + ParagraphBuilder titleParagraphBuilder = ParagraphBuilder(ParagraphStyle( + fontStyle: FontStyle.normal, + fontSize: 46, + fontFamily: 'EncodeSansExpanded', + strutStyle: ui.StrutStyle(height: 1.8), + )); + titleParagraphBuilder + .pushStyle(ui.TextStyle(color: const Color(0xFFF8C077))); + titleParagraphBuilder.addText(key); + const title = ParagraphConstraints(width: 660); + canvas.drawParagraph(titleParagraphBuilder.build()..layout(title), + Offset(320, i * 136 + 40)); + + ParagraphBuilder valueParagraphBuilder = ParagraphBuilder(ParagraphStyle( + fontStyle: FontStyle.normal, + fontSize: value.length > 50 ? 28 : 40, + fontFamily: 'EncodeSansExpanded', + strutStyle: ui.StrutStyle(height: 1.8), + )); + valueParagraphBuilder.pushStyle(ui.TextStyle(color: Colors.white)); + valueParagraphBuilder.addText(value); + const content = ParagraphConstraints(width: 660); + canvas.drawParagraph(valueParagraphBuilder.build()..layout(content), + Offset(320, i * 136 + 90)); + } + + final image = await recorder + .endRecording() + .toImage(canvasRect.width.toInt(), canvasRect.height.toInt()); + ByteData? imgBytes = await image.toByteData(format: ui.ImageByteFormat.png); + Uint8List pngBytes = imgBytes!.buffer.asUint8List(); + return pngBytes; + } + + static Future waterMarkWidget( + {String title = 'Guru Game', + String subTitle = '', + int lineCount = 6}) async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + + return SizedBox( + width: double.infinity, + height: double.infinity, + child: Column( + children: [ + for (var i = 0; i < lineCount; i++) + Expanded( + child: Center( + child: Transform.rotate( + angle: -0.34, + child: Column(children: [ + Text( + title, + style: const TextStyle(color: Colors.black, fontSize: 20), + ), + Text( + packageInfo.packageName, + style: const TextStyle(color: Colors.black, fontSize: 16), + ), + Text( + subTitle, + style: const TextStyle(color: Colors.black, fontSize: 16), + ), + ])), + )), + ], + ), + ); + } +} diff --git a/guru_app/packages/guru_utils/lib/audio/audio_bundle.dart b/guru_app/packages/guru_utils/lib/audio/audio_bundle.dart new file mode 100644 index 0000000..040a25c --- /dev/null +++ b/guru_app/packages/guru_utils/lib/audio/audio_bundle.dart @@ -0,0 +1,41 @@ +/// Created by Haoyi on 2022/8/25 + +part of 'audio_effector.dart'; + +abstract class AudioBundle { + String get name; + + Map get audios; + + Completer? _loadedCompleter; + + Future load() async { + final completer = _loadedCompleter; + if (completer != null) { + Log.w("already loading, waiting... ${completer.isCompleted}"); + if (completer.isCompleted) { + return; + } + return completer.future; + } + _loadedCompleter = Completer(); + + final sources = audios.values.map((a) => a.load).toList(); + for (var source in sources) { + try { + await source.call(); + } catch (error, stacktrace) { + Log.w("load audio error! $error", + tag: "audio", stackTrace: stacktrace, syncCrashlytics: true); + } + } + _loadedCompleter?.complete(true); + _loadedCompleter = null; + } + + void dispose() { + for (var audio in audios.values) { + audio.dispose(); + } + } +} diff --git a/guru_app/packages/guru_utils/lib/audio/audio_effector.dart b/guru_app/packages/guru_utils/lib/audio/audio_effector.dart new file mode 100644 index 0000000..fd89163 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/audio/audio_effector.dart @@ -0,0 +1,82 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/quiver/cache/cache.dart'; +import 'package:guru_utils/quiver/cache/map_cache.dart'; + +import 'package:soundpool/soundpool.dart'; + +part 'audio_bundle.dart'; + +/// Created by Haoyi on 2022/8/25 +/// +part 'audio_model.dart'; + +part 'sound_pool.dart'; + +typedef Stoppable = void Function(); + +class AudioEffector { + static final AudioEffector instance = AudioEffector._(); + + static int soundEffect = SoundEffect.on; + + AudioEffector._(); + + final Map audios = {}; + + final Map bundles = {}; + + static void init() {} + + Future registerBundle(AudioBundle bundle) async { + if (!bundles.containsKey(bundle.name)) { + await bundle.load(); + bundles[bundle.name] = bundle; + audios.addAll(bundle.audios); + } + } + + Future unregisterBundle(AudioBundle bundle) async { + bundles.remove(bundle.name); + final _audios = bundle.audios; + audios.removeWhere((key, _) => _audios.containsKey(key)); + bundle.dispose(); + } + + void enable() { + AudioEffector.soundEffect = SoundEffect.on; + } + + void disable() { + AudioEffector.soundEffect = SoundEffect.off; + } + + void play(AudioEffect audio) async { + _internalPlay(audio); + } + + Future startLoop(AudioEffect audio) { + final control = _internalPlay(audio, -1); + return Future.value(() { + control.then((value) => value?.stop()); + }); + } + + Future _internalPlay(AudioEffect audio, [int repeat = 0]) async { + try { + if (soundEffect == SoundEffect.off) { + return null; + } + assert( + audios.containsKey(audio), + 'Tried to play unregistered audio $audio', + ); + return audios[audio]?.play(repeat); + } catch (error, stacktrace) { + Log.w("play error:$error"); + } + return null; + } +} diff --git a/guru_app/packages/guru_utils/lib/audio/audio_model.dart b/guru_app/packages/guru_utils/lib/audio/audio_model.dart new file mode 100644 index 0000000..64ee5fd --- /dev/null +++ b/guru_app/packages/guru_utils/lib/audio/audio_model.dart @@ -0,0 +1,63 @@ +part of 'audio_effector.dart'; + +/// Created by Haoyi on 2022/8/25 + +class SoundEffect { + static const off = 0; + static const on = 1; + static const reduced = 2; +} + +class AudioEffect { + final String path; + final String? package; + + const AudioEffect(this.path, {this.package}); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AudioEffect && + runtimeType == other.runtimeType && + path == other.path && + package == other.package; + + @override + int get hashCode => path.hashCode ^ package.hashCode; +} + +abstract class Audio { + Future play([int repeat = 0]); + + Future load(); + + void dispose(); + + String prefixFile(String file) { + return 'assets/audio/$file'; + } +} + +class SingleSoundPool extends Audio { + static final SoundPoolCache _cache = SoundPoolCache(); + final AudioEffect audio; + + SingleSoundPool({ + required this.audio, + }); + + @override + Future load() async { + await _cache.load(audio); + } + + @override + Future play([int repeat = 0]) { + return _cache.play(audio, repeat); + } + + @override + void dispose() { + _cache.invalidate(audio); + } +} diff --git a/guru_app/packages/guru_utils/lib/audio/sound_pool.dart b/guru_app/packages/guru_utils/lib/audio/sound_pool.dart new file mode 100644 index 0000000..960245e --- /dev/null +++ b/guru_app/packages/guru_utils/lib/audio/sound_pool.dart @@ -0,0 +1,49 @@ +part of 'audio_effector.dart'; + +/// Created by Haoyi on 2022/7/12 +class SoundPoolCache { + final Soundpool _pool; + + late Cache _cache; + + SoundPoolCache() + : _pool = Soundpool.fromOptions( + options: const SoundpoolOptions( + maxStreams: 8, + iosOptions: SoundpoolOptionsIos( + audioSessionCategory: AudioSessionCategory.ambient, + audioSessionMode: AudioSessionMode.normal))) { + _cache = MapCache.lru( + maximumSize: 32, + onRemoved: (streamId) { + _pool.stop(streamId); + }); + } + + Future _loadAudio(AudioEffect audio) async { + final assetName = "assets/audio/${audio.path}"; + final soundData = await rootBundle.load(audio.package != null + ? "packages/${audio.package}/$assetName" + : assetName); + final soundId = await _pool.load(soundData); + await _pool.setVolume(soundId: soundId, volume: 0.8); + return soundId; + } + + Future load(AudioEffect audio) async { + await _cache.get(audio, ifAbsent: _loadAudio); + } + + Future play(AudioEffect audio, int repeat) async { + final soundId = await _cache.get(audio, ifAbsent: _loadAudio); + Log.d("soundId: $soundId"); + if (soundId != null) { + return _pool.playWithControls(soundId, repeat: repeat); + } + return null; + } + + void invalidate(AudioEffect effect) { + _cache.invalidate(effect); + } +} diff --git a/guru_app/packages/guru_utils/lib/collection/collectionutils.dart b/guru_app/packages/guru_utils/lib/collection/collectionutils.dart new file mode 100644 index 0000000..0e45769 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/collection/collectionutils.dart @@ -0,0 +1,84 @@ +/// Created by @Haoyi on 2020/5/27 +/// + +class CollectionUtils { + static Map filterOutNulls(Map parameters) { + final Map filtered = {}; + parameters.forEach((String key, dynamic value) { + if (value != null) { + filtered[key] = value; + } + }); + return filtered; + } + + static dynamic checkAndGet(Map map, String key, [dynamic defaultValue]) { + return ((map != null) ? map[key] : defaultValue) ?? defaultValue; + } + + static Map modifyMap(Map src, + {List? removeKeys, Map? replaceMap, bool create = true}) { + return (create ? Map.from(src) : src) + ..addAll(replaceMap ?? {}) + ..removeWhere((key, value) { + return removeKeys != null && removeKeys.contains(key); + }); + } + + static List foreachJsonList(List list, bool predicate(dynamic key, dynamic value), + dynamic replace(dynamic key, dynamic value)) { + List result = []; + for (var item in list) { + dynamic value = item; + if (item is Map) { + value = foreachJsonMap(item as Map, predicate, replace); + } else if (item is List) { + value = foreachJsonList(item, predicate, replace); + } + result.add(value); + } + return result; + } + + static Map foreachJsonMap(Map map, + bool predicate(dynamic key, dynamic value), dynamic replace(dynamic key, dynamic value)) { + final result = Map.of(map); + for (var entry in map.entries) { + final needReplace = predicate(entry.key, entry.value); + if (needReplace) { + result[entry.key] = replace(entry.key, entry.value); + continue; + } + if (entry.value is Map) { + result[entry.key] = foreachJsonMap(entry.value, predicate, replace); + } else if (entry.value is List) { + result[entry.key] = foreachJsonList(entry.value, predicate, replace); + } + } + return result; + } +} + +class ListUtils { + static List filterOutNulls(List data) { + final List filtered = []; + for (var item in data) { + if (item != null) { + filtered.add(item); + } + } + return filtered; + } +} + +class MapUtils { + static Map filterOutNulls(Map parameters) { + final Map filtered = {}; + parameters.forEach((String key, dynamic value) { + if (value != null) { + filtered[key] = value; + } + }); + return filtered; + } +} diff --git a/guru_app/packages/guru_utils/lib/collection/int_array.dart b/guru_app/packages/guru_utils/lib/collection/int_array.dart new file mode 100644 index 0000000..bfdb6a3 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/collection/int_array.dart @@ -0,0 +1,82 @@ +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:guru_utils/hash/hash.dart'; + + + +/// Created by @Haoyi on 4/13/21 + +class IntArray { + int head = 0; + int tail = 0; + + final List data; + + IntArray() : this.data = []; + + IntArray.fromUint8List(Uint8List list) : data = list.buffer.asUint32List().toList() { + head = 0; + tail = data.length; + } + + void set(int index, int value) { + data[index] = value; + } + + int get(int index) => data[index]; + + int write(int value) { + data.add(value); + return tail++; + } + + int writeAll(List values) { + data.addAll(values); + final start = tail; + tail += values.length; + return start; + } + + int? read() { + if (head != tail && head < tail) { + return data[head++]; + } + return null; + } + + int? peek() { + if (head != tail && head < tail) { + return data[head]; + } + return null; + } + + + int get remains => tail - head; + + List readAll(int length) { + final r = remains; + if (r >= length) { + print("readAll length:$length"); + final start = head; + final end = head + length; + head = end; + return data.sublist(start, end); + } + return []; + } + + Uint8List asUint8List() { + return Uint32List.fromList(data).buffer.asUint8List(); + } + + int checksum({int start = 0, int? end}) { + int hash = 0x9E370001; + final limit = (end != null) ? min(end, data.length) : data.length; + for (int index = start; index < limit; ++index) { + hash = hashCombine(hash, data[index]); + } + return hashFinish(hash); + } +} diff --git a/guru_app/packages/guru_utils/lib/collection/rbtree.dart b/guru_app/packages/guru_utils/lib/collection/rbtree.dart new file mode 100644 index 0000000..cc19dc7 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/collection/rbtree.dart @@ -0,0 +1,18 @@ +/// Created by @Haoyi on 4/15/21 + +enum RBColor { + Red, Black +} + +// class _LinkedRBEntry { +// _LinkedRBEntry(this.key, this.value); +// +// K key; +// V value; +// +// RBColor _color; +// _LinkedRBEntry _parent; +// _LinkedRBEntry? _left; +// _LinkedRBEntry? _right; +// +// } \ No newline at end of file diff --git a/guru_app/packages/guru_utils/lib/colors/color_utils.dart b/guru_app/packages/guru_utils/lib/colors/color_utils.dart new file mode 100644 index 0000000..13606eb --- /dev/null +++ b/guru_app/packages/guru_utils/lib/colors/color_utils.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +/// Created by Haoyi on 4/28/21 + +class ColorUtils { + static Color toColor(String? colorStr, {Color defaultColor = Colors.white}) { + if (colorStr == null) { + return defaultColor; + } + String colorValue = colorStr.toUpperCase().replaceAll("#", ""); + int value = int.parse(colorValue, radix: 16); + if (colorValue.length == 3) { + return Color.fromARGB( + 0xFF, + ((((value >> 8) & 0x0F) / 0x0F) * 0xFF).toInt(), + ((((value >> 4) & 0x0F) / 0x0F) * 0xFF).toInt(), + (((value & 0x0F) / 0x0F) * 0xFF).toInt()); + } + if (colorValue.length == 6) { + value = value | 0xFF000000; + } + + return Color(value); + } + + + static final _ASCII_A = "A".codeUnitAt(0); + static final _ASCII_0 = "0".codeUnitAt(0); + static final _ASCII_9 = "9".codeUnitAt(0); + static final _ASCII_DOT = ".".codeUnitAt(0); + + static int toColorValue(String colorStr, {Color defaultColor = Colors.white}) { + if (colorStr == null) { + return defaultColor.value; + } + String colorValue = colorStr.toUpperCase().replaceAll("#", ""); + if (colorValue.length == 3) { + return Color.fromARGB( + 0xFF, + (((colorValue[0].codeUnitAt(0) - _ASCII_A) / 0x0F) * 0xFF).toInt(), + (((colorValue[1].codeUnitAt(0) - _ASCII_A) / 0x0F) * 0xFF).toInt(), + (((colorValue[2].codeUnitAt(0) - _ASCII_A) / 0x0F) * 0xFF).toInt()) + .value; + } + if (colorValue.length == 6) { + colorValue = "FF" + colorValue; + } + + return int.parse(colorValue, radix: 16); + } +} diff --git a/guru_app/packages/guru_utils/lib/controller/ads_controller.dart b/guru_app/packages/guru_utils/lib/controller/ads_controller.dart new file mode 100644 index 0000000..6f69935 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/controller/ads_controller.dart @@ -0,0 +1,123 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:guru_utils/ads/ads.dart'; +import 'package:guru_utils/ads/data/ads_model.dart'; +import 'package:guru_utils/controller/lifecycle_controller.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/timer/timer_scheduler.dart'; + +import 'aware/controller_aware.dart'; + +/// Created by Haoyi on 2022/3/2 + +class AdsCompleter { + final AdType adType; + final Completer completer; + final DateTime at; + + AdsCompleter(this.adType) + : completer = Completer.sync(), + at = DateTime.now(); + + void error(AdCause adCause) { + Log.d("AdsCompleter error! $adCause", tag: "Ads"); + completer.complete(AdsResult.build(adType, adCause)); + } + + void success() { + Log.d("AdsCompleter success!", tag: "Ads"); + completer.complete(AdsResult.success(adType)); + } + + void close() { + Log.d("AdsCompleter success!", tag: "Ads"); + completer.complete(AdsResult.build(adType, AdCause.ignore)); + } + + bool get isCompleted => completer.isCompleted; + + Future waiting() { + return completer.future; + } +} + +abstract class AdsController extends LifecycleController { + AdsCompleter? adsCompleter; + ActiveTimer? _autoCompleteFrozenAdsTimer; + + @override + @mustCallSuper + void onInit() { + super.onInit(); + if (this is BannerAware) { + (this as BannerAware).initBanner(); + } else if (this is SharedBannerAware) { + (this as SharedBannerAware).initBanner(); + } + + if (this is InterstitialAware) { + (this as InterstitialAware).bindInterstitialAd(); + } + + if (this is RewardedAware) { + (this as RewardedAware).bindRewardedAd(); + } + } + + @mustCallSuper + @override + void onClose() { + if (this is BannerAware) { + (this as BannerAware).disposeBanner(); + } else if (this is SharedBannerAware) { + (this as SharedBannerAware).disposeBanner(); + } + if (this is InterstitialAware) { + (this as InterstitialAware).unbindInterstitialAd(); + } + if (this is RewardedAware) { + (this as RewardedAware).unbindRewardedAd(); + } + + adsCompleter?.close(); + super.onClose(); + } + + void clearAutoCompleteFrozenAdsTimer() { + if (_autoCompleteFrozenAdsTimer != null) { + removeTimer(_autoCompleteFrozenAdsTimer); + Log.w("[$runtimeType] onPaused, remove _autoCompleteFrozenAdsTimer", tag: "Ads"); + } + } + + @override + @mustCallSuper + void onResumed() { + super.onResumed(); + if (this is SharedBannerAware) { + (this as SharedBannerAware).processResumed(); + } + if (adsCompleter?.isCompleted == false) { + Log.d("[$runtimeType] onResumed, adsCompleter not complete!, start ADS Frozen check! waiting...", tag: "Ads"); + _autoCompleteFrozenAdsTimer = delayed(const Duration(seconds: 5), () { + if (adsCompleter?.isCompleted == false) { + Log.w( + "[$runtimeType] onResumed, ADS Frozen!!! but adsCompleter is not completed, will complete with AdCause.noResponse", + tag: "Ads"); + adsCompleter?.error(AdCause.noResponse); + } + }); + } + } + + @override + @mustCallSuper + void onPaused() { + if (this is SharedBannerAware) { + (this as SharedBannerAware).processPaused(); + } + clearAutoCompleteFrozenAdsTimer(); + super.onPaused(); + } +} diff --git a/guru_app/packages/guru_utils/lib/controller/aware/account/account_aware.dart b/guru_app/packages/guru_utils/lib/controller/aware/account/account_aware.dart new file mode 100644 index 0000000..9c74053 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/controller/aware/account/account_aware.dart @@ -0,0 +1,87 @@ + + +/// Created by Haoyi on 2022/5/23 + +// mixin AccountAware on LifecycleController { +// final accountDataStore = Injector.provide(); +// +// Stream get observableAccountProfile => accountDataStore.observableAccountProfile; +// +// String? get saasToken => accountDataStore.saasToken; +// +// String? get uid => accountDataStore.uid; +// +// AccountProfile? get accountProfile => accountDataStore.accountProfile; +// +// String? get nickname => accountDataStore.nickname; +// +// String? get countryCode => accountDataStore.countryCode; +// +// SaasUser? get user => accountDataStore.user; +// +// DeviceInfo? get currentDevice => accountDataStore.currentDevice; +// +// String? get userAvatar => accountProfile?.avatar; +// +// CumulativeInt? get bestScore => accountProfile?.bestScore; +// +// bool get accountInitialized => accountDataStore.initialized; +// +// Stream get observableNickname => +// observableAccountProfile.map((accountProfile) => accountProfile?.nickname); +// +// Stream get observableAccountInitialized => accountDataStore.observableInitialized; +// +// void initAccount() { +// Injector.provide().init(); +// } +// +// Future updateAccountProfile( +// {String? nickname, String? avatar, CumulativeInt? bestScore, String? countryCode}) async { +// final accountService = Injector.provide(); +// // final rankService = Injector.provide(); +// return await accountService.modifyProfile( +// nickname: nickname, avatar: avatar, bestScore: bestScore, countryCode: countryCode); +// // if (result) { +// // await rankService.refreshAccountProfile(); +// // } +// return true; +// } +// +// Future uploadBestScore() async { +// final accountService = Injector.provide(); +// final rankService = Injector.provide(); +// final latestBestScore = StatisticManager.instance.peekBestScore(); +// final accountBestScore = bestScore ?? CumulativeInt.zero; +// +// Log.w("uploadBestScore latestBestScore! $latestBestScore $accountBestScore", +// syncFirebase: true); +// if (latestBestScore > accountBestScore) { +// String? changedCountryCode = DeviceUtils.buildLocaleInfo().countryCode.toUpperCase(); +// if (DartExt.isBlank(changedCountryCode) || countryCode == changedCountryCode) { +// changedCountryCode = null; +// } +// try { +// await accountService.modifyProfile( +// bestScore: latestBestScore, countryCode: changedCountryCode); +// } catch (error, stacktrace) { +// Log.w("modifyProfile error!", error: error, stackTrace: stacktrace, syncFirebase: true); +// } +// } +// await rankService.uploadBestScore(latestBestScore); +// return true; +// } +// +// RankData buildEmptyRankData(String boardId, {CumulativeInt? bestScore}) { +// return RankData( +// boardId, +// uid ?? "", +// -1, +// bestScore ?? this.bestScore, +// LbUserInfo( +// nickname: nickname ?? "", +// countryCode: DeviceUtils.buildLocaleInfo().countryCode.toUpperCase(), +// avatar: "avatar_1", +// attr: UserAttr.real)); +// } +// } diff --git a/guru_app/packages/guru_utils/lib/controller/aware/ads/banner_aware.dart b/guru_app/packages/guru_utils/lib/controller/aware/ads/banner_aware.dart new file mode 100644 index 0000000..73d6429 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/controller/aware/ads/banner_aware.dart @@ -0,0 +1,375 @@ +/// Created by Haoyi on 5/10/21 + +import 'dart:async'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart' hide Rx; +import 'package:guru_popup/guru_popup.dart'; +import 'package:guru_utils/ads/ads_delegate.dart'; +import 'package:guru_utils/ads/ads_manager_delegate.dart'; +import 'package:guru_utils/ads/data/ads_model.dart'; +import 'package:guru_utils/ads/handler/ads_handler.dart'; +import 'package:guru_utils/controller/ads_controller.dart'; +import 'package:guru_utils/controller/aware/ads/overlay/ads_overlay.dart'; +import 'package:guru_utils/controller/lifecycle_controller.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/network/network_utils.dart'; +import 'package:guru_utils/remote/remote_config.dart'; +import 'package:guru_utils/tuple/tuple.dart'; +import 'package:system_clock/system_clock.dart'; + +class _BannerStatus { + final LifecycleInfo lifecycleInfo; + final bool adLoaded; + final bool isNoAds; + + _BannerStatus(this.lifecycleInfo, this.adLoaded, this.isNoAds); +} + +@Deprecated("use SharedBannerAware instead") +mixin BannerAware on AdsController { + AdsDelegate? bannerAds; + + String get bannerScene => ""; + final BehaviorSubject adLoadedSubject = BehaviorSubject.seeded(false); + + bool bannerAdIsShow = false; + bool bannerAdIsLoad = false; + + Stream get loadBannerWhenReady => Stream.value(true); + + bool shouldHide() => !isTopRoute; + + void onBannerAdLoaded(AdsBundle adsBundle) { + Log.i("onBannerAdLoaded !", tag: "Ads"); + if (isResumed) { + adLoadedSubject.addEx(true); + } + } + + void onBannerAdLoadFailed(AdsBundle adsBundle) { + Log.i("onBannerAdLoadFailed !", tag: "Ads"); + } + + Future _getBanner() async { + bannerAds ??= await AdsManagerDelegate.instance.createBannerAds( + scene: bannerScene, + observer: AdsLifecycleObserverDelegate( + onAdLoadedCallback: onBannerAdLoaded, + onAdLoadFailedCallback: onBannerAdLoadFailed, + )); + return bannerAds!; + } + + void initBanner() async { + addSubscription(Rx.combineLatest2( + AdsManagerDelegate.instance.observableInitialized, + loadBannerWhenReady, + (a, b) => a && b).listen((ready) async { + if (ready) { + await _loadBanner(); + } + }, onError: (error, stacktrace) { + Log.e("observable Sdk initialized error!", tag: "Ads", error: error, stackTrace: stacktrace); + })); + addSubscription(Rx.combineLatest3( + observableLifecycleInfo, + adLoadedSubject.stream, + AdsManagerDelegate.instance.observableNoAds, + (lifecycleInfo, loaded, noAds) => _BannerStatus(lifecycleInfo, loaded, noAds)) + .listen((status) { + Log.i( + "[$runtimeType] observable banner lifecycle state:${status.lifecycleInfo} noAds:${status.isNoAds} adLoaded:${status.adLoaded} bannerAdIsShow:$bannerAdIsShow", + tag: "Ads"); + if (status.isNoAds) { + hideBanner(); + } else if (status.lifecycleInfo.isResumed()) { + if (status.adLoaded) { + Log.i("showBanner!", tag: "Ads"); + showBanner(); + } + } else if (shouldHide()) { + Log.i("hideBanner!", tag: "Ads"); + hideBanner(); + } + }, onError: (error, stacktrace) { + Log.e("observable lifecycle state and loaded state error!", + tag: "Ads", error: error, stackTrace: stacktrace); + })); + } + + void disposeBanner() { + bannerAds?.dispose(); + bannerAds = null; + adLoadedSubject.close(); + bannerAdIsShow = false; + bannerAdIsLoad = false; + } + + void onBannerResume() {} + + void onBannerPaused() {} + + void hideBanner() { + if (bannerAdIsShow) { + bannerAds?.hide(); + bannerAdIsShow = false; + } + } + + Future _loadBanner() async { + final banner = await _getBanner(); + if (AdsManagerDelegate.instance.isPurchasedNoAd) { + return; + } + if (!bannerAdIsLoad) { + Log.v("load banner!!", tag: "Ads"); + bannerAdIsLoad = true; + final result = await banner.load() == AdCause.success; + if (result != bannerAdIsLoad) { + bannerAdIsLoad = result; + } + } + } + + void showBanner() async { + final banner = await _getBanner(); + if (!bannerAdIsLoad) { + await _loadBanner(); + } else if (!bannerAdIsShow) { + bannerAdIsShow = (await banner.show(scene: bannerScene)) == AdCause.success; + Log.v("banner show $bannerScene", tag: "Ads"); + } + } +} + +enum _BannerState { idle, ready, loaded, shown } + +mixin SharedBannerAware on AdsController { + // static final BehaviorSubject<_BannerState> _bannerStateSubject = + // BehaviorSubject.seeded(_BannerState.idle); + static final BehaviorSubject adLoadedSubject = BehaviorSubject.seeded(false); + static StreamSubscription? _bannerVisibilitySubscription; + static AdsDelegate? bannerAds; + static int latestLoadedAt = 0; + + static bool bannerAdIsLoad = false; + static bool bannerAdIsShow = false; + + static Timer? checkTimer; + static Timer? abnormalClickCheckTimer; + static int referenceCount = 0; + + static String currentBannerScene = ""; + + String get bannerScene => ""; + + Stream get loadBannerWhenReady => Stream.value(true); + + StreamSubscription? _lifecycleSubscription; + + static int? latestClickAt; + static int abnormalClick = 0; + + static Future _fetchBanner() async { + bannerAds ??= await AdsManagerDelegate.instance.createBannerAds( + scene: currentBannerScene, + observer: AdsLifecycleObserverDelegate( + onAdLoadedCallback: onBannerAdLoaded, + onAdLoadFailedCallback: onBannerAdLoadFailed, + onAdClickedCallback: onBannerAdClicked)); + return bannerAds!; + } + + bool shouldHide() => !isTopRoute; + + Completer? _internalAdsBannerCompleter; + + Future showInternalBanner() async { + _internalAdsBannerCompleter?.complete(); + _internalAdsBannerCompleter = AdsOverlay.showBanner(); + } + + Future hideInternalBanner() async { + _internalAdsBannerCompleter?.complete(); + _internalAdsBannerCompleter = null; + } + + void initBanner() { + Log.d("[$runtimeType] initBanner! $referenceCount"); + referenceCount++; + currentBannerScene = bannerScene; + addSubscription(Rx.combineLatest3( + AdsManagerDelegate.instance.observableInitialized, + loadBannerWhenReady, + NetworkUtils.observableConnectivityTrack + .map((track) => track.newResult != ConnectivityResult.none), (a, b, c) { + Log.i("[$runtimeType] initialized:$a loadBannerWhenReady:$b connectivity:$c", tag: "Ads"); + return a && b && c; + }).debounceTime(const Duration(seconds: 1)).listen((ready) async { + if (ready) { + _loadBanner(); + } + }, onError: (error, stacktrace) { + Log.e("observable Sdk initialized error!", tag: "Ads", error: error, stackTrace: stacktrace); + })); + } + + static void onBannerAdLoaded(AdsBundle adsBundle) { + adLoadedSubject.addEx(true); + final elapsed = SystemClock.elapsedRealtime().inMilliseconds; + Log.i("onBannerAdLoaded interval: ${elapsed - latestLoadedAt}!", tag: "Ads"); + latestLoadedAt = elapsed; + } + + static void onBannerAdLoadFailed(AdsBundle adsBundle) { + Log.i("onBannerAdLoadFailed !", tag: "Ads"); + } + + static void onBannerAdClicked(AdsBundle adsBundle) { + Log.i("onBannerAdClicked !", tag: "Ads"); + // final now = SystemClock.elapsedRealtime().inMilliseconds; + // final latestAt = (latestClickAt ??= now); + // latestClickAt = now; + // if (now - latestAt > 2) { + // abnormalClick++; + // if (abnormalClick > 3) { + // abnormalClick = 0; + // latestClickAt = null; + // abnormalClickCheckTimer?.cancel(); + // abnormalClickCheckTimer = Timer(duration, () { + // + // }); + // } + // } + } + + static Future _loadBanner() async { + final banner = await _fetchBanner(); + if (AdsManagerDelegate.instance.isPurchasedNoAd) { + return; + } + if (!bannerAdIsLoad) { + Log.v("load banner!!", tag: "Ads"); + bannerAdIsLoad = true; + final result = await banner.load() == AdCause.success; + if (result != bannerAdIsLoad) { + bannerAdIsLoad = result; + } + } + } + + static void registerChecker() { + if (checkTimer?.isActive == true) { + Log.i("already register checker! ignore", tag: "Ads"); + return; + } + final autoDisposeInterval = + (AdsManagerDelegate.instance.getConfig("bannerAutoDisposeInterval") as int?) ?? 5; + Log.i("register checker autoDisposeInterval:$autoDisposeInterval", tag: "Ads"); + checkTimer?.cancel(); + checkTimer = Timer.periodic(Duration(minutes: autoDisposeInterval), (timer) async { + final interval = SystemClock.elapsedRealtime().inMilliseconds - latestLoadedAt; + Log.i("check banner loaded interval: $interval! autoDisposeInterval:$autoDisposeInterval", + tag: "Ads"); + if (interval > (DateTimeUtils.minuteInMillis * autoDisposeInterval)) { + Log.i("banner not response too long, dispose it! $referenceCount", tag: "Ads"); + checkTimer?.cancel(); + checkTimer = null; + _disposeBanner(); + if (referenceCount > 0 && (await NetworkUtils.isNetworkConnected())) { + Log.i("exists reference controller!($referenceCount) reload!}", tag: "Ads"); + _loadBanner(); + } + } + }); + } + + void showBanner() async { + final banner = await _fetchBanner(); + if (!bannerAdIsLoad) { + await _loadBanner(); + Log.i("[$runtimeType] load and show banner!", tag: "Ads"); + } else if (!bannerAdIsShow) { + bannerAdIsShow = (await banner.show(scene: bannerScene)) == AdCause.success; + Log.d("[$runtimeType] banner show $bannerScene", tag: "Ads"); + registerChecker(); + } else { + Log.d("[$runtimeType] banner not show $bannerScene", tag: "Ads"); + } + } + + void hideBanner() async { + Log.i("hideBanner! bannerAdIsShow:$bannerAdIsShow", tag: "Ads"); + if (bannerAdIsShow) { + bannerAds?.hide(); + bannerAdIsShow = false; + } + hideInternalBanner(); + } + + @mustCallSuper + void processResumed() { + _lifecycleSubscription?.cancel(); + final showInternalAdsWhenBannerUnavailable = + (AdsManagerDelegate.instance.getConfig("showInternalAdsWhenBannerUnavailable") as bool?) ?? + false; + _bannerVisibilitySubscription = Rx.combineLatest3>( + loadBannerWhenReady, + adLoadedSubject.stream, + AdsManagerDelegate.instance.observableNoAds, + (ready, loaded, noAds) => Tuple3(ready, loaded, noAds)).listen((tuple) { + final ready = tuple.item1; + final loaded = tuple.item2; + final noAds = tuple.item3; + final canShow = ready && loaded && !noAds; + Log.d("[$runtimeType] SharedBannerAware onResumed! canShow:$canShow showInternalAdsWhenBannerUnavailable:$showInternalAdsWhenBannerUnavailable", tag: "Ads"); + if (canShow) { + showBanner(); + hideInternalBanner(); + } else { + hideBanner(); + if (showInternalAdsWhenBannerUnavailable && !noAds) { + // TODO: 暂时不开放 + // showInternalBanner(); + } + } + }); + } + + @mustCallSuper + void processPaused() { + _bannerVisibilitySubscription?.cancel(); + _bannerVisibilitySubscription = null; + if (shouldHide()) { + hideBanner(); + } else { + Log.d("[$runtimeType] SharedBannerAware onPaused! observableLifecycleInfo", tag: "Ads"); + _lifecycleSubscription = observableLifecycleInfo.listen((event) { + final canHide = shouldHide(); + Log.d("[$runtimeType] SharedBannerAware onPaused! ${event.isResumed()} canHide:$canHide", + tag: "Ads"); + if (!event.isResumed() && canHide) { + hideBanner(); + } + }); + } + } + + static void _disposeBanner() { + bannerAds?.dispose(); + bannerAds = null; + bannerAdIsLoad = false; + bannerAdIsShow = false; + adLoadedSubject.addIfChanged(false); + } + + void disposeBanner() { + referenceCount--; + hideBanner(); + } +} diff --git a/guru_app/packages/guru_utils/lib/controller/aware/ads/interstitial_aware.dart b/guru_app/packages/guru_utils/lib/controller/aware/ads/interstitial_aware.dart new file mode 100644 index 0000000..31507c7 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/controller/aware/ads/interstitial_aware.dart @@ -0,0 +1,162 @@ +import 'dart:async'; + +import 'package:guru_utils/ads/ads_delegate.dart'; +import 'package:guru_utils/ads/ads_manager_delegate.dart'; +import 'package:guru_utils/ads/data/ads_model.dart'; +import 'package:guru_utils/ads/handler/ads_handler.dart'; +import 'package:guru_utils/controller/ads_controller.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/guru_utils.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/property/runtime_property.dart'; + +/// Created by Haoyi on 2022/3/2 + +mixin InterstitialAware on AdsController { + AdsDelegate? interstitialAds; + + AdsLifecycleObserver? _interstitialObserver; + + bool isShowing = false; + + final BehaviorSubject _interstitialAdsLoadedSubject = BehaviorSubject.seeded(false); + + bool get isLoadedInterstitialAds => _interstitialAdsLoadedSubject.value; + + void bindInterstitialAd() async { + addSubscription(AdsManagerDelegate.instance.observableInitialized.listen((ready) async { + if (ready) { + // await _loadInterstitial(); + await _getInterstitial(); + } + }, onError: (error, stacktrace) { + Log.e("observable Sdk initialized error!", tag: "Ads", error: error, stackTrace: stacktrace); + })); + } + + void unbindInterstitialAd() async { + final _observer = _interstitialObserver; + if (_observer != null) { + interstitialAds?.removeObserver(_observer); + } + // 这里并不需要释放,该插屏广告会缓存在内存中快速响应下一次请求 + interstitialAds = null; + } + + Future _getInterstitial() async { + final ads = interstitialAds ?? await AdsManagerDelegate.instance.getInterstitialAds(); + interstitialAds ??= ads; + if (_interstitialObserver == null) { + _interstitialObserver = AdsLifecycleObserverDelegate( + onAdDisplayFailedCallback: _onInterstitialAdDisplayFailed, + onAdHiddenCallback: _onInterstitialAdHidden); + ads.addObserver(_interstitialObserver!); + addSubscription(ads.observableLoaded.listen((loaded) { + Log.d("ads loaded:$loaded"); + _interstitialAdsLoadedSubject.addEx(loaded); + })); + } + return ads; + } + + void _onInterstitialAdDisplayFailed(AdsBundle adsBundle) { + final completer = adsCompleter; + if (completer != null && !completer.isCompleted) { + Log.d("[$runtimeType] _onInterstitialAdDisplayFailed", tag: "Ads"); + completer.error(AdCause.displayFailed); + } + adsCompleter = null; + } + + void _onInterstitialAdHidden(AdsBundle adsBundle) { + final completer = adsCompleter; + if (completer != null && !completer.isCompleted) { + Log.d("[$runtimeType] _onInterstitialAdHidden", tag: "Ads"); + completer.success(); + } + adsCompleter = null; + } + + // Future _loadInterstitial() async { + // final ads = await _getInterstitial(); + // _interstitialObserver = AdsLifecycleObserverDelegate(onAdDisplayFailedCallback: _onInterstitialAdDisplayFailed, onAdHiddenCallback: _onInterstitialAdHidden); + // ads.addObserver(_interstitialObserver!); + // Log.d("load _loadInterstitial!!", tag: "Ads"); + // final status = await ads.getStatus(); + // if (status != AdStatus.LOADING && status != AdStatus.LOADED) { + // await ads.load(); + // } + // } + + Future showInterstitialAd( + {required String scene, bool ignoreNoAds = false, AdsValidator? validator}) async { + const adType = AdType.interstitial; + try { + if (!ignoreNoAds && AdsManagerDelegate.instance.isPurchasedNoAd) { + Log.i("showInterstitial ignore isPurchasedNoAd", tag: "Ads"); + return AdsResult.build(AdType.interstitial, AdCause.noAds); + } + if (adsCompleter?.isCompleted == false) { + Log.e("$runtimeType Ads conflict!", tag: "Ads", syncFirebase: true); + return AdsResult.build(adType, AdCause.conflict); + } + + final adCause = + await AdsManagerDelegate.instance.validateInterstitial(scene, validator: validator); + if (RuntimeProperty.instance.fakeInterstitialAds) { + GuruUtils.showToast("FAKE[$scene]\n INTER Ads has been shown!\n$adCause", + duration: const Duration(seconds: 5)); + return AdsResult.success(AdType.interstitial); + } + + if (adCause == AdCause.success) { + final ads = await _getInterstitial(); + final adCause = await ads.show(scene: scene); + if (adCause == AdCause.success) { + Log.i("showInterstitial ads success!", tag: "Ads"); + adsCompleter = AdsCompleter(adType); + return await adsCompleter!.waiting(); + } else { + Log.i("showInterstitial ads not success! $adCause", tag: "Ads"); + return AdsResult.build(adType, adCause); + } + } else { + Log.i("showInterstitialAd[$scene] error! $adCause", tag: "Ads"); + return AdsResult.build(adType, adCause); + } + } catch (error, stacktrace) { + Log.i("showInterstitialAd exception! ", tag: "Ads", error: error, stackTrace: stacktrace); + return AdsResult.build(AdType.interstitial, AdCause.internalError); + } + } + + Future directShowInterstitialAd({required String scene}) async { + const adType = AdType.interstitial; + try { + if (adsCompleter?.isCompleted == false) { + Log.e("$runtimeType Ads conflict!", tag: "Ads", syncFirebase: true); + return AdsResult.build(adType, AdCause.conflict); + } + + if (RuntimeProperty.instance.fakeInterstitialAds) { + GuruUtils.showToast("FAKE[$scene]\n INTER Ads has been shown!\n", + duration: const Duration(seconds: 5)); + return AdsResult.success(AdType.interstitial); + } + + final ads = await _getInterstitial(); + final adCause = await ads.show(scene: scene, ignoreCheck: true); + if (adCause == AdCause.success) { + Log.i("showInterstitial ads success!", tag: "Ads"); + adsCompleter = AdsCompleter(adType); + return await adsCompleter!.waiting(); + } else { + Log.i("showInterstitial ads not success! $adCause", tag: "Ads"); + return AdsResult.build(adType, adCause); + } + } catch (error, stacktrace) { + Log.i("showInterstitialAd exception! ", tag: "Ads", error: error, stackTrace: stacktrace); + return AdsResult.build(AdType.interstitial, AdCause.internalError); + } + } +} diff --git a/guru_app/packages/guru_utils/lib/controller/aware/ads/overlay/ads_overlay.dart b/guru_app/packages/guru_utils/lib/controller/aware/ads/overlay/ads_overlay.dart new file mode 100644 index 0000000..95f3afc --- /dev/null +++ b/guru_app/packages/guru_utils/lib/controller/aware/ads/overlay/ads_overlay.dart @@ -0,0 +1,13 @@ +import 'dart:async'; + +class AdsOverlay { + static void bind({Completer Function()? showBanner}) { + _showBannerInvoker = showBanner; + } + + static Completer Function()? _showBannerInvoker; + + static Completer? showBanner() { + return _showBannerInvoker?.call(); + } +} diff --git a/guru_app/packages/guru_utils/lib/controller/aware/ads/rewarded_aware.dart b/guru_app/packages/guru_utils/lib/controller/aware/ads/rewarded_aware.dart new file mode 100644 index 0000000..561dcd3 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/controller/aware/ads/rewarded_aware.dart @@ -0,0 +1,249 @@ +import 'dart:async'; + +import 'package:guru_utils/ads/ads_delegate.dart'; +import 'package:guru_utils/ads/ads_manager_delegate.dart'; +import 'package:guru_utils/ads/ads_manager_delegate.dart'; +import 'package:guru_utils/ads/data/ads_model.dart'; +import 'package:guru_utils/ads/handler/ads_handler.dart'; +import 'package:guru_utils/analytics/analytics.dart'; +import 'package:guru_utils/controller/ads_controller.dart'; +import 'package:guru_utils/controller/aware/ads/interstitial_aware.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/guru_utils.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/property/runtime_property.dart'; + +/// Created by Haoyi on 2022/3/24 + +class LoadInterrupter { + bool _interrupted = false; + + bool interrupted() { + return _interrupted; + } + + void interrupt() { + _interrupted = true; + } +} + +mixin RewardedAware on AdsController { + AdsDelegate? rewardedAds; + + AdsLifecycleObserver? _rewardedObserver; + + bool isShowing = false; + + bool _latestUserRewarded = false; + + final BehaviorSubject _rewardedAdsLoadedSubject = BehaviorSubject.seeded(false); + + bool get _allowInterstitialAsAlternativeReward => + (AdsManagerDelegate.instance.getConfig("allowInterstitialAsAlternativeReward") as bool?) ?? + false; + + void bindRewardedAd() async { + addSubscription(AdsManagerDelegate.instance.observableInitialized.listen((ready) async { + if (ready) { + await _getRewarded(); + } + }, onError: (error, stacktrace) { + Log.e("observable Sdk initialized error!", tag: "Ads", error: error, stackTrace: stacktrace); + })); + } + + bool get isLoadedRewardedAds => _rewardedAdsLoadedSubject.value; + + Stream get observableRewardedAdsLoaded => _rewardedAdsLoadedSubject.stream; + + Future checkRewardedAdsIsLoaded() async { + final ads = await _getRewarded(); + return ads.loaded; + } + + void unbindRewardedAd() async { + final _observer = _rewardedObserver; + if (_observer != null) { + rewardedAds?.removeObserver(_observer); + } + // 这里并不需要释放,该插屏广告会缓存在内存中快速响应下一次请求 + rewardedAds = null; + } + + Future _createRewarded() async { + final ads = await AdsManagerDelegate.instance.getRewardsAds(); + _rewardedObserver = AdsLifecycleObserverDelegate( + onAdDisplayFailedCallback: _onRewardedAdDisplayFailed, + onAdDisplayedCallback: _onRewardedAdDisplayed, + onAdHiddenCallback: _onRewardedAdHidden, + onAdRewardedCallback: _onUserRewarded); + ads.addObserver(_rewardedObserver!); + Log.i("bindRewardedAd success! ${ads.hashCode}"); + addSubscription(ads.observableLoaded.listen((loaded) { + Log.d("ads loaded:$loaded"); + _rewardedAdsLoadedSubject.addEx(loaded); + })); + return ads; + } + + Future _getRewarded() async { + return rewardedAds ??= await _createRewarded(); + } + + void _onRewardedAdDisplayFailed(AdsBundle adsBundle) { + final completer = adsCompleter; + if (completer != null && !completer.isCompleted) { + Log.d("[$runtimeType] _onRewardedAdDisplayFailed $_latestUserRewarded", tag: "Ads"); + completer.error(AdCause.displayFailed); + } + adsCompleter = null; + } + + void _onRewardedAdDisplayed(AdsBundle adsBundle) { + Log.d("[$runtimeType] _onRewardedAdDisplayed: invoke clearAutoCompleteFrozenAdsTimer", + tag: "Ads"); + clearAutoCompleteFrozenAdsTimer(); + } + + void _onRewardedAdHidden(AdsBundle adsBundle) { + final completer = adsCompleter; + Log.d("[$runtimeType] _onRewardedAdHidden $_latestUserRewarded ${completer?.isCompleted}", + tag: "Ads"); + if (completer != null && !completer.isCompleted) { + _latestUserRewarded ? completer.success() : completer.error(AdCause.rewardedFailed); + } + adsCompleter = null; + } + + void _onUserRewarded(AdsBundle adsBundle) { + _latestUserRewarded = true; + Log.d("[$runtimeType] _onUserRewarded", tag: "Ads"); + } + + // Future _loadRewarded() async { + // final ads = await _getRewarded(); + // _rewardedObserver = AdsLifecycleObserverDelegate( + // onAdDisplayFailedCallback: _onRewardedAdDisplayFailed, onAdHiddenCallback: _onRewardedAdHidden, onAdRewardedCallback: _onUserRewarded); + // ads.addObserver(_rewardedObserver!); + // Log.d("load _loadRewarded!!", tag: "Ads"); + // final status = await ads.getStatus(); + // if (status != AdStatus.LOADING && status != AdStatus.LOADED) { + // await ads.load(); + // } + // } + + void pollAdsLoaded(AdsDelegate ads, Completer completer, LoadInterrupter interrupter, + {Duration maxDuration = const Duration(seconds: 90), int maxRetry = -1}) async { + const Duration interval = Duration(milliseconds: 5000); + int retry = 0; + + while (!interrupter.interrupted() && + maxDuration.inSeconds > 0 && + (maxRetry < 0 || retry < maxRetry)) { + final state = await ads.getState(); + Log.d("load ads currentStatus:${toAdStatusName(state)}", tag: "Ads"); + if (state != AdState.loaded && state != AdState.loading) { + final adCause = await ads.load(); + if (adCause == AdCause.success) { + Log.d("pollAdsLoaded! $adCause", tag: "Ads"); + } else { + Log.w("load error! $adCause", tag: "Ads"); + completer.complete(AdCause.internalError); + return; + } + } else { + if (state == AdState.loading) { + if (ads.needReset()) { + await ads.reset(); + Log.d("reset complete", tag: "Ads"); + } else { + await Future.delayed(const Duration(seconds: 1)); + } + continue; + } else if (state == AdState.loaded) { + completer.complete(AdCause.success); + return; + } + } + await Future.delayed(interval); + retry++; + maxDuration -= interval; + Log.d("pollAdsLoaded! retry! $retry $maxDuration $interval", tag: "Ads"); + } + Log.d("pollAdsLoaded complete! $retry", tag: "Ads"); + } + + Completer createWaitAdsLoadedCompleter(AdsDelegate ads, + {required LoadInterrupter interrupter}) { + final completer = Completer(); + Log.d("createWaitAdsLoadedCompleter!", tag: "Ads"); + pollAdsLoaded(ads, completer, interrupter); + return completer; + } + + Future isRewardedAdsLoaded() async { + final ads = await _getRewarded(); + return await ads.getState() == AdState.loaded; + } + + Future showRewardedAd( + {required String scene, + bool ignoreNoAds = false, + Future Function(Completer)? loadingDialog}) async { + const adType = AdType.rewarded; + try { + if (adsCompleter?.isCompleted == false) { + Log.e("$runtimeType Ads conflict!", tag: "Ads", syncFirebase: true); + return AdsResult.build(adType, AdCause.conflict); + } + Log.i( + "showRewardedAd[$scene] ignoreNoAds:$ignoreNoAds showLoading:${loadingDialog != null} !", + tag: "Ads"); + AdCause adCause = await AdsManagerDelegate.instance.validateRewards(scene); + if (RuntimeProperty.instance.fakeRewardedAds) { + GuruUtils.showToast("FAKE[$scene]\n REWARDED Ads has been shown!\n$adCause", + duration: const Duration(seconds: 3)); + return AdsResult.success(AdType.rewarded); + } + if (adCause != AdCause.success) { + return AdsResult.build(adType, adCause); + } + + final ads = await _getRewarded(); + if (loadingDialog != null) { + final interrupter = LoadInterrupter(); + final loadCompleter = createWaitAdsLoadedCompleter(ads, interrupter: interrupter); + try { + adCause = await loadingDialog(loadCompleter); + if (adCause == AdCause.canceled) { + AnalyticsUtils.instance.logEventEx("rads_load_cancel", itemCategory: scene); + } + } catch (error, stacktrace) { + Log.w("showAdsLoading error!", error: error, stackTrace: stacktrace, tag: "Ads"); + } + interrupter.interrupt(); + } + + if (adCause == AdCause.success) { + adCause = await ads.show(scene: scene); + if (adCause == AdCause.success) { + Log.i("showRewardedAd ads success!", tag: "Ads"); + adsCompleter = AdsCompleter(adType); + + return await adsCompleter!.waiting(); + } + } + + if ((this is InterstitialAware) && _allowInterstitialAsAlternativeReward) { + final fallbackScene = "${scene}_fallback"; + Log.i("showRewardedAd error! $adCause use alternative ads! [$fallbackScene]", tag: "Ads"); + return (this as InterstitialAware).directShowInterstitialAd(scene: fallbackScene); + } + Log.i("showRewardedAd error! $adCause ", tag: "Ads"); + return AdsResult.build(adType, adCause); + } catch (error, stacktrace) { + Log.i("showRewardedAd exception! ", tag: "Ads", error: error, stackTrace: stacktrace); + return AdsResult.build(AdType.rewarded, AdCause.internalError); + } + } +} diff --git a/guru_app/packages/guru_utils/lib/controller/aware/assets/assets_aware.dart b/guru_app/packages/guru_utils/lib/controller/aware/assets/assets_aware.dart new file mode 100644 index 0000000..43f95a6 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/controller/aware/assets/assets_aware.dart @@ -0,0 +1,64 @@ +// import 'package:guru_app/controller/lifecycle_controller.dart'; +// import 'package:guru_app/financial/asset/assets_store.dart'; +// import 'package:guru_app/financial/iap/iap_manager.dart'; +// import 'package:guru_app/financial/iap/iap_model.dart'; +// import 'package:guru_app/financial/product/product_model.dart'; +// import 'package:guru_app/financial/product/product_store.dart'; +// import 'package:guru_app/guru_app.dart'; +// import 'package:guru_utils/datetime/datetime_utils.dart'; +// import 'package:guru_utils/extensions/extensions.dart'; +// +// /// Created by Haoyi on 2022/4/7 +// +// mixin AssetsAware on LifecycleController { +// final BehaviorSubject> _productStoreSubject = +// BehaviorSubject.seeded(ProductStore()); +// +// ProductStore get currentProductStore => _productStoreSubject.value; +// +// AssetsStore get currentIapAssetStore => IapManager.instance.purchasedStore; +// +// Stream> get observableProductStore => _productStoreSubject.stream; +// +// Stream> get observableIapPurchased => +// IapManager.instance.observablePurchasedStore; +// +// int _latestRefreshIapProductTimestamp = 0; +// +// bool get isIapCanceled => IapManager.instance.latestIapCause == IapCause.canceled; +// +// bool get isIapError => IapManager.instance.latestIapCause == IapCause.error; +// +// Future restorePurchases() async { +// return await IapManager.instance.restorePurchases(); +// } +// +// Future clearIapAssets() async { +// return await IapManager.instance.clearAssetRecord(); +// } +// +// void observeIapProducts(Set intents) { +// addSubscription(IapManager.instance.observableProductDetails.listen((details) async { +// final productStore = await IapManager.instance.buildProducts(intents); +// _productStoreSubject.addEx(productStore); +// })); +// } +// +// void refreshIapProducts() async { +// final now = DateTimeUtils.currentTimeInMillis(); +// if (now - _latestRefreshIapProductTimestamp > DateTimeUtils.minuteInMillis) { +// IapManager.instance.refreshProducts(); +// _latestRefreshIapProductTimestamp = now; +// } else { +// Log.w("refreshIapProducts Too Frequency!", tag: "IAP"); +// } +// } +// +// Future requestProduct(Product product, {String from = ""}) async { +// if (product is IapProduct) { +// return await IapManager.instance.buy(product); +// } else { +// return false; +// } +// } +// } diff --git a/guru_app/packages/guru_utils/lib/controller/aware/controller_aware.dart b/guru_app/packages/guru_utils/lib/controller/aware/controller_aware.dart new file mode 100644 index 0000000..04cf5c7 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/controller/aware/controller_aware.dart @@ -0,0 +1,5 @@ +/// Created by Haoyi on 2023/2/9 + +export 'ads/banner_aware.dart'; +export 'ads/interstitial_aware.dart'; +export 'ads/rewarded_aware.dart'; \ No newline at end of file diff --git a/guru_app/packages/guru_utils/lib/controller/aware/keep_screen_on_aware.dart b/guru_app/packages/guru_utils/lib/controller/aware/keep_screen_on_aware.dart new file mode 100644 index 0000000..9ff2efa --- /dev/null +++ b/guru_app/packages/guru_utils/lib/controller/aware/keep_screen_on_aware.dart @@ -0,0 +1,108 @@ +import 'dart:async'; + +import 'package:get/get.dart'; +import 'package:guru_platform_data/guru_platform_data.dart'; +import 'package:guru_utils/controller/lifecycle_controller.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/settings/settings.dart'; +import 'package:system_clock/system_clock.dart'; + +mixin KeepScreenOnAware on LifecycleController { + static bool isKeepScreenOn = false; + + static Timer? checkKeepScreenTimer; + + static int keepingAt = 0; + + static int sustainedDuration = 0; + + String get screenName => runtimeType.toString(); + + static bool _scheduleTimer(int duration) { + final interval = SystemClock.elapsedRealtime().inMilliseconds - keepingAt; + final remaining = duration - interval; + Log.d("schedule timer interval: $interval, remaining: $remaining"); + if (remaining < 1000) { + checkKeepScreenTimer?.cancel(); + checkKeepScreenTimer = null; + // 如果这里的 Duration 是大于等于 0 的,表示已经超时或是配置了关闭 + if (duration < 0) { + Log.d("Always keepScreenOn, no need to set a timer"); + return true; + } + _restoreKeepScreenStatus(); + return false; + } else { + Log.d("schedule new timer remaining: $remaining"); + checkKeepScreenTimer = Timer(Duration(milliseconds: remaining), () { + _scheduleTimer(duration); + }); + return true; + } + } + + // true: 表示已经成功注册了屏幕长亮的定时器 + // false: 在注册定时器时,发现已经没有必要注册了,因为已经超过了屏幕长亮的时间 + static bool keepAlive() { + final duration = Settings.get().keepOnScreenDuration.get(); + if ((sustainedDuration == duration) && (duration < 0 || checkKeepScreenTimer != null)) { + Log.i("already registerKeepingTimer same timer:$sustainedDuration, ignore!"); + return true; + } + + final result = _scheduleTimer(duration); + if (result) { + sustainedDuration = duration; + } + return result; + } + + void _keepScreenOn() async { + keep(info: "_keepScreenOn $isKeepScreenOn"); + final keeping = keepAlive(); + if (!keeping) { + Log.i("_keepScreenOn failed! already over duration: $sustainedDuration"); + return; + } + if (!isKeepScreenOn) { + isKeepScreenOn = await GuruPlatformData.keepScreenOn(true); + } + } + + static void _restoreKeepScreenStatus() async { + if (isKeepScreenOn) { + await GuruPlatformData.keepScreenOn(false); + isKeepScreenOn = false; + } + Log.d("_restoreKeepScreenStatus $isKeepScreenOn"); + } + + void initKeepScreenOn() { + keep(info: "initKeepScreenOn"); + } + + void keep({String? info}) { + final elapsed = SystemClock.elapsedRealtime().inMilliseconds; + Log.d("[$screenName] KEEP${info != null ? "($info)" : ""} interval: ${elapsed - keepingAt}"); + keepingAt = elapsed; + } + + @override + void onResumed() { + super.onResumed(); + _keepScreenOn(); + } + + @override + void onPaused() { + if (Get.isDialogOpen != true) { + _restoreKeepScreenStatus(); + } + super.onPaused(); + } + + @override + void dispose() { + super.dispose(); + } +} diff --git a/guru_app/packages/guru_utils/lib/controller/base_controller.dart b/guru_app/packages/guru_utils/lib/controller/base_controller.dart new file mode 100644 index 0000000..1faceed --- /dev/null +++ b/guru_app/packages/guru_utils/lib/controller/base_controller.dart @@ -0,0 +1,62 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:get/get.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/timer/timer_scheduler.dart'; + +/// Created by @Haoyi on 2022/1/19 + +class BaseController extends GetxController { + final CompositeSubscription subscriptions = CompositeSubscription(); + + final TimerScheduler timerScheduler = TimerScheduler(); + + BaseController(); + + void addSubscription(StreamSubscription? subscription) { + if (subscription != null && !subscriptions.isDisposed) { + subscriptions.add(subscription); + } + } + + void disposeSubscriptions() { + if (!subscriptions.isDisposed) { + subscriptions.dispose(); + } + } + + ActiveTimer delayed(Duration duration, VoidCallback callback) { + return timerScheduler.delayed(duration, callback); + } + + ActiveTimer periodic(Duration duration, VoidCallback callback) { + return timerScheduler.periodic(duration, callback); + } + + void removeTimer(ActiveTimer? timer) { + if (timer != null) { + timerScheduler.removeTimer(timer); + } + } + + @override + @mustCallSuper + void onClose() { + disposeSubscriptions(); + timerScheduler.disposeAll(); + super.onClose(); + } + + @override + @mustCallSuper + void onReady() { + super.onReady(); + } + + @override + @mustCallSuper + void onInit() { + super.onInit(); + } +} diff --git a/guru_app/packages/guru_utils/lib/controller/controller.dart b/guru_app/packages/guru_utils/lib/controller/controller.dart new file mode 100644 index 0000000..d59a603 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/controller/controller.dart @@ -0,0 +1,4 @@ +/// Created by Haoyi on 2023/2/9 + +export 'lifecycle_controller.dart'; +export 'ads_controller.dart'; \ No newline at end of file diff --git a/guru_app/packages/guru_utils/lib/controller/gems_controller.dart b/guru_app/packages/guru_utils/lib/controller/gems_controller.dart new file mode 100644 index 0000000..2574cd7 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/controller/gems_controller.dart @@ -0,0 +1,61 @@ +import 'dart:math'; + +import 'package:get/get.dart' hide Rx; + +import 'dart:ui' as ui show Image; + +/// Created by Haoyi on 2022/7/16 +/// +/// +// abstract class GemsController extends AdsController +// with AssetsAware, InterstitialAware, RewardedAware, VisualFeastAware { +// Future loadGemsResource(VisualFeastEngine engine) async { +// final imageFutures = [ +// Flame.images.load("ic_gem.png"), +// Flame.images.load("ic_gem_add.png"), +// ]; +// final loadedResources = await Future.wait([ +// Future.wait(imageFutures), +// // Future.wait(lottieFutures) +// ]); +// final images = loadedResources[0] as List; +// addSprite("gem", VisualFeastSprite.fromImage(images[0])); +// addSprite("gemAdd", VisualFeastSprite.fromImage(images[1])); +// } +// +// void startClaim(int gems, String method, {bool useBg = true, VoidCallback? onCompleted}) async { +// final engine = createEngine(onCompleted: onCompleted); +// await loadGemsResource(engine); +// +// final designSpec = GemsRewardsDesignSpec.get(); +// final gemsBarSpec = designSpec.buildGemBarSpec(); +// final gemsBar = GemsBar( +// gemBarSpec: gemsBarSpec, +// gemSprite: getSprite("gem"), +// gemAddSprite: getSprite("gemAdd"), +// assetsAware: this); +// final size = designSpec.measuredSize / 2; +// final gemsReward = GemsReward( +// Rect.fromCenter( +// center: Offset(size.width, size.height + gemsBarSpec.gemRect.width * 2), +// width: gemsBarSpec.gemRect.width, +// height: gemsBarSpec.gemRect.width), onFirstGemComplete: () { +// claimGems(gems, method); +// }); +// final background = Background(gemsBarSpec); +// final gemsHeight = gemsBarSpec.gemRect.width; +// final gemsText = GemsText(gems, Offset(size.width, size.height), +// Offset(size.width, size.height - gemsHeight * 2)); +// engine.attachRenders( +// ListUtils.filterOutNulls([useBg ? background : null, gemsBar, gemsReward, gemsText])); +// +// dispatch(engine); +// } +// +// Future claimGems(int gems, String method) async { +// onClaimed(gems, method); +// } +// +// void onClaimed(int gems, String method) { +// } +// } diff --git a/guru_app/packages/guru_utils/lib/controller/lifecycle_controller.dart b/guru_app/packages/guru_utils/lib/controller/lifecycle_controller.dart new file mode 100644 index 0000000..c972adc --- /dev/null +++ b/guru_app/packages/guru_utils/lib/controller/lifecycle_controller.dart @@ -0,0 +1,290 @@ +import 'dart:collection'; + +import 'package:flutter/material.dart'; +import 'package:get/get_core/src/get_main.dart'; +import 'package:get/get_navigation/src/extension_navigation.dart'; +import 'package:get/get_navigation/src/routes/observers/route_observer.dart'; +import 'package:guru_utils/controller/aware/keep_screen_on_aware.dart'; +import 'package:guru_utils/lifecycle/lifecycle_manager.dart'; +import 'package:guru_utils/router/router.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/tuple/tuple.dart'; +import 'base_controller.dart'; +import 'package:guru_utils/aigc/bi/ai_bi.dart'; + +/// Created by Haoyi on 2022/1/30 +/// + +enum LifecycleState { create, paused, resumed } + +class LifecycleEvent { + const LifecycleEvent(); +} + +enum LifecycleActionResult { consumed, suspend, failed } + +typedef LifecycleInvoker = Future Function(); + +class LifecycleAction { + final LifecycleInvoker invoker; + + LifecycleAction({required this.invoker}); + + Future invoke() async { + try { + return await invoker(); + } catch (error, stacktrace) { + Log.w("invoke error! $error"); + } + return LifecycleActionResult.failed; + } +} + +class RouteInfo { + final bool topRoute; + final Routing? routing; + + const RouteInfo(this.topRoute, {this.routing}); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is RouteInfo && + runtimeType == other.runtimeType && + topRoute == other.topRoute && + routing == other.routing; + + @override + int get hashCode => topRoute.hashCode ^ routing.hashCode; +} + +class LifecycleInfo { + final LifecycleSnapshot snapshot; + final bool topRoute; + + LifecycleState get state => snapshot.state; + + bool get foreground => snapshot.foreground; + + Routing get routing => snapshot.routing; + + const LifecycleInfo(this.snapshot, this.topRoute); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is LifecycleInfo && + runtimeType == other.runtimeType && + snapshot == other.snapshot && + topRoute == other.topRoute; + + @override + int get hashCode => snapshot.hashCode ^ topRoute.hashCode; + + bool isResumed() { + return snapshot.state == LifecycleState.resumed && snapshot.foreground && topRoute; + } + + @override + String toString() { + return 'LifecycleInfo{snapshot: $snapshot, topRoute: $topRoute}'; + } +} + +abstract class LifecycleController extends BaseController with LifecycleObserver { + bool _dispatching = false; + bool _paused = true; + final DoubleLinkedQueue actions = DoubleLinkedQueue(); + + late Uri routeUri; + final BehaviorSubject lifecycleState = + BehaviorSubject.seeded(LifecycleState.create); + + final BehaviorSubject lifecycleInfo = + BehaviorSubject.seeded(LifecycleInfo(LifecycleSnapshot.invalid, false)); + + bool get isResumed => currentLifecycleInfo.isResumed(); + + LifecycleInfo get currentLifecycleInfo => lifecycleInfo.value; + + bool get isTopRoute => _checkIsTopRoute(); + + // Stream get observableLifecycleState => lifecycleState.stream; + + Stream get observableLifecycleInfo => lifecycleInfo.stream; + + bool _checkIsTopRoute() { + try { + final top = buildCurrentRouteUri(); + return top.path == routeUri.path; + } catch (error) { + return false; + } + } + + Uri buildCurrentRouteUri() { + final route = Get.rawRoute; + if (route != null) { + return Uri.parse("${route.settings.name}"); + } + + return Uri.parse(Get.currentRoute); + } + + Route? get rawRoute => Get.rawRoute; + + Future waitingResume() { + if (isResumed) { + return Future.value(true); + } + Log.w("waiting resume...$currentLifecycleInfo", tag: "Game"); + return observableLifecycleInfo.firstWhere((info) { + Log.d("waiting resume check:$info $isResumed"); + return isResumed; + }).then((value) => true); + } + + void _refreshLifecycle(LifecycleInfo info) { + final latestInfo = currentLifecycleInfo; + + Log.i( + "[$runtimeType] refresh lifecycle! $isTopRoute $isResumed ${info.routing.current} $latestInfo => $info"); + + lifecycleInfo.addIfChanged(info); + if (!latestInfo.foreground && info.foreground) { + onAppForeground(); + AiBi.instance.applife(true); + } + if (!latestInfo.isResumed() && info.isResumed()) { + _dispatchResumed(); + AiBi.instance.lifecycle(runtimeType.toString().replaceAll("Controller", "Page"), true); + } else if (latestInfo.isResumed() && !info.isResumed()) { + _dispatchPaused(); + AiBi.instance.lifecycle(runtimeType.toString().replaceAll("Controller", "Page"), false); + } + if (latestInfo.foreground && !info.foreground) { + onAppBackground(); + AiBi.instance.applife(false); + } + } + + @override + @mustCallSuper + void onInit() { + super.onInit(); + addSubscription(LifecycleManager.instance.observableLifecycleEvent.listen((event) { + onLifecycleEvent(event); + })); + if (this is KeepScreenOnAware) { + (this as KeepScreenOnAware).initKeepScreenOn(); + } + } + + @override + void onReady() { + super.onReady(); + LifecycleManager.instance.addLifecycleObserver(this); + routeUri = buildCurrentRouteUri(); + Log.d("[$runtimeType] onReady:$_paused $routeUri"); + delayed(LifecycleManager.verifyLifecycleDuration, () { + final snapshot = LifecycleManager.instance.currentLifecycleSnapshot; + final info = LifecycleInfo(snapshot, _checkIsTopRoute()); + final changed = info != currentLifecycleInfo; + Log.d("[$runtimeType] onReady delayed: $_paused $snapshot $routeUri $changed"); + if (_paused || changed) { + _refreshLifecycle(info); + } + }); + } + + void postAction(LifecycleAction action) { + actions.add(action); + if (isResumed) { + _dispatchLifecycleAction(); + } + } + + void postActionDelayed(Duration duration, LifecycleAction action) { + delayed(duration, () { + postAction(action); + }); + } + + void onLifecycleEvent(LifecycleEvent event) {} + + @override + void onLifecycleChanged(LifecycleSnapshot snapshot) { + Log.d("[$runtimeType] onLifecycleChanged:$snapshot"); + _refreshLifecycle(LifecycleInfo(snapshot, _checkIsTopRoute())); + } + + @mustCallSuper + @override + void onClose() { + Log.d("[$runtimeType] onClose:$_paused"); + LifecycleManager.instance.removeLifecycleObserver(this); + if (!_paused) { + _dispatchPaused(); + } + super.onClose(); + } + + // + // @mustCallSuper + // @override + // void didChangeAppLifecycleState(AppLifecycleState state) { + // Log.d("[$runtimeType] didChangeAppLifecycleState:$state"); + // if (state == AppLifecycleState.resumed) { + // lifecycleState.addEx(LifecycleState.resumed); + // } else { + // lifecycleState.addEx(LifecycleState.paused); + // } + // } + + void _dispatchLifecycleAction() async { + if (!_dispatching) { + _dispatching = true; + while (actions.isNotEmpty && isResumed) { + final action = actions.removeFirst(); + try { + final result = await action.invoke(); + if (result == LifecycleActionResult.suspend) { + actions.addLast(action); + } + } catch (error, stacktrace) { + Log.e("_dispatchLifecycleAction invoke error! $error $stacktrace"); + } + } + _dispatching = false; + } + } + + void _dispatchResumed() { + _paused = false; + onResumed(); + + _dispatchLifecycleAction(); + } + + void _dispatchPaused() { + _paused = true; + onPaused(); + } + + void onResumed() { + Log.d("LifecycleChanged: $runtimeType onResumed!"); + } + + void onPaused() { + Log.d("LifecycleChanged: $runtimeType onPaused!"); + } + + void onAppBackground() { + Log.d("LifecycleChanged: $runtimeType onAppBackground!"); + } + + void onAppForeground() { + Log.d("LifecycleChanged: $runtimeType onAppForeground!"); + } +} diff --git a/guru_app/packages/guru_utils/lib/converts/color_convert.dart b/guru_app/packages/guru_utils/lib/converts/color_convert.dart new file mode 100644 index 0000000..251194e --- /dev/null +++ b/guru_app/packages/guru_utils/lib/converts/color_convert.dart @@ -0,0 +1,23 @@ +/// Created by Haoyi on 4/28/21 + +part of "converts.dart"; + +class ColorStringConvert implements JsonConverter { + final Color defaultColor; + + const ColorStringConvert({required this.defaultColor}); + + @override + Color fromJson(String json) { + try { + return ColorUtils.toColor(json); + } catch (error) { + return defaultColor; + } + } + + @override + String toJson(Color color) { + return "#${color.value.toRadixString(16)}"; + } +} diff --git a/guru_app/packages/guru_utils/lib/converts/config_map_convert.dart b/guru_app/packages/guru_utils/lib/converts/config_map_convert.dart new file mode 100644 index 0000000..42ab63a --- /dev/null +++ b/guru_app/packages/guru_utils/lib/converts/config_map_convert.dart @@ -0,0 +1,32 @@ +/// Created by Haoyi on 2022/7/20 +part of "converts.dart"; + +class ConfigStringIntMapStringConvert implements JsonConverter, String> { + const ConfigStringIntMapStringConvert(); + + @override + Map fromJson(String json) { + final entries = json.split(";"); + final Map result = {}; + for (String itemStr in entries) { + try { + final item = itemStr.split(":"); + if (item.length == 2) { + result[item[0]] = int.parse(item[1]); + } + } catch (error, stacktrace) { + Log.w("parse ConfigMap error", error: error); + } + } + return result; + } + + @override + String toJson(Map object) { + String result = ""; + for (var entry in object.entries) { + result += "${entry.key}:${entry.value};"; + } + return result; + } +} diff --git a/guru_app/packages/guru_utils/lib/converts/converts.dart b/guru_app/packages/guru_utils/lib/converts/converts.dart new file mode 100644 index 0000000..ad177e5 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/converts/converts.dart @@ -0,0 +1,60 @@ +import 'dart:convert'; +import 'dart:ui'; +import 'package:guru_utils/colors/color_utils.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:json_annotation/json_annotation.dart'; + +/// Created by Haoyi on 4/28/21 + +part "color_convert.dart"; + +part "list_joined_string_convert.dart"; + +part "config_map_convert.dart"; + +const joinedStringConvert = JoinedStringListConvert(); +const configStringIntMapStringConvert = ConfigStringIntMapStringConvert(); +const IntStringConvert intStringConvert = IntStringConvert(); +const BoolStringConvert boolStringConvert = BoolStringConvert(); + +class IntStringConvert implements JsonConverter { + const IntStringConvert(); + + @override + int fromJson(String? json) { + if (json == null) { + return -1; + } + try { + return int.parse(json); + } catch (error) { + return -1; + } + } + + @override + String toJson(int? value) { + return value.toString(); + } +} + +class BoolStringConvert implements JsonConverter { + const BoolStringConvert(); + + @override + bool fromJson(String? json) { + if (json == null) { + return false; + } + try { + return json == "true" || json == "ok"; + } catch (error) { + return false; + } + } + + @override + String toJson(bool? value) { + return (value != null && value == true) ? "true" : "false"; + } +} diff --git a/guru_app/packages/guru_utils/lib/converts/list_joined_string_convert.dart b/guru_app/packages/guru_utils/lib/converts/list_joined_string_convert.dart new file mode 100644 index 0000000..b6a0e58 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/converts/list_joined_string_convert.dart @@ -0,0 +1,21 @@ +/// Created by Haoyi on 2022/4/27 + +part of "converts.dart"; + +class JoinedStringListConvert implements JsonConverter, String> { + const JoinedStringListConvert(); + + @override + List fromJson(String joinedStr) { + if (joinedStr.isEmpty) { + return []; + } + final segments = joinedStr.split('|'); + return segments.where((segment) => segment.isNotEmpty).toList(); + } + + @override + String toJson(List data) { + return data.join('|'); + } +} diff --git a/guru_app/packages/guru_utils/lib/core/ext.dart b/guru_app/packages/guru_utils/lib/core/ext.dart new file mode 100644 index 0000000..1188c4a --- /dev/null +++ b/guru_app/packages/guru_utils/lib/core/ext.dart @@ -0,0 +1,19 @@ +import 'package:dartx/dartx.dart'; + +/// Created by Haoyi on 2022/5/30 + +class DartExt { + static String adjustBlank(String? check, String defValue) { + if (isBlank(check)) { + return defValue; + } + return check!; + } + + static bool isBlank(String? check) => check == null || check == ''; + + static bool isNotBlank(String? check) => !isBlank(check); + + static String capitalize(String text) => text.capitalize(); + +} diff --git a/guru_app/packages/guru_utils/lib/currency/currency_utils.dart b/guru_app/packages/guru_utils/lib/currency/currency_utils.dart new file mode 100644 index 0000000..cbcb7fc --- /dev/null +++ b/guru_app/packages/guru_utils/lib/currency/currency_utils.dart @@ -0,0 +1,19 @@ +import 'package:intl/intl.dart'; + +/// Created by Haoyi on 2020/6/9 + +class CurrencyUtils { + static final coinsFormatter = NumberFormat.currency(locale: "en_US", symbol: "", decimalDigits: 0); + + static String formatCoins(num price) { + return coinsFormatter.format(price); + } + + static String toW(num price) { + if (price < 10000) { + return formatCoins(price); + } + var numberFormat = NumberFormat.compact(locale: "en_US"); + return numberFormat.format(price); + } +} diff --git a/guru_app/packages/guru_utils/lib/database/batch/batch_aware.dart b/guru_app/packages/guru_utils/lib/database/batch/batch_aware.dart new file mode 100644 index 0000000..c43903a --- /dev/null +++ b/guru_app/packages/guru_utils/lib/database/batch/batch_aware.dart @@ -0,0 +1,73 @@ +import 'package:guru_utils/id/identifiable.dart'; +import 'package:rxdart/rxdart.dart'; + +import 'batch_data.dart'; + +/// Created by @Haoyi on 2021/7/25 +/// + +mixin BatchAware { + final BehaviorSubject> _subject = BehaviorSubject.seeded({}); + + Stream> get observableData => _subject.stream; + + T? getData(String key) => _subject.value[key]; + + List get dataList => _subject.value.values.toList() ?? []; + + bool get isDataEmpty => _subject.value.isEmpty; + + Map get allData => _subject.value; + + void touch() { + _subject.add(_subject.value); + } + + void processBatchData(BatchData batchData) async { + for (var action in batchData.getActions()) { + switch (action.method) { + case BatchMethod.insert: + case BatchMethod.update: + case BatchMethod.select: + final changedData = Map.from(_subject.value); + for (var entity in action.collection) { + changedData[entity.id] = entity; + } + _subject.add(changedData); + break; + case BatchMethod.delete: + final changedData = Map.from(_subject.value); + bool changed = false; + for (var entity in action.collection) { + if (changedData.containsKey(entity.id)) { + changedData.remove(entity.id); + changed = true; + } + } + if (changed) { + _subject.add(changedData); + } + break; + case BatchMethod.clear: + _subject.add({}); + break; + case BatchMethod.remove: + final copiedData = Map.from(_subject.value); + final reservedData = {}; + for (var entity in copiedData.values) { + if (action.where?.call(entity) == false) { + reservedData[entity.id] = entity; + } + } + _subject.add(reservedData); + break; + default: + break; + } + } + } + + void disposeBatch() { + _subject.close(); + } +} diff --git a/guru_app/packages/guru_utils/lib/database/batch/batch_data.dart b/guru_app/packages/guru_utils/lib/database/batch/batch_data.dart new file mode 100644 index 0000000..d4d9dcd --- /dev/null +++ b/guru_app/packages/guru_utils/lib/database/batch/batch_data.dart @@ -0,0 +1,234 @@ +/// Created by Haoyi on 2021/6/4 +/// + +enum BatchMethod { insert, update, delete, select, clear, remove, replace, error } +enum BatchState { ignore, success, error } + +const ALL_METHODS = [ + BatchMethod.insert, + BatchMethod.update, + BatchMethod.delete, + BatchMethod.select, + BatchMethod.clear, + BatchMethod.remove, + BatchMethod.replace, + BatchMethod.error +]; + +class BatchResult { + final BatchState state; + final dynamic cause; + + const BatchResult.success() + : state = BatchState.success, + cause = null; + + const BatchResult.error({this.cause}) : this.state = BatchState.error; + + const BatchResult.ignore({this.cause}) : this.state = BatchState.ignore; +} + +const BatchResultSuccess = BatchResult.success(); + +class BatchAction { + final BatchMethod method; + final List collection = []; + final BatchResult result; + final bool Function(T)? where; + final T Function(T)? updater; + + get length => collection.length; + + BatchAction(this.method, + {List? data, this.result = BatchResultSuccess, this.where, this.updater}) { + if (data?.isNotEmpty == true) { + collection.addAll(data!); + } + } + + void append(T? data) { + if (data != null) { + collection.add(data); + } + } + + void appendAll(List data) { + collection.addAll(data); + } +} + +class BatchData { + final List> actions = []; + + BatchData.error(dynamic cause) { + _append(BatchMethod.error, null, result: BatchResult.error(cause: cause)); + } + + BatchData.singleSuccess(BatchMethod method, T data) { + _append(method, data); + } + + BatchData.singleError(BatchMethod method, T data, BatchResult result) { + _append(method, data, result: result); + } + + BatchData(BatchMethod method, List data, {BatchResult result = BatchResultSuccess}) { + _appendAll(method, data, result: result); + } + + BatchData.empty(); + + void _append(BatchMethod method, T? data, {BatchResult result = BatchResultSuccess}) { + final batchAction = actions.isNotEmpty ? actions.last : null; + if (batchAction != null && + batchAction.method == method && + batchAction.result.state == result.state) { + batchAction.append(data); + } else { + actions.add(BatchAction(method, data: (data != null) ? [data] : null, result: result)); + } + } + + void _appendAll(BatchMethod method, List data, {BatchResult result = BatchResultSuccess}) { + final batchAction = actions.isNotEmpty ? actions.last : null; + if (batchAction != null && + batchAction.method == method && + batchAction.result.state == result.state) { + batchAction.appendAll(data); + } else { + actions.add(BatchAction(method, data: data, result: result)); + } + } + + void insert(T data, {BatchResult result = BatchResultSuccess}) { + _append(BatchMethod.insert, data, result: result); + } + + void insertAll(List data, {BatchResult result = BatchResultSuccess}) { + _appendAll(BatchMethod.insert, data, result: result); + } + + void update(T data, {BatchResult result = BatchResultSuccess}) { + _append(BatchMethod.update, data, result: result); + } + + void updateAll(List data, {BatchResult result = BatchResultSuccess}) { + _appendAll(BatchMethod.update, data, result: result); + } + + void replace( + {bool Function(T)? where, T Function(T)? updater, BatchResult result = BatchResultSuccess}) { + actions.add(BatchAction(BatchMethod.replace, + data: [], + result: BatchResultSuccess, + where: where ?? (_) => true, + updater: updater ?? (r) => r)); + } + + void delete(T data, {BatchResult result = BatchResultSuccess}) { + _append(BatchMethod.delete, data, result: result); + } + + void deleteAll(List data, {BatchResult result = BatchResultSuccess}) { + _appendAll(BatchMethod.delete, data, result: result); + } + + void query(T data, {BatchResult result = BatchResultSuccess}) { + _append(BatchMethod.select, data, result: result); + } + + void queryAll(List data, {BatchResult result = BatchResultSuccess}) { + _appendAll(BatchMethod.select, data, result: result); + } + + void clear() { + _append(BatchMethod.clear, null, result: BatchResultSuccess); + } + + void removeWhere({bool Function(T)? where}) { + actions.add(BatchAction(BatchMethod.remove, + data: [], result: BatchResultSuccess, where: where ?? (_) => true)); + } + + bool containsMethodResult(BatchMethod method, List state) { + if (isNotEmpty) { + for (var action in actions) { + if (action.method == method && state.contains(action.result.state)) { + return true; + } + } + } + return false; + } + + bool get isEmpty => actions.isEmpty; + + bool get isNotEmpty => actions.isNotEmpty; + + int get length => isEmpty + ? 0 + : actions.map((action) => action.length).reduce((value, element) => element + value); + + bool get hasError => isEmpty + ? false + : actions.where((action) => action.result.state == BatchState.error).isNotEmpty; + + bool get hasInsertSuccess => containsMethodResult(BatchMethod.insert, [BatchState.success]); + + bool get hasInsertError => containsMethodResult(BatchMethod.insert, [BatchState.error]); + + bool get hasUpdateSuccess => containsMethodResult(BatchMethod.update, [BatchState.success]); + + bool get hasUpdateError => containsMethodResult(BatchMethod.update, [BatchState.error]); + + bool get hasDeleteSuccess => containsMethodResult(BatchMethod.delete, [BatchState.success]); + + bool get hasDeleteError => containsMethodResult(BatchMethod.delete, [BatchState.error]); + + int size(List methods, {BatchResult? result}) => isEmpty + ? 0 + : actions + .where((action) { + final exists = methods.contains(action.method); + return (exists && result != null) ? action.result.state == result.state : exists; + }) + .map((action) => action.length) + .reduce((value, element) => element + value); + + List> getActions({List methods = ALL_METHODS, BatchResult? result}) => + isEmpty + ? [] + : actions.where((action) { + final exists = methods.contains(action.method); + return (exists && result != null) ? action.result.state == result.state : exists; + }).toList(); + + T? first({List methods = ALL_METHODS, BatchResult? result}) { + for (var action in actions) { + final exists = methods.contains(action.method); + if ((exists && result != null) ? action.result.state == result.state : exists) { + for (var data in action.collection) { + return data; + } + } + } + return null; + } + + List data({List methods = ALL_METHODS, BatchResult? result}) => isEmpty + ? [] + : actions + .where((action) { + final exists = methods.contains(action.method); + return (exists && result != null) ? action.result.state == result.state : exists; + }) + .map((action) => action.collection) + .reduce((data, collection) { + final List result = []; + if (data.isNotEmpty == true) { + result.addAll(data); + } + result.addAll(collection); + return result; + }); +} diff --git a/guru_app/packages/guru_utils/lib/database/database.dart b/guru_app/packages/guru_utils/lib/database/database.dart new file mode 100644 index 0000000..ad680e5 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/database/database.dart @@ -0,0 +1,94 @@ +import 'dart:io'; + +import 'package:guru_utils/log/log.dart'; +import 'package:path/path.dart'; +import 'package:sqflite/sqflite.dart'; +export 'batch/batch_data.dart'; +export 'batch/batch_aware.dart'; +export 'package:sqflite/sqflite.dart'; + +/// Created by Haoyi on 2022/8/24 +/// +part 'migration.dart'; + + +typedef TableCreator = Future Function(Transaction delegate); + +abstract class AppDatabase { + late Database _database; + + List get tableCreators; + + List get migrations; + + int get version; + + String get dbName; + + Future getTestingPath() async { + return "./mocks_dir/db"; + } + + Future _createTables(Database db) { + return db.transaction((Transaction delegate) async { + final List creators = tableCreators; + for (var creator in creators) { + await creator(delegate); + } + }); + } + + Future _migrate(Database database, int from, int to) async { + Log.d("MIGRATE [$from] => [$to]"); + final _migrations = migrations; + return database.transaction((txn) async { + for (int index = from - 1; (index < to - 1) && (index < _migrations.length); ++index) { + var result = MigrateResult.failed; + try { + Log.d("====> BEGIN MIGRATE [${index + 1}] => [${index + 2}]"); + result = await _migrations[index].migrate(txn); + } catch (error) { + Log.d(" migrate [${index + 1}] => [${index + 2}] error:[$error]"); + rethrow; + } + Log.d("====> END MIGRATE [${index + 1}] => [${index + 2}] result: $result"); + } + }); + } + + Future initDatabase() async { + final isTesting = Platform.environment.containsKey('FLUTTER_TEST'); + final dbDir = isTesting ? await getTestingPath() : await getDatabasesPath(); + final dbPath = join(dbDir, "$dbName.db"); + Log.d("dbPath:$dbPath"); + _database = await openDatabase( + dbPath, + version: version, + onCreate: (Database db, int version) async { + // When creating the db, create the table + try { + await _createTables(db); + } catch (error, stacktrace) { + Log.w("createTables error! $error", stackTrace: stacktrace); + } + }, + onUpgrade: (Database db, int oldVersion, int newVersion) async { + await _migrate(db, oldVersion, newVersion); + }, + ); + } + + Database getDb() { + return _database; + } + + Future runInTransaction(Future Function(Transaction txn) action) { + return getDb().transaction(action); + } +} + +extension DatabaseExt on AppDatabase { + String joinTextValue(List value, [String separator = ""]) { + return value.map((str) => "'$str'").toList().join(separator); + } +} diff --git a/guru_app/packages/guru_utils/lib/database/migration.dart b/guru_app/packages/guru_utils/lib/database/migration.dart new file mode 100644 index 0000000..316b404 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/database/migration.dart @@ -0,0 +1,10 @@ +/// Created by @Haoyi on 2020/5/22 +/// + +part of "database.dart"; + +enum MigrateResult { success, failed } + +abstract class Migration { + Future migrate(Transaction transaction); +} diff --git a/guru_app/packages/guru_utils/lib/datetime/date/date.dart b/guru_app/packages/guru_utils/lib/datetime/date/date.dart new file mode 100644 index 0000000..45ba26d --- /dev/null +++ b/guru_app/packages/guru_utils/lib/datetime/date/date.dart @@ -0,0 +1,116 @@ +import 'package:guru_utils/hash/hash.dart'; + +import '../datetime_utils.dart'; + +/// Created by @Haoyi on 2021/8/5 + +class Date { + int get year => _datetime.year; + + int get month => _datetime.month; + + int get day => _datetime.day; + + int get weekday => _datetime.weekday; + + String get yyyyMMdd => DateTimeUtils.yyyyMMddDateFormat.format(_datetime); + + String get yyyyMM => "${_datetime.year}${DateTimeUtils.twoDigits(_datetime.month)}"; + + int get millisecondsSinceEpoch => _datetime.millisecondsSinceEpoch; + + int get yyyyMMddNumber => year * 10000 + month * 100 + day; + + final DateTime _datetime; + + Date._(DateTime dateTime) : _datetime = DateTime(dateTime.year, dateTime.month, dateTime.day); + + Date.fromDateTime(DateTime dateTime) + : _datetime = DateTime(dateTime.year, dateTime.month, dateTime.day); + + Date(int year, [int month = 1, int day = 1]) : _datetime = DateTime(year, month, day); + + Date.fromDateNumber(int yyyyMMdd) : _datetime = DateTimeUtils.createDateTimeFromDateNum(yyyyMMdd); + + static Date fromMillisecondsSinceEpoch(int millisecondsSinceEpoch) { + final dateTime = DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch); + return Date._(dateTime); + } + + DateTime toDateTime() => _datetime; + + static Date today() { + return Date._(DateTime.now()); + } + + static Date tomorrow() { + final now = DateTime.now(); + return Date._(DateTime(now.year, now.month, now.day).add(const Duration(days: 1))); + } + + static Date yesterday() { + final now = DateTime.now(); + return Date._(DateTime(now.year, now.month, now.day).subtract(const Duration(days: 1))); + } + + Date nextDay() { + return Date._(_datetime.add(const Duration(days: 1))); + } + + Date prevDay() { + return Date._(_datetime.subtract(const Duration(days: 1))); + } + + bool isSame(Date date) { + return year == date.year && month == date.month && day == date.day; + } + + bool isAfter(Date date) { + return _datetime.isAfter(date._datetime); + } + + bool isBefore(Date date) { + return _datetime.isBefore(date._datetime); + } + + Date add({int years = 0, int months = 0, int days = 0}) { + return Date._(DateTime(_datetime.year + years, _datetime.month + months, _datetime.day + days)); + } + + Date subtract({int years = 0, int months = 0, int days = 0}) { + return Date._(DateTime(_datetime.year - years, _datetime.month - months, _datetime.day - days)); + } + + Duration difference(Date other) { + return _datetime.difference(other._datetime); + } + + Date clamp(Date lowerLimit, Date upperLimit) { + DateTime dt = _datetime; + + if (dt.isBefore(lowerLimit._datetime)) { + dt = lowerLimit._datetime; + } + if (dt.isAfter(upperLimit._datetime)) { + dt = upperLimit._datetime; + } + return Date._(dt); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Date && runtimeType == other.runtimeType && _datetime == other._datetime; + + @override + int get hashCode => hash3(year, month, day); + + int differenceDays(Date date) { + return _datetime.difference(date._datetime).inDays; + } + + @override + String toString() { + return DateTimeUtils.yyyyMMddBuild(_datetime); + } +} diff --git a/guru_app/packages/guru_utils/lib/datetime/datespan/datespan.dart b/guru_app/packages/guru_utils/lib/datetime/datespan/datespan.dart new file mode 100644 index 0000000..9351e4a --- /dev/null +++ b/guru_app/packages/guru_utils/lib/datetime/datespan/datespan.dart @@ -0,0 +1,133 @@ +import 'package:guru_utils/datetime/date/date.dart'; + +/// Created by @Haoyi on 2021/8/2 +/// + +enum PeriodUnit { NONE, DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY } + +// [start, end] +class DateSpan { + final Date start; + final Date end; + + DateSpan(this.start, this.end); + + int get differenceDays => end.differenceDays(start); + + DateSpan.empty() + : start = Date(0), + end = Date(0); + + DateSpan.forever() + : start = Date(0), + end = Date((1 << 32) - 1); + + static DateSpan latest7Days() { + final today = Date.today(); + return DateSpan(today.subtract(days: 7), today); + } + + static DateSpan latest14Days() { + final today = Date.today(); + return DateSpan(today.subtract(days: 14), today); + } + + static DateSpan latest30Days() { + final today = Date.today(); + return DateSpan(today.subtract(days: 30), today); + } + + @override + String toString() { + return '$start - $end}'; + } +} + +class DatePeriod extends Iterable { + final DateSpan span; + + final PeriodUnit unit; + + const DatePeriod.create(this.span, this.unit); + + @override + Iterator get iterator => throw UnimplementedError(); +} + +class DailyDateSpanIterator extends Iterator { + final DateSpan dateSpan; + + DateSpan? _current; + + DailyDateSpanIterator(this.dateSpan); + + @override + DateSpan get current => _current!; + + @override + bool moveNext() { + final _start = + _current?.end ?? Date(dateSpan.start.year, dateSpan.start.month, dateSpan.start.day); + if (_start.isAfter(dateSpan.end)) { + _current = null; + return false; + } + final _end = _start.add(days: 1); + + _current = DateSpan(_start, _end); + return true; + } +} + +class WeeklyDateSpanIterator extends Iterator { + final DateSpan dateSpan; + + DateSpan? _current; + + WeeklyDateSpanIterator(this.dateSpan); + + @override + DateSpan get current => _current!; + + @override + bool moveNext() { + // final dt = DateTime(dateTime.year, dateTime.month, dateTime.day); + + final _start = _current?.end ?? Date(dateSpan.start.year, dateSpan.start.month, 1); + // final weeklyStart = dt.subtract(Duration(days: dateTime.weekday - 1)); + // final weeklyEnd = dt.add(Duration(days: 7 - dateTime.weekday)); + + if (_start.isAfter(dateSpan.end)) { + _current = null; + return false; + } + final _end = Date(dateSpan.start.year, dateSpan.start.month + 1, 1); + + _current = DateSpan(_start, _end); + return true; + } +} + +class MonthlyDateSpanIterator extends Iterator { + final DateSpan dateSpan; + + DateSpan? _current; + + MonthlyDateSpanIterator(this.dateSpan); + + @override + DateSpan get current => _current!; + + @override + bool moveNext() { + final _start = _current?.end ?? Date(dateSpan.start.year, dateSpan.start.month, 1); + if (_start.isAfter(dateSpan.end)) { + _current = null; + return false; + } + final _end = Date(dateSpan.start.year, dateSpan.start.month + 1, 1); + + _current = DateSpan(_start, _end); + return true; + } +} diff --git a/guru_app/packages/guru_utils/lib/datetime/datetime_utils.dart b/guru_app/packages/guru_utils/lib/datetime/datetime_utils.dart new file mode 100644 index 0000000..ada3560 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/datetime/datetime_utils.dart @@ -0,0 +1,653 @@ +import 'dart:math'; + +import 'package:flutter/cupertino.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/number/number_utils.dart'; +import 'package:intl/intl.dart'; + +/// Created by @Haoyi on 2020/5/20 +/// +/// + +enum TimeUnit { milliseconds, seconds, minutes, hour, day, month, year, infinite } + +/// Signature for a function that creates a widget for a given `day`. +typedef DayBuilder = Widget? Function(BuildContext context, DateTime day); + +/// Signature for a function that creates a widget for a given `day`. +/// Additionally, contains the currently focused day. +typedef FocusedDayBuilder = Widget? Function( + BuildContext context, DateTime day, DateTime focusedDay); + +/// Signature for a function returning text that can be localized and formatted with `DateFormat`. +typedef TextFormatter = String Function(DateTime date, dynamic locale); + +/// Gestures available for the calendar. +enum AvailableGestures { none, verticalSwipe, horizontalSwipe, all } + +/// Formats that the calendar can display. +enum CalendarFormat { month, twoWeeks, week } + +/// Days of the week that the calendar can start with. +enum DayOfWeek { + monday, + tuesday, + wednesday, + thursday, + friday, + saturday, + sunday, +} + +final daysFormat = RegExp(r"#d{([^#]*)}"); +final hoursFormat = RegExp(r"#h{([^#]*)}"); +final minutesFormat = RegExp(r"#m{([^#]*)}"); +final secondsFormat = RegExp(r"#s{([^#]*)}"); +final millisecondsFormat = RegExp(r"#ms{([^#]*)}"); + +/// Days in a month. This array uses 1-based month numbers, i.e. January is +/// the 1-st element in the array, not the 0-th. +const _daysInMonth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + +class DateTimeUtils { + static final yyyyMMddDateFormat = DateFormat("yyyyMMdd"); + static final yyMMddDateFormat = DateFormat("yyMMdd"); + static final yyyyMMNormalDateFormat = DateFormat("yyyy-MM"); + static final yyyyMMddNormalDateFormat = DateFormat("yyyy-MM-dd"); + static final MMddNormalDateFormat = DateFormat("MM.dd"); + static final standardDateFormat = DateFormat("yyyy-MM-dd HH:mm:ss"); + + static final yyyyMMDateFormat = DateFormat("yyyyMM"); + static final MMMDateFormat = DateFormat("MMM"); + static final monthAbbreviatedFormat = DateFormat("MMM"); + + static final hamanDateFormat = DateFormat.yMd().add_jm(); + + static final standardTimeFormat = DateFormat("HH:mm:ss"); + + static const secondInMillis = 1000; + static const minuteInMillis = secondInMillis * 60; + static const hourInMillis = secondInMillis * 3600; + static const sixHourInMillis = hourInMillis * 6; + static const quarterOfHourInMillis = minuteInMillis * 15; + static const halfHourInMillis = minuteInMillis * 30; + static const dayInMillis = hourInMillis * 24; + static const weekInMillis = dayInMillis * 7; + + static const minuteInSecond = 60; + static const hourInSecond = 3600; + static const quarterOfHourInSecond = 900; + static const halfHourInSecond = 1800; + + static String get yyyyMMdd => yyyyMMddDateFormat.format(DateTime.now()); + + static String get yyMMdd => yyMMddDateFormat.format(DateTime.now()); + + static String get yyyyMM => yyyyMMDateFormat.format(DateTime.now()); + + static String get MMM => MMMDateFormat.format(DateTime.now()); + + static String get humanDatetime => hamanDateFormat.format(DateTime.now()); + + static String get humanTime => standardTimeFormat.format(DateTime.now()); + + static int get yyyyMMddUtcNum { + final now = DateTime.now().toUtc(); + return now.year * 10000 + now.month * 100 + now.day; + } + + static int get yearMonthNum { + final now = DateTime.now(); + return generateYearMonthNum(now); + } + + static String yyyyMMddBuild(DateTime dateTime) { + // return yyyyMMddDateFormat.format(dateTime); + return "${dateTime.year}${NumberUtils.twoDigits(dateTime.month)}${NumberUtils.twoDigits( + dateTime.day)}"; + } + + static String yyyyMMBuild(DateTime dateTime) { + return "${dateTime.year}${NumberUtils.twoDigits(dateTime.month)}"; + // return yyyyMMDateFormat.format(dateTime); + } + + static int generateYearMonthNum(DateTime dateTime) { + return dateTime.year * 100 + dateTime.month; + } + + static int generateYearMonthDayNum(DateTime dateTime) { + return dateTime.year * 10000 + dateTime.month * 100 + dateTime.day; + } + + static int yyyyMMddStr2Num(String yyyyMMdd) { + return int.parse(yyyyMMdd); + } + + static String yyyyMMddStr2yyyyMMStr(String yyyyMMdd) { + return yyyyMMdd.substring(0, 6); + } + + static int yyyyMMddStr2yyyyMMNum(String yyyyMMdd) { + return int.parse(yyyyMMddStr2yyyyMMStr(yyyyMMdd)); + } + + static String formatMMdd(DateTime dateTime) { + return "${NumberUtils.twoDigits(dateTime.month)}${NumberUtils.twoDigits(dateTime.day)}"; + // return yyyyMMDateFormat.format(dateTime); + } + + // static String yyyyMMddNormalBuild(DateTime dateTime) { + // return yyyyMMddNormalDateFormat.format(dateTime); + // } + + static int millis2second(int millis) { + return millis ~/ Duration.millisecondsPerSecond; + } + + static int minutes2millis(int minutes) { + return minutes * minuteInMillis; + } + + static int minutes2seconds(int minutes) { + return minutes * minuteInSecond; + } + + static int hour2millis(int hours) { + return hours * hourInMillis; + } + + static int hour2seconds(int hours) { + return hours * hourInSecond; + } + + static DateTime createDateTimeFromDateNum(int yyyyMMdd) { + final year = yyyyMMdd ~/ 10000; + final month = (yyyyMMdd - (year * 10000)) ~/ 100; + final day = yyyyMMdd - (year * 10000 + month * 100); + return DateTime(year, month, day); + } + + static DateTime createDateTimeFromYearMonth(int yyyyMM) { + final year = yyyyMM ~/ 100; + final month = yyyyMM - (year * 100); + return DateTime(year, month, 1); + } + + static List buildDailyDateTimeGroup() { + final now = DateTime.now(); + final result = []; + for (int year = now.year; year >= 2020; year--) { + int month = (year != now.year) ? 12 : now.month; + int minMonth = year == 2020 ? 5 : 1; + for (; month >= minMonth; month--) { + result.add(DateTime(year, month)); + } + } + return result; + } + + static int currentTimeInSecond() { + return DateTime + .now() + .millisecondsSinceEpoch + .toDouble() ~/ Duration.millisecondsPerSecond; + } + + static int currentTimeInMillis() { + return DateTime + .now() + .millisecondsSinceEpoch; + } + + static int currentTimeInMicros() { + return DateTime + .now() + .microsecondsSinceEpoch; + } + + static int toSecondSinceEpoch(DateTime dateTime) { + return dateTime.millisecondsSinceEpoch ~/ 1000; + } + + + static String abbrevDayOfWeek(DateTime dateTime) { + switch (dateTime.weekday) { + case DateTime.monday: + return "M"; + case DateTime.tuesday: + case DateTime.thursday: + return "T"; + case DateTime.wednesday: + return "W"; + case DateTime.friday: + return "F"; + default: + return "S"; + } + } + + static Duration getDuration(DateTime start, DateTime to) { + return Duration(milliseconds: to.millisecondsSinceEpoch - start.millisecondsSinceEpoch); + } + + static Duration remainingDuration() { + final now = DateTime.now(); + final nextDay = now.add(Duration(days: 1)); + return getDuration(now, DateTime(nextDay.year, nextDay.month, nextDay.day)); + } + + static Duration getDurationForMinutes(int startMinutes, int endMinutes) { + return Duration(minutes: endMinutes - startMinutes); + } + + static Duration getDurationForMillis(int start, int end) { + return Duration(milliseconds: end - start); + } + + static String formatMilliseconds(int milliseconds) { + String twoDigitHour = ""; + String twoDigitMinutes = NumberUtils.twoDigits( + (milliseconds ~/ Duration.millisecondsPerMinute).remainder(Duration.minutesPerHour)); + String twoDigitSeconds = NumberUtils.twoDigits( + (milliseconds ~/ Duration.millisecondsPerSecond).remainder(Duration.secondsPerMinute)); + if (milliseconds > DateTimeUtils.hourInMillis) { + twoDigitHour = NumberUtils.twoDigits((milliseconds ~/ Duration.millisecondsPerHour)); + return "$twoDigitHour:$twoDigitMinutes:$twoDigitSeconds"; + } + return "$twoDigitMinutes:$twoDigitSeconds"; + } + + static String formatDurationShort(int milliseconds) { + final int hour = (milliseconds ~/ Duration.microsecondsPerHour); + int minute = + (milliseconds ~/ Duration.millisecondsPerMinute).remainder(Duration.minutesPerHour); + int second = + (milliseconds ~/ Duration.millisecondsPerSecond).remainder(Duration.secondsPerMinute); + int millis = milliseconds.remainder(Duration.millisecondsPerSecond); + + if (hour > 0) { + return "${hour.format(2)}:${minute.format(2)}:${second.format(2)}.${millis.format(3)}"; + } else if (minute > 0) { + return "${minute.format(2)} m ${second.format(2)}.${millis.format(3)} s"; + } else { + return "${second.format(2)}.${millis.format(3)} s"; + } + } + + static String formatSeconds(int seconds) { + return formatMilliseconds(seconds * 1000); + } + + static String formatMillisecondsToStandard(int milliseconds) { + return milliseconds == 0 + ? "00:00:00" + : standardDateFormat.format(DateTime.fromMillisecondsSinceEpoch(milliseconds)); + } + + static String formatLooseDuration(Duration? duration, {TimeUnit minUnit = TimeUnit.day}) { + if (duration == null) { + return "-- : -- : --"; + } + + if (duration.inMicroseconds < 1000) { + return "00 : 00 : 00"; + } + String twoDigitMinutes = + NumberUtils.twoDigits(duration.inMinutes.remainder(Duration.minutesPerHour)); + String twoDigitSeconds = + NumberUtils.twoDigits(duration.inSeconds.remainder(Duration.secondsPerMinute)); + if (duration.inHours <= 0) { + if (minUnit.index >= TimeUnit.hour.index) { + return "00 : $twoDigitMinutes : $twoDigitSeconds"; + } else { + return "$twoDigitMinutes : $twoDigitSeconds"; + } + } else { + return "${NumberUtils.twoDigits(duration.inHours)} : $twoDigitMinutes : $twoDigitSeconds"; + } + } + + static String formatDuration(Duration? duration, + {bool verbose = false, TimeUnit minUnit = TimeUnit.day}) { + if (duration == null) { + return "--:--:--"; + } + + if (duration.inMicroseconds < 1000) { + if (verbose != true) { + return "00:00:00"; + } else { + return "00 hr 00 min 00 sec"; + } + } + if (duration.inDays > 1) { + String twoDigitHour = NumberUtils.twoDigits(duration.inHours.remainder(Duration.hoursPerDay)); + return "${duration.inDays}d ${twoDigitHour}h"; + } + + String twoDigitMinutes = + NumberUtils.twoDigits(duration.inMinutes.remainder(Duration.minutesPerHour)); + String twoDigitSeconds = + NumberUtils.twoDigits(duration.inSeconds.remainder(Duration.secondsPerMinute)); + if (verbose != true) { + if (duration.inHours <= 0) { + if (minUnit.index >= TimeUnit.hour.index) { + return "00:$twoDigitMinutes:$twoDigitSeconds"; + } else { + return "$twoDigitMinutes:$twoDigitSeconds"; + } + } else { + return "${NumberUtils.twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds"; + } + } else { + if (duration.inHours <= 0) { + return "$twoDigitMinutes min $twoDigitSeconds sec"; + } else if (duration.inHours < 10) { + return "${NumberUtils.twoDigits( + duration.inHours)} hr $twoDigitMinutes min $twoDigitSeconds sec"; + } else { + return "${duration.inHours} hr $twoDigitMinutes min $twoDigitSeconds sec"; + } + } + } + + // {dd} : inDays + // {hh} : inHours + // {mm} : inMinutes + // {ss} : inSeconds + // {ms} : inMillis remainder per seconds + static String formatDurationV2(Duration? duration, + {String format = "", bool eliminateRedundancy = true}) { + if (duration == null || duration.inMilliseconds < 1000) { + return "---"; + } + + int days = duration.inDays > 0 ? duration.inDays : 0; + int hours = duration.inHours.remainder(Duration.hoursPerDay); + int minutes = duration.inMinutes.remainder(Duration.minutesPerHour); + int seconds = duration.inSeconds.remainder(Duration.secondsPerMinute); + int millis = duration.inMilliseconds.remainder(Duration.millisecondsPerSecond); + + bool canIgnore = eliminateRedundancy; + String result = format; + final daysMatches = daysFormat.allMatches(result); + if (daysMatches.isNotEmpty) { + for (var match in daysMatches) { + final pattern = match.group(0); + if (pattern != null) { + final group = match.groupCount > 0 ? match.group(1) : ""; + if (days > 0) { + result = result.replaceAll(pattern, "$days$group"); + canIgnore = false; + } else if (canIgnore) { + result = result.replaceAll(pattern, ""); + } else { + result = result.replaceAll(pattern, "00$group"); + } + } + } + } else { + hours += days * Duration.hoursPerDay; + } + + final hoursMatches = hoursFormat.allMatches(result); + if (hoursMatches.isNotEmpty) { + for (var match in hoursMatches) { + final pattern = match.group(0); + if (pattern != null) { + final group = match.groupCount > 0 ? match.group(1) : ""; + if (hours > 0) { + result = result.replaceAll(pattern, "${hours.format(2)}$group"); + canIgnore = false; + } else if (canIgnore) { + result = result.replaceAll(pattern, ""); + } else { + result = result.replaceAll(pattern, "00$group"); + } + } + } + } else { + minutes += hours * Duration.minutesPerHour; + } + + final minutesMatches = minutesFormat.allMatches(result); + if (minutesMatches.isNotEmpty) { + for (var match in minutesMatches) { + final pattern = match.group(0); + if (pattern != null) { + final group = match.groupCount > 0 ? match.group(1) : ""; + if (minutes > 0) { + result = result.replaceAll(pattern, "${minutes.format(2)}$group"); + canIgnore = false; + } else if (canIgnore) { + result = result.replaceAll(pattern, ""); + } else { + result = result.replaceAll(pattern, "00$group"); + } + } + } + } else { + seconds += minutes * Duration.secondsPerMinute; + } + + final secondsMatches = secondsFormat.allMatches(result); + if (secondsMatches.isNotEmpty) { + for (var match in secondsMatches) { + final pattern = match.group(0); + if (pattern != null) { + final group = match.groupCount > 0 ? match.group(1) : ""; + if (seconds > 0) { + result = result.replaceAll(pattern, "${seconds.format(2)}$group"); + canIgnore = false; + } else if (canIgnore) { + result = result.replaceAll(pattern, ""); + } else { + result = result.replaceAll(pattern, "00$group"); + } + } + } + } else { + millis += minutes * Duration.secondsPerMinute; + } + + final millisecondsMatches = millisecondsFormat.allMatches(result); + if (millisecondsMatches.isNotEmpty) { + for (var match in millisecondsMatches) { + final pattern = match.group(0); + if (pattern != null) { + final group = match.groupCount > 0 ? match.group(1) : ""; + if (millis > 0) { + result = result.replaceAll(pattern, "${millis.format(3)}$group"); + canIgnore = false; + } else if (canIgnore) { + result = result.replaceAll(pattern, ""); + } else { + result = result.replaceAll(pattern, "00$group"); + } + } + } + } else { + millis += minutes * Duration.secondsPerMinute; + } + + return result; + } + + static List getYearList(int start, [int? end]) { + final now = DateTime.now(); + final endYear = (end == null || end < start) ? now.year : end; + final startYear = min(start, endYear); + + return List.generate(endYear - startYear + 1, (year) => year + start); + } + + static List generateDaysInMonth(int year, int month) { + final count = daysInMonth(year, month); + final List result = []; + for (int day = 1; day <= count; ++day) { + result.add(DateTime(year, month, day)); + } + return result; + } + + static Week currentDaysInWeek() { + return Week.current(); + } + + static String twoDigits(int n) { + if (n >= 10) return "$n"; + return "0$n"; + } + + static int getDayCountOfMonth(DateTime dateTime) { + final year = dateTime.year; + final month = dateTime.month; + return DateTime(year, month + 1, 0).day; + } + + /// Returns a numerical value associated with given `weekday`. + /// + /// Returns 1 for `DayOfWeek.monday`, all the way to 7 for `DayOfWeek.sunday`. + static int getWeekdayNumber(DayOfWeek weekday) { + return DayOfWeek.values.indexOf(weekday) + 1; + } + + /// Returns `date` in UTC format, without its time part. + static DateTime normalizeDate(DateTime date) { + return DateTime.utc(date.year, date.month, date.day); + } + + /// Checks if two DateTime objects are the same day. + /// Returns `false` if either of them is null. + static bool isSameDay(DateTime? a, DateTime? b) { + if (a == null || b == null) { + return false; + } + + return a.year == b.year && a.month == b.month && a.day == b.day; + } + + static bool isSameMonth(DateTime? a, DateTime? b) { + if (a == null || b == null) { + return false; + } + return a.year == b.year && a.month == b.month; + } + + static int getHashCode(DateTime key) { + return key.day * 1000000 + key.month * 10000 + key.year; + } + + static int getRowCount(DateTime focusedDay) { + final first = _firstDayOfMonth(focusedDay); + final daysBefore = getDaysBefore(first); + final firstToDisplay = first.subtract(Duration(days: daysBefore)); + + final last = lastDayOfMonth(focusedDay); + final daysAfter = getDaysAfter(last); + final lastToDisplay = last.add(Duration(days: daysAfter)); + + return (lastToDisplay + .difference(firstToDisplay) + .inDays + 1) ~/ 7; + } + + static DateTime _firstDayOfMonth(DateTime month) { + return DateTime.utc(month.year, month.month, 1); + } + + static int dayOfYear(DateTime dateTime) { + final _first = + dateTime.isUtc ? DateTime.utc(dateTime.year, 1, 1) : DateTime(dateTime.year, 1, 1); + return dateTime + .difference(_first) + .inDays; + } + + static int getDaysBefore(DateTime firstDay) { + return (firstDay.weekday + 7 - getWeekdayNumber(DayOfWeek.sunday)) % 7; + } + + static DateTime lastDayOfMonth(DateTime month) { + final date = month.month < 12 + ? DateTime.utc(month.year, month.month + 1, 1) + : DateTime.utc(month.year + 1, 1, 1); + return date.subtract(const Duration(days: 1)); + } + + static int getDaysAfter(DateTime lastDay) { + int invertedStartingWeekday = 8 - getWeekdayNumber(DayOfWeek.sunday); + + int daysAfter = 7 - ((lastDay.weekday + invertedStartingWeekday) % 7); + if (daysAfter == 7) { + daysAfter = 0; + } + + return daysAfter; + } + + /// Returns the number of days in the specified month. + /// + /// This function assumes the use of the Gregorian calendar or the proleptic + /// Gregorian calendar. + static int daysInMonth(int year, int month) => + (month == DateTime.february && isLeapYear(year)) ? 29 : _daysInMonth[month]; + + /// Returns true if [year] is a leap year. + /// + /// This implements the Gregorian calendar leap year rules wherein a year is + /// considered to be a leap year if it is divisible by 4, excepting years + /// divisible by 100, but including years divisible by 400. + /// + /// This function assumes the use of the Gregorian calendar or the proleptic + /// Gregorian calendar. + static bool isLeapYear(int year) => (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0)); + + /// Takes a [date] that may be outside the allowed range of dates for a given + /// [month] in a given [year] and returns the closest date that is within the + /// allowed range. + /// + /// For example: + /// + /// February 31, 2013 => February 28, 2013 + /// + /// When jumping from month to month or from leap year to common year we may + /// end up in a month that has fewer days than the month we are jumping from. + /// In that case it is impossible to preserve the exact date. So we "clamp" the + /// date value to fit within the month. For example, jumping from March 31 one + /// month back takes us to February 28 (or 29 during a leap year), as February + /// doesn't have 31-st date. + static int clampDayOfMonth({ + required int year, + required int month, + required int day, + }) => + day.clamp(1, daysInMonth(year, month)); +} + +class DailyMonthGroup { + final DateTime dateTime; + final int year; + final int month; + + DailyMonthGroup(this.dateTime, this.year, this.month); +} + +class Week { + final List date; + + Week.begin(DateTime first) + : date = + List.generate(7, (index) => first.add(Duration(days: index))).toList(growable: false); + + Week.end(DateTime first) + : date = List.generate(7, (index) => first.subtract(Duration(days: index))) + .toList(growable: false); + + static Week current({int offset = 0}) { + final now = DateTime.now(); + final first = now.subtract(Duration(days: now.weekday - 1 + offset)); + return Week.begin(first); + } +} diff --git a/guru_app/packages/guru_utils/lib/device/device_info.dart b/guru_app/packages/guru_utils/lib/device/device_info.dart new file mode 100644 index 0000000..5ba92fb --- /dev/null +++ b/guru_app/packages/guru_utils/lib/device/device_info.dart @@ -0,0 +1,245 @@ +import 'package:flutter/widgets.dart'; +import 'package:guru_utils/device/device_utils.dart'; +import 'package:guru_utils/log/log.dart'; + +import 'package:json_annotation/json_annotation.dart'; + +/// Created by Haoyi on 2020/7/31 +/// + +part 'device_info.g.dart'; + +class LocaleInfo { + final String languageCode; + final String script; + final String countryCode; + + const LocaleInfo(this.languageCode, this.script, this.countryCode); + + String get fullTag => "$languageCode-$script-$countryCode"; + + String get languageAndCountry => "$languageCode-$countryCode"; +} + +// 上报用户信息 +@JsonSerializable() +class DeviceInfo { + @JsonKey(defaultValue: "") + final String androidId; + + @JsonKey(defaultValue: "") + final String appIdentifier; + + @JsonKey(defaultValue: "") + final String appVersion; + + @JsonKey(defaultValue: "") + final String brand; + + @JsonKey(defaultValue: "") + final String deviceCountry; + + @JsonKey(defaultValue: "") + final String deviceId; + + @JsonKey(defaultValue: "") + final String deviceType; + + @JsonKey(defaultValue: "") + final String language; + + @JsonKey(defaultValue: "") + final String locale; + + @JsonKey(defaultValue: "") + final String model; + + // 后端的,不用管 + // final String pushDeviceType; + + @JsonKey(defaultValue: true) + final bool pushNotificationEnable = true; + + // pushNotifications + + @JsonKey(name: "pushType", defaultValue: "FCM") + final String pushType; + + @JsonKey(defaultValue: "") + final String timezone; + + @JsonKey(defaultValue: "") + String uid; + + @JsonKey(defaultValue: "") + String deviceToken; + + DeviceInfo({ + required this.appIdentifier, + required this.appVersion, + required this.brand, + required this.deviceCountry, + required this.deviceType, + required this.language, + required this.locale, + required this.model, + required this.timezone, + required this.deviceId, + this.pushType = 'FCM', + this.androidId = '', + this.uid = '', + this.deviceToken = '', + }); + + static bool equals(DeviceInfo? deviceInfo1, DeviceInfo? deviceInfo2) { + if (deviceInfo1 != null && deviceInfo2 != null) { + return (deviceInfo1.appVersion == deviceInfo2.appVersion) && + (deviceInfo1.uid == deviceInfo2.uid) && + (deviceInfo1.deviceCountry == deviceInfo2.deviceCountry) && + (deviceInfo1.language == deviceInfo2.language) && + (deviceInfo1.locale == deviceInfo2.locale) && + (deviceInfo1.timezone == deviceInfo2.timezone) && + (deviceInfo1.deviceToken == deviceInfo2.deviceToken) && + (deviceInfo1.androidId == deviceInfo2.androidId) && + (deviceInfo1.deviceId == deviceInfo2.deviceId) && + (deviceInfo1.appIdentifier == deviceInfo2.appIdentifier) && + (deviceInfo1.brand == deviceInfo2.brand) && + (deviceInfo1.deviceType == deviceInfo2.deviceType) && + (deviceInfo1.model == deviceInfo2.model) && + (deviceInfo1.pushType == deviceInfo2.pushType); + } else { + return deviceInfo1 == deviceInfo2; + } + } + + bool get isValid => deviceToken.isNotEmpty && uid.isNotEmpty; + + factory DeviceInfo.fromJson(Map json) => _$DeviceInfoFromJson(json); + + Map toJson() => _$DeviceInfoToJson(this); + + void dumpDevice({String msg = "default"}) { + Log.d("=============={$msg}=============="); + Log.d(" [androidId]: $androidId"); + Log.d(" [appIdentifier]: $appIdentifier"); + Log.d(" [appVersion]: $appVersion"); + Log.d(" [brand]: $brand"); + Log.d(" [deviceCountry]: $deviceCountry"); + Log.d(" [deviceId]: $deviceId"); + Log.d(" [deviceType]: $deviceType"); + Log.d(" [language]: $language"); + Log.d(" [model]: $model"); + Log.d(" [pushNotificationEnable]: $pushNotificationEnable"); + Log.d(" [pushType]: $pushType"); + Log.d(" [timezone]: $timezone"); + Log.d(" [uid]: $uid"); + Log.d(" [deviceToken]: $deviceToken"); + Log.d("====================================="); + } + + @override + String toString() { + return 'DeviceInfo{androidId: $androidId, appIdentifier: $appIdentifier, appVersion: $appVersion, brand: $brand, deviceCountry: $deviceCountry, deviceId: $deviceId, deviceType: $deviceType, language: $language, locale: $locale, model: $model, pushNotificationEnable: $pushNotificationEnable, pushType: $pushType, timezone: $timezone, uid: $uid, deviceToken: $deviceToken}'; + } + + String toXDeviceInfo() { + return "appIdentifier=$appIdentifier;appVersion=$appVersion;deviceType=$deviceType;deviceCountry=$deviceCountry;appCountry=${DeviceUtils.buildLocaleInfo().countryCode};local=$locale;language=$language;timezone=$timezone;brand=$brand;model=$model;androidId=$androidId"; + } +} + +// // 收到 push 上报打点 +// @JsonSerializable() +// class PushEventReqBody { +// // yyyy-MM-ddTHH:mm:ssZ +// @JsonKey(defaultValue: "") +// final String appEventTime; +// +// // 本机数据 +// final DeviceInfo deviceData; +// +// //See PushEventType +// @JsonKey(defaultValue: "") +// final String eventType; +// +// // 从 push data 中获取 +// @JsonKey(defaultValue: "") +// final String pushEventId; +// +// @JsonKey(defaultValue: "") +// final String serverPushTime; +// +// @JsonKey(defaultValue: "") +// final String taskName; +// +// PushEventReqBody( +// {this.appEventTime, +// this.deviceData, +// this.eventType, +// this.pushEventId, +// this.serverPushTime, +// this.taskName}); +// +// factory PushEventReqBody.fromJson(Map json) => _$PushEventReqBodyFromJson(json); +// +// Map toJson() => _$PushEventReqBodyToJson(this); +// } + +class DeviceTrack { + final DeviceInfo? newDevice; + final DeviceInfo? oldDevice; + + DeviceTrack(this.newDevice, this.oldDevice); + + DeviceInfo? get device => newDevice ?? oldDevice; + + bool get isChanged => !DeviceInfo.equals(newDevice, oldDevice); + +// void dump() { +// if (EventLogger.dumpLog) { +// print("========== DeviceTrack =========="); +// print(" [New][appVersion] ${newDevice?.appVersion}"); +// print(" [Old][appVersion] ${oldDevice?.appVersion}"); +// print(" >>>> diff result: ${newDevice?.appVersion == oldDevice?.appVersion}"); +// print(" [New][uid] ${newDevice?.uid}"); +// print(" [Old][uid] ${oldDevice?.uid}"); +// print(" >>>> diff result: ${newDevice?.uid == oldDevice?.uid}"); +// print(" [New][deviceCountry] ${newDevice?.deviceCountry}"); +// print(" [Old][deviceCountry] ${oldDevice?.deviceCountry}"); +// print(" >>>> diff result: ${newDevice?.deviceCountry == oldDevice?.deviceCountry}"); +// print(" [New][language] ${newDevice?.language}"); +// print(" [Old][language] ${oldDevice?.language}"); +// print(" >>>> diff result: ${newDevice?.language == oldDevice?.language}"); +// print(" [New][locale] ${newDevice?.locale}"); +// print(" [Old][locale] ${oldDevice?.locale}"); +// print(" >>>> diff result: ${newDevice?.locale == oldDevice?.locale}"); +// print(" [New][timezone] ${newDevice?.timezone}"); +// print(" [Old][timezone] ${oldDevice?.timezone}"); +// print(" >>>> diff result: ${newDevice?.timezone == oldDevice?.timezone}"); +// print(" [New][deviceToken] ${newDevice?.deviceToken}"); +// print(" [Old][deviceToken] ${oldDevice?.deviceToken}"); +// print(" >>>> diff result: ${newDevice?.deviceToken == oldDevice?.deviceToken}"); +// print(" [New][androidId] ${newDevice?.androidId}"); +// print(" [Old][androidId] ${oldDevice?.androidId}"); +// print(" >>>> diff result: ${newDevice?.androidId == oldDevice?.androidId}"); +// print(" [New][deviceId] ${newDevice?.deviceId}"); +// print(" [Old][deviceId] ${oldDevice?.deviceId}"); +// print(" >>>> diff result: ${newDevice?.deviceId == oldDevice?.deviceId}"); +// print(" [New][appIdentifier] ${newDevice?.appIdentifier}"); +// print(" [Old][appIdentifier] ${oldDevice?.appIdentifier}"); +// print(" >>>> diff result: ${newDevice?.appIdentifier == oldDevice?.appIdentifier}"); +// print(" [New][brand] ${newDevice?.brand}"); +// print(" [Old][brand] ${oldDevice?.brand}"); +// print(" >>>> diff result: ${newDevice?.brand == oldDevice?.brand}"); +// print(" [New][deviceType] ${newDevice?.deviceType}"); +// print(" [Old][deviceType] ${oldDevice?.deviceType}"); +// print(" >>>> diff result: ${newDevice?.deviceType == oldDevice?.deviceType}"); +// print(" [New][model] ${newDevice?.model}"); +// print(" [Old][model] ${oldDevice?.model}"); +// print(" >>>> diff result: ${newDevice?.model == oldDevice?.model}"); +// print(" [New][pushType] ${newDevice?.pushType}"); +// print(" [Old][pushType] ${oldDevice?.pushType}"); +// print(" >>>> diff result: ${newDevice?.pushType == oldDevice?.pushType}"); +// print("========== DeviceTrack =========="); +// } +// } +} diff --git a/guru_app/packages/guru_utils/lib/device/device_info.g.dart b/guru_app/packages/guru_utils/lib/device/device_info.g.dart new file mode 100644 index 0000000..19645b7 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/device/device_info.g.dart @@ -0,0 +1,42 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'device_info.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +DeviceInfo _$DeviceInfoFromJson(Map json) => DeviceInfo( + appIdentifier: json['appIdentifier'] as String? ?? '', + appVersion: json['appVersion'] as String? ?? '', + brand: json['brand'] as String? ?? '', + deviceCountry: json['deviceCountry'] as String? ?? '', + deviceType: json['deviceType'] as String? ?? '', + language: json['language'] as String? ?? '', + locale: json['locale'] as String? ?? '', + model: json['model'] as String? ?? '', + timezone: json['timezone'] as String? ?? '', + deviceId: json['deviceId'] as String? ?? '', + pushType: json['pushType'] as String? ?? 'FCM', + androidId: json['androidId'] as String? ?? '', + uid: json['uid'] as String? ?? '', + deviceToken: json['deviceToken'] as String? ?? '', + ); + +Map _$DeviceInfoToJson(DeviceInfo instance) => + { + 'androidId': instance.androidId, + 'appIdentifier': instance.appIdentifier, + 'appVersion': instance.appVersion, + 'brand': instance.brand, + 'deviceCountry': instance.deviceCountry, + 'deviceId': instance.deviceId, + 'deviceType': instance.deviceType, + 'language': instance.language, + 'locale': instance.locale, + 'model': instance.model, + 'pushType': instance.pushType, + 'timezone': instance.timezone, + 'uid': instance.uid, + 'deviceToken': instance.deviceToken, + }; diff --git a/guru_app/packages/guru_utils/lib/device/device_utils.dart b/guru_app/packages/guru_utils/lib/device/device_utils.dart new file mode 100644 index 0000000..db2f064 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/device/device_utils.dart @@ -0,0 +1,479 @@ +import 'dart:io'; + +import 'package:android_id/android_id.dart'; +import 'package:flutter/services.dart'; +import 'package:guru_platform_data/guru_platform_data.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:device_info_plus/device_info_plus.dart'; + +import 'dart:ui' as ui; + +import 'device_info.dart'; + +/// Created by Haoyi on 4/14/21 +/// +class DeviceUtils { + static final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); + + // static final Uuid uuid = Uuid(); + static final List _latestOverlays = [ + SystemUiOverlay.top, + SystemUiOverlay.bottom + ]; + + // + static bool? realTablet; + + static DeviceInfo? mockDeviceInfo; + + // static Future getDeviceId() async { + // String deviceId = ""; + // try { + // if (Platform.isAndroid) { + // deviceId = await deviceInfoPlugin.androidInfo.then((info) => info.androidId); + // } else if (Platform.isIOS) { + // deviceId = await deviceInfoPlugin.iosInfo.then((info) => info.identifierForVendor); + // } + // } catch (error) {} + // + // if (deviceId != null && deviceId.isNotEmpty) { + // return deviceId; + // } + // return uuid.v4(); + // } + + static setMockDeviceInfo(DeviceInfo mock) { + mockDeviceInfo = mock; + } + + static LocaleInfo buildLocaleInfo() { + final currentLocale = Platform.localeName.split('_'); + String languageCode = ""; + if (currentLocale.isNotEmpty) { + languageCode = currentLocale[0].toLowerCase(); + } + String countryCode = ""; + if (currentLocale.length > 1) { + countryCode = currentLocale.last.toLowerCase(); + } + String script = ""; + if (currentLocale.length > 2) { + script = currentLocale[1].toLowerCase(); + } + + return LocaleInfo(languageCode, script, countryCode); + } + + static String countryCodeToFlagEmoji(String countryCode) { + if (countryCode.length != 2) { + return ""; + } + final upper = countryCode.toUpperCase(); + final code = upper == "UK" ? "GB" : upper; + + const flagOffset = 0x1F1E6; + const asciiOffset = 0x41; + + return String.fromCharCodes([ + code.codeUnitAt(0) - asciiOffset + flagOffset, + code.codeUnitAt(1) - asciiOffset + flagOffset + ]); + } + + static String currentCountryCode({String defaultCountryCode = "us"}) { + final currentLocale = Platform.localeName.split('_'); + if (currentLocale.length > 1) { + return currentLocale.last.toLowerCase(); + } + return defaultCountryCode; + } + + static void setEnabledSystemUIOverlays(List overlays) { + if (_latestOverlays != overlays) { + // SystemChrome.setEnabledSystemUIOverlays(overlays); + _latestOverlays.clear(); + if (overlays.isNotEmpty == true) { + _latestOverlays.addAll(overlays); + } + } + } + + static Future buildDeviceInfo( + {required String deviceId, String firebasePushToken = '', String uid = ''}) async { + Log.d('device init'); + + final _mockDeviceInfo = mockDeviceInfo; + if (_mockDeviceInfo != null) { + return _mockDeviceInfo..uid = uid; + } + // Platform special info + String _androidId = ""; + String _deviceId = deviceId; + String _deviceName = ""; + String _brand = ""; + String _deviceType = ""; + + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + if (Platform.isAndroid) { + const androidId = AndroidId(); + var build = await deviceInfoPlugin.androidInfo; + _androidId = (await androidId.getId()) ?? deviceId; + _deviceName = build.model; + _brand = build.brand; + _deviceType = 'android'; + } else if (Platform.isIOS) { + var data = await deviceInfoPlugin.iosInfo; + _deviceName = data.name ?? ""; + _brand = data.model ?? ""; + if (DeviceUtils.isTablet()) { + _deviceType = 'iPad'; + } else { + _deviceType = 'iPhone'; + } + } + + String timezone = await GuruPlatformData.getLocalTimezone(); + String locale = Platform.localeName; + List localeNames = locale.split('_').toList(); + String deviceCountry = localeNames.last; + String language = localeNames.first; + + return DeviceInfo( + androidId: _androidId, + appIdentifier: packageInfo.packageName, + appVersion: packageInfo.version, + deviceId: _deviceId, + model: _deviceName, + brand: _brand, + deviceToken: firebasePushToken, + deviceType: _deviceType, + deviceCountry: deviceCountry, + language: language, + locale: locale, + timezone: timezone, + uid: uid, + ); + } + + static bool isTablet() { + if (realTablet != null) { + return realTablet!; + } + final ui.Size physicalSize = ui.window.physicalSize; + if (ui.window.devicePixelRatio < 2 && + (physicalSize.width >= 1000 || physicalSize.height >= 1000)) { + return true; + } else if (ui.window.devicePixelRatio == 2 && + (physicalSize.width >= 1920 || physicalSize.height >= 1920)) { + return true; + } else { + return false; + } + } + + static Future isOutdatedTablet() async { + if (isTablet()) { + if (Platform.isAndroid) { + final info = await deviceInfoPlugin.androidInfo; + Log.d("isOutdatedTablet android: ${info.version.sdkInt}"); + return info.version.sdkInt < 24; + } else if (Platform.isIOS) { + final info = await deviceInfoPlugin.iosInfo; + final versions = info.systemVersion?.split(".") ?? []; + Log.d("isOutdatedTablet ios: $versions"); + return versions.isNotEmpty && int.parse(versions[0]) < 13; + } + } + return false; + } + + static Future isGreaterThanOrEqualIOS14() async { + if (Platform.isIOS) { + final info = await deviceInfoPlugin.iosInfo; + final versions = info.systemVersion?.split(".") ?? []; + return versions.isNotEmpty && int.parse(versions[0]) >= 14; + } else { + return false; + } + } + + static Future buildContactDeviceInfo({required int firstInstallTime, List? extraMessages}) async { + final StringBuffer sb = StringBuffer(); + sb.writeln("-----Please type your reply above this line-----"); + sb.writeln( + "It would help us a lot if you could include the following information in your email:"); + sb.writeln("* Your Reason For Contacting us"); + sb.writeln("* A Screenshot or Video of the issue (If relevant or necessary)\n"); + // final accountDataStore = Injector.provide(); + // sb.writeln("UID: ${accountDataStore.uid}"); + // final firstInstallTime = + // await AppProperty.getInstance().getInt(PropertyKeys.firstInstallTime, defValue: ""); + final formatTime = + firstInstallTime > 0 ? DateTimeUtils.formatMillisecondsToStandard(firstInstallTime) : ""; + sb.writeln("Install Date: $formatTime"); + if (Platform.isAndroid) { + var build = await deviceInfoPlugin.androidInfo; + sb.writeln("Platform: Android"); + sb.writeln("Device: ${build.model}"); + sb.writeln("Brand: ${build.brand}"); + } else if (Platform.isIOS) { + var data = await deviceInfoPlugin.iosInfo; + sb.writeln("Platform: ${DeviceUtils.isTablet() ? "iPad" : "iPhone"}"); + sb.writeln("Device: ${data.name}"); + sb.writeln("Brand: ${data.model}"); + } + final localInfo = buildLocaleInfo(); + sb.writeln("Country: ${localInfo.countryCode}"); + sb.writeln("Language: ${localInfo.languageCode}"); + sb.writeln("TimeZone: ${DateTime.now().timeZoneName}"); + // sb.writeln("HighestScore: ${StatisticManager.instance.peekBestScore().displayScore}"); + // sb.writeln("HighestBlock: ${StatisticManager.instance.peekBestBlock().displayNum}\n"); + if (extraMessages != null) { + for (var element in extraMessages) { + sb.writeln(element); + } + } + sb.writeln("-----Please Don't Delete the info above-----"); + return sb.toString(); + } + + static final List countryCodes = [ + "AD", + "AE", + "AF", + "AG", + "AI", + "AL", + "AM", + "AN", + "AO", + "AQ", + "AR", + "AS", + "AT", + "AU", + "AW", + "AZ", + "BA", + "BB", + "BD", + "BE", + "BF", + "BG", + "BH", + "BI", + "BJ", + "BM", + "BN", + "BO", + "BR", + "BS", + "BT", + "BW", + "BY", + "BZ", + "CA", + "CC", + "CD", + "CF", + "CG", + "CH", + "CI", + "CL", + "CM", + "CN", + "CO", + "CR", + "CU", + "CV", + "CX", + "CY", + "CZ", + "DE", + "DJ", + "DK", + "DM", + "DO", + "DZ", + "EC", + "EE", + "EG", + "EH", + "ER", + "ES", + "ET", + "FI", + "FJ", + "FK", + "FM", + "FO", + "FR", + "GA", + "GB", + "GD", + "GE", + "GG", + "GH", + "GI", + "GL", + "GM", + "GN", + "GQ", + "GR", + "GT", + "GU", + "GW", + "GY", + "HK", + "HN", + "HR", + "HT", + "HU", + "ID", + "IE", + "IL", + "IM", + "IN", + "IO", + "IQ", + "IR", + "IS", + "IT", + "JE", + "JM", + "JO", + "JP", + "KE", + "KG", + "KH", + "KI", + "KM", + "KN", + "KP", + "KR", + "KW", + "KY", + "KZ", + "LA", + "LB", + "LC", + "LI", + "LK", + "LR", + "LS", + "LT", + "LU", + "LV", + "LY", + "MA", + "MC", + "MD", + "ME", + "MG", + "MH", + "MK", + "ML", + "MM", + "MN", + "MO", + "MP", + "MR", + "MS", + "MT", + "MU", + "MV", + "MW", + "MX", + "MY", + "MZ", + "NA", + "NC", + "NE", + "NG", + "NI", + "NL", + "NO", + "NP", + "NR", + "NU", + "NZ", + "OM", + "PA", + "PE", + "PF", + "PG", + "PH", + "PK", + "PL", + "PM", + "PN", + "PR", + "PS", + "PT", + "PW", + "PY", + "QA", + "RE", + "RO", + "RS", + "RU", + "RW", + "SA", + "SB", + "SC", + "SD", + "SE", + "SG", + "SH", + "SI", + "SJ", + "SK", + "SL", + "SM", + "SN", + "SO", + "SR", + "SS", + "ST", + "SV", + "SX", + "SY", + "SZ", + "TC", + "TD", + "TG", + "TH", + "TJ", + "TK", + "TL", + "TM", + "TN", + "TO", + "TR", + "TT", + "TV", + "TW", + "TZ", + "UA", + "UG", + "US", + "UY", + "UZ", + "VA", + "VC", + "VE", + "VG", + "VI", + "VN", + "VU", + "WF", + "WS", + "YE", + "YT", + "ZA", + "ZM", + "ZW", + ]; +} diff --git a/guru_app/packages/guru_utils/lib/events/event_bus.dart b/guru_app/packages/guru_utils/lib/events/event_bus.dart new file mode 100644 index 0000000..83507f5 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/events/event_bus.dart @@ -0,0 +1,50 @@ +import 'dart:async'; + +import 'package:guru_utils/log/log.dart'; +import 'package:rxdart/rxdart.dart'; + +import 'models/event.dart'; +export 'models/event.dart'; + +/// Created by Haoyi on 2022/2/23 + +class RxEventBus { + static RxEventBus? _instance; + + final PublishSubject _subject = PublishSubject(); + + static RxEventBus _getInstance() { + _instance ??= RxEventBus._internal(); + return _instance!; + } + + factory RxEventBus() => _getInstance(); + + static RxEventBus get instance => _getInstance(); + + RxEventBus._internal(); + + void post(Event event) { + _subject.add(event); + } + + void postDelayed(Event event, Duration duration) { + Future.delayed(duration, () { + post(event); + }); + } + + StreamSubscription on(List listeners) { + return _subject.asBroadcastStream().listen((event) { + for (EventListener listener in listeners) { + try { + if (listener.call(event)) { + return; + } + } catch (error, stacktrace) { + Log.d("observe event error:$error $stacktrace"); + } + } + }, onError: (error, stacktrace) {}); + } +} diff --git a/guru_app/packages/guru_utils/lib/events/models/event.dart b/guru_app/packages/guru_utils/lib/events/models/event.dart new file mode 100644 index 0000000..1e050a8 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/events/models/event.dart @@ -0,0 +1,27 @@ +/// Created by Haoyi on 2022/2/23 + +typedef EventListener = bool Function(T event); +typedef EventProcessor = void Function(T event); + +class EventObserver { + final Type eventType; + final EventListener listener; + + EventObserver(this.eventType, this.listener); + + void process(T data) { + listener.call(data); + } +} + +abstract class Event { + static EventListener listen(EventProcessor processor) { + return (event) { + if (event is T) { + processor.call(event); + return true; + } + return false; + }; + } +} diff --git a/guru_app/packages/guru_utils/lib/events/models/game_event.dart b/guru_app/packages/guru_utils/lib/events/models/game_event.dart new file mode 100644 index 0000000..c5276bb --- /dev/null +++ b/guru_app/packages/guru_utils/lib/events/models/game_event.dart @@ -0,0 +1,16 @@ + +import 'event.dart'; + +/// Created by Haoyi on 2022/2/23 + +abstract class GameEvent extends Event { + +} + +class ResetEvent extends GameEvent { + +} + +class StartFromEvent extends GameEvent { + +} \ No newline at end of file diff --git a/guru_app/packages/guru_utils/lib/extensions/async_extension.dart b/guru_app/packages/guru_utils/lib/extensions/async_extension.dart new file mode 100644 index 0000000..eceae6b --- /dev/null +++ b/guru_app/packages/guru_utils/lib/extensions/async_extension.dart @@ -0,0 +1,29 @@ +/// Created by @Haoyi on 2021/7/7 +/// +/// + +part of "extensions.dart"; + +extension SubjectExtension on BehaviorSubject { + void addEx(T event) { + if (!isClosed) { + add(event); + } + } + + bool addIfChanged(T event) { + if (!isClosed && value != event) { + add(event); + return true; + } + return false; + } +} + +extension StreamControllerExtension on StreamController { + void addEx(T event) { + if (!isClosed) { + add(event); + } + } +} diff --git a/guru_app/packages/guru_utils/lib/extensions/extensions.dart b/guru_app/packages/guru_utils/lib/extensions/extensions.dart new file mode 100644 index 0000000..e9dd645 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/extensions/extensions.dart @@ -0,0 +1,18 @@ +import 'dart:async'; + +import 'package:guru_utils/hash/hash.dart'; +import 'package:guru_utils/number/number_utils.dart'; +import 'package:rxdart/rxdart.dart'; + +/// Created by @Haoyi on 4/9/21 + +export 'package:rxdart/rxdart.dart'; + +part "list_extension.dart"; + +part "async_extension.dart"; + +part 'string_extension.dart'; + +part 'number_extension.dart'; + diff --git a/guru_app/packages/guru_utils/lib/extensions/list_extension.dart b/guru_app/packages/guru_utils/lib/extensions/list_extension.dart new file mode 100644 index 0000000..9d9437d --- /dev/null +++ b/guru_app/packages/guru_utils/lib/extensions/list_extension.dart @@ -0,0 +1,34 @@ +/// Created by @Haoyi on 2021/7/7 + +part of "extensions.dart"; + +extension ListExtension on Iterable { + T? get safeLast => isEmpty ? null : last; + + T? get safeFirst => isEmpty ? null : first; + + int get lastIndex => length - 1; + + List get filterOutNulls { + final List filtered = []; + for (var item in this) { + if (item != null) { + filtered.add(item); + } + } + return filtered; + } + + T? firstWhereOrNull(bool test(T element), {T? orElse()?}) { + for (T element in this) { + if (test(element)) return element; + } + if (orElse != null) return orElse(); + return null; + } + + T? firstOrNull() { + return isNotEmpty ? first : null; + } + +} diff --git a/guru_app/packages/guru_utils/lib/extensions/number_extension.dart b/guru_app/packages/guru_utils/lib/extensions/number_extension.dart new file mode 100644 index 0000000..7543742 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/extensions/number_extension.dart @@ -0,0 +1,24 @@ +/// Created by @Haoyi on 2021/12/5 + +part of "extensions.dart"; + +List _converts = [ + NumberUtils.twoDigits, + NumberUtils.threeDigits, + NumberUtils.fourDigits, + NumberUtils.fiveDigits, + NumberUtils.sixDigits, + NumberUtils.sevenDigits, + NumberUtils.eightDigits, + NumberUtils.nineDigits, + NumberUtils.tenDigits +]; + +extension IntExtension on int { + String format(int digits) { + if (digits - 2 < _converts.length) { + return _converts[digits - 2](this); + } + return NumberUtils.nDigits(this, digits); + } +} diff --git a/guru_app/packages/guru_utils/lib/extensions/string_extension.dart b/guru_app/packages/guru_utils/lib/extensions/string_extension.dart new file mode 100644 index 0000000..0de5f29 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/extensions/string_extension.dart @@ -0,0 +1,29 @@ +part of "extensions.dart"; +extension StringExtension on String { + String capitalizeFirst() { + return "${this[0].toUpperCase()}${substring(1)}"; + } + + int checksum() { + int hash = 0x9E370001; + final cus = codeUnits; + for (var cu in cus) { + hash = hashCombine(hash, cu); + } + return hashFinish(hash); + } + + int substringCount(String sub) { + int count = 0; + int start = 0; + while (start < length) { + final index = indexOf(sub, start); + if (index < 0) { + break; + } + start = index + sub.length; + count++; + } + return count; + } +} diff --git a/guru_app/packages/guru_utils/lib/feedback/feedback_manager.dart b/guru_app/packages/guru_utils/lib/feedback/feedback_manager.dart new file mode 100644 index 0000000..171e5bf --- /dev/null +++ b/guru_app/packages/guru_utils/lib/feedback/feedback_manager.dart @@ -0,0 +1,113 @@ +import 'package:guru_utils/audio/audio_effector.dart'; +import 'package:guru_utils/vibration/vibrate_model.dart'; +import 'package:guru_utils/vibration/vibrate_utils.dart'; + +/// Created by Haoyi on 2023/9/26 + +class FeedbackOccasion { + final String name; + final FeedbackOccasion? parent; + + const FeedbackOccasion._(this.name, {this.parent}); + + static const click = FeedbackOccasion._('click'); + static const page = FeedbackOccasion._('page'); + static const popup = FeedbackOccasion._('popup'); + + static const clickButton = FeedbackOccasion._('clickButton', parent: click); + static const clickTab = FeedbackOccasion._('clickTab', parent: click); + static const clickItem = FeedbackOccasion._('clickItem', parent: click); + static const clickSwitch = FeedbackOccasion._('clickList', parent: click); + + static const openPage = FeedbackOccasion._('openPage', parent: page); + static const backPage = FeedbackOccasion._('backPage', parent: page); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FeedbackOccasion && + runtimeType == other.runtimeType && + name == other.name && + parent == other.parent; + + @override + int get hashCode => name.hashCode ^ parent.hashCode; +} + +class FeedbackProposal { + final FeedbackOccasion occasion; + + final AudioEffect? audio; + + final VibrateEffect? vibration; + + const FeedbackProposal(this.occasion, {this.audio, this.vibration}); +} + +class FeedbackCapabilities { + static const int _sound = 0x01; + static const int _vibration = 0x02; + + static const disabled = FeedbackCapabilities._(0); + static const fullCapabilities = FeedbackCapabilities._(_sound | _vibration); + static const sound = FeedbackCapabilities._(_sound); + static const vibration = FeedbackCapabilities._(_vibration); + + final int value; + + const FeedbackCapabilities._(this.value); + + bool hasCapabilities(FeedbackCapabilities capabilities) { + return (value & capabilities.value) == capabilities.value; + } + + bool canPerform() { + return value != disabled.value; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FeedbackCapabilities && runtimeType == other.runtimeType && value == other.value; + + @override + int get hashCode => value.hashCode; +} + +class FeedbackManager { + static final FeedbackManager instance = FeedbackManager._(); + + FeedbackManager._(); + + final Map _feedbacks = {}; + + static void init({List proposals = const []}) { + for (var proposal in proposals) { + instance._feedbacks[proposal.occasion] = proposal; + } + } + + FeedbackProposal? _searchProposal(FeedbackOccasion occasion) { + for (FeedbackOccasion? o = occasion; o != null; o = o.parent) { + final proposal = _feedbacks[o]; + if (proposal != null) { + return proposal; + } + } + return null; + } + + void perform(FeedbackOccasion occasion, + {FeedbackCapabilities capabilities = FeedbackCapabilities.fullCapabilities}) { + final proposal = _searchProposal(occasion); + if (proposal != null) { + if (capabilities.hasCapabilities(FeedbackCapabilities.sound) && proposal.audio != null) { + AudioEffector.instance.play(proposal.audio!); + } + if (capabilities.hasCapabilities(FeedbackCapabilities.vibration) && + proposal.vibration != null) { + VibrateUtils.vibrate(proposal.vibration!); + } + } + } +} diff --git a/guru_app/packages/guru_utils/lib/file/archive_utils.dart b/guru_app/packages/guru_utils/lib/file/archive_utils.dart new file mode 100644 index 0000000..c239034 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/file/archive_utils.dart @@ -0,0 +1,20 @@ +import 'dart:convert'; + +import 'package:archive/archive.dart'; + +/// Created by Haoyi on 2/5/21 +/// + +class ArchiveUtils { + static String compressString(String data, {bool useUtf8 = false}) { + final bytes = utf8.encode(data); + final gzipBytes = GZipEncoder().encode(bytes) ?? []; + return base64Url.encode(gzipBytes); + } + + static String decompressString(String base64String) { + final gzipBytes = base64Url.decode(base64String); + final bytes = GZipDecoder().decodeBytes(gzipBytes); + return utf8.decode(bytes); + } +} diff --git a/guru_app/packages/guru_utils/lib/file/file_utils.dart b/guru_app/packages/guru_utils/lib/file/file_utils.dart new file mode 100644 index 0000000..7b9853c --- /dev/null +++ b/guru_app/packages/guru_utils/lib/file/file_utils.dart @@ -0,0 +1,202 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:archive/archive.dart'; +import 'package:archive/archive_io.dart'; +import 'package:guru_utils/http/http_ex.dart'; + +import 'package:guru_utils/log/log.dart'; +import 'package:http/http.dart' as http; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; + +/// Created by Haoyi on 2020/4/22 +/// + +class FileUtils { + static FileUtils? _instance; + + static FileUtils _getInstance() { + _instance ??= FileUtils._internal(); + return _instance!; + } + + factory FileUtils() => _getInstance(); + + static FileUtils get instance => _getInstance(); + + final _utf8Decoder = const Utf8Decoder(); + + FileUtils._internal(); + + static Directory? appFileDirectory; + + Future getFileDirectory() async { + appFileDirectory ??= await (Platform.isIOS + ? getApplicationDocumentsDirectory() + : getApplicationSupportDirectory()); + return appFileDirectory!; + } + + Future getCacheDir({List? subDirs, bool recursive = true}) async { + return getTemporaryDirectory().then((dir) => + Directory("${dir.path}/${subDirs != null ? path.joinAll(subDirs) : ''}") + .create(recursive: recursive)); + } + + Future getAppDir({List? subDirs, bool recursive = true}) async { + return getFileDirectory().then((dir) => + Directory("${dir.path}/${subDirs != null ? path.joinAll(subDirs) : ''}") + .create(recursive: recursive)); + } + + Future createCacheFile(String name) async { + return getTemporaryDirectory().then((dir) => File(path.join(dir.path, name))); + } + + File joinPathSegments(List paths) { + return File(path.joinAll(paths)); + } + + File createFile(Directory directory, name) { + return File(path.join(directory.path, name)); + } + + Future downloadFile(String url, File file) async { + final path = file.path; + final newFile = File(path + ".tmp"); + final data = await HttpEx.get(Uri.parse(url)); + if (data.statusCode != 200) { + return file; + } + await newFile.writeAsBytes(data.bodyBytes); + if (file.existsSync()) { + file.deleteSync(); + } + return newFile.renameSync(path); + } + + Future loadFileAndConvert(File file, T Function(String data) convert) async { + try { + final dataStr = file.readAsStringSync(); + return convert(dataStr); + } catch (error, stacktrace) { + Log.w("downloadFileAndCovert error! $error $stacktrace"); + return null; + } + } + + Future downloadFileAndConvert( + String url, File file, T Function(String data) convert) async { + final path = file.path; + final newFile = File(path + ".tmp"); + final data = await HttpEx.get(Uri.parse(url)); + if (data.statusCode != 200) { + return null; + } + try { + final result = convert(_utf8Decoder.convert(data.bodyBytes)); + await newFile.writeAsBytes(data.bodyBytes); + if (file.existsSync()) { + file.deleteSync(); + } + newFile.renameSync(path); + return result; + } catch (error, stacktrace) { + Log.w("downloadFileAndCovert error! $error $stacktrace"); + return null; + } + } + + Future checkFileExists(File file) async { + return file.exists(); + } + + Future saveFile(File file, String data) async { + file.writeAsStringSync(data); + return true; + } + + Future copyFile(File srcFile, File dstFile, {bool deleteSrc = false}) async { + if (!dstFile.existsSync()) { + dstFile.createSync(); + } + srcFile.copySync(dstFile.path); + if (deleteSrc) { + srcFile.deleteSync(); + } + return dstFile; + } + + void deleteFile(File file, {bool recursive = false}) { + file.deleteSync(recursive: recursive); + } + + static Future unzipFile(File zipFile, String unzipPath) async { + try { + List bytes = zipFile.readAsBytesSync(); + Archive archive = ZipDecoder().decodeBytes(bytes); + for (ArchiveFile file in archive) { + if (file.isFile) { + List data = file.content; + File(unzipPath + "/" + file.name) + ..createSync(recursive: true) + ..writeAsBytesSync(data); + } else { + Directory(unzipPath + "/" + file.name).create(recursive: true); + } + } + print("unzip success"); + return true; + } catch (error) { + print("unzip error=${error}"); + return false; + } + } + + static Future unzipTo(Uint8List data, String unzipPath, {String? password}) async { + try { + Archive archive = ZipDecoder().decodeBytes(data, password: password); + for (ArchiveFile file in archive) { + if (file.isFile) { + List data = file.content; + File(unzipPath + "/" + file.name) + ..createSync(recursive: true) + ..writeAsBytesSync(data); + } else { + Directory(unzipPath + "/" + file.name).create(recursive: true); + } + } + Log.d("unzip success"); + return true; + } catch (error) { + Log.d("unzip error:$error"); + } + return false; + } + + void zipFile(String zipName, String zipPath) { + var encoder = ZipFileEncoder(); + encoder.zipDirectory(Directory(zipPath)); + encoder.create(zipName); + encoder.addDirectory(Directory(zipPath)); + encoder.addFile(File(zipName)); + encoder.close(); + } + + String getFileName(File file, {bool withoutExtension = false}) { + if (!file.existsSync()) { + return ""; + } + if (withoutExtension) { + return path.basenameWithoutExtension(file.path); + } else { + return path.basename(file.path); + } + } + + Future saveFileData(File file, Uint8List data) async { + file.writeAsBytesSync(data); + } +} \ No newline at end of file diff --git a/guru_app/packages/guru_utils/lib/guru_utils.dart b/guru_app/packages/guru_utils/lib/guru_utils.dart new file mode 100644 index 0000000..1b855c0 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/guru_utils.dart @@ -0,0 +1,15 @@ +library guru_utils; + +typedef ToastDelegate = void Function(String message, {Duration duration}); + +class GuruUtils { + static ToastDelegate? toastDelegate; + + static String? flavor; + + static bool isTablet = false; + + static void showToast(String message, {Duration duration = const Duration(seconds: 3)}) { + toastDelegate?.call(message, duration: duration); + } +} diff --git a/guru_app/packages/guru_utils/lib/hash/hash.dart b/guru_app/packages/guru_utils/lib/hash/hash.dart new file mode 100644 index 0000000..252e689 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/hash/hash.dart @@ -0,0 +1,40 @@ +library hash; + +/// Created by @Haoyi on 2021/7/7 + +/// Generates a hash code for multiple [objects]. +int hashObjects(Iterable objects) => _finish(objects.fold(0, (h, i) => _combine(h, i.hashCode))); + +/// Generates a hash code for two objects. +int hash2(a, b) => _finish(_combine(_combine(0, a.hashCode), b.hashCode)); + +/// Generates a hash code for three objects. +int hash3(a, b, c) => _finish(_combine(_combine(_combine(0, a.hashCode), b.hashCode), c.hashCode)); + +/// Generates a hash code for four objects. +int hash4(a, b, c, d) => _finish( + _combine(_combine(_combine(_combine(0, a.hashCode), b.hashCode), c.hashCode), d.hashCode)); + +int hashIntList(List list) => _finish(list.fold(0x9E370001, (h, i) => _combine(h, i))); + +// Jenkins hash functions + +int _combine(int hash, int value) { + hash = 0x1fffffff & (hash + value); + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); +} + +int _finish(int hash) { + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); +} + +int hashCombine(int hash, int value) { + return _combine(hash, value); +} + +int hashFinish(int hash) { + return _finish(hash); +} diff --git a/guru_app/packages/guru_utils/lib/http/http_ex.dart b/guru_app/packages/guru_utils/lib/http/http_ex.dart new file mode 100644 index 0000000..8f0dfd7 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/http/http_ex.dart @@ -0,0 +1,274 @@ +import 'dart:convert'; + +import 'package:guru_utils/core/ext.dart'; +import 'package:guru_utils/http/http_model.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:http/http.dart' as _http; + +/// Created by Haoyi on 2022/8/9 + +class Cdn { + final String cdnPrefix; + final String srcPrefix; + final Uri uri; + + const Cdn(this.uri, {required this.srcPrefix, this.cdnPrefix = ""}); + + @override + String toString() { + return 'Cdn{cdnPrefix: $cdnPrefix, srcPrefix: $srcPrefix, uri: $uri}'; + } +} + +class CdnDetails { + final int count; + final int failed; + final int fatal; + + CdnDetails(this.count, this.failed, this.fatal); + + CdnDetails copyWith({int? count, int? failed, int? fatal}) { + return CdnDetails(count ?? this.count, failed ?? this.failed, fatal ?? this.fatal); + } + + @override + String toString() { + return 'CdnDetails{count: $count, failed: $failed, fatal: $fatal}'; + } +} + +class HttpEx { + static CdnConfig _cdnConfig = CdnConfig.fromJson({}); + static String _firebaseStoragePrefix = ""; + static final Map _cdnDetails = {}; + + static const _fatalResponseCode = { + 401, // Unauthorized + 403, // Forbidden + 405, // Method Not Allowed + 406, // Not Acceptable + 407, // Proxy Authentication Required + 415, // Unsupported Media Type + 500, // Internal Server Error + 501, // Not Implemented + 503, // Service Unavailable + 505, // HTTP Version not Supported + }; + + static void init(CdnConfig cdnConfig, String firebaseStoragePrefix) { + _cdnConfig = cdnConfig; + _firebaseStoragePrefix = firebaseStoragePrefix; + } + + static bool isFirebaseStoragePrefix(String url) { + return url.startsWith(_firebaseStoragePrefix); + } + + static Cdn selectCdn(Uri srcUrl) { + final prefixList = _cdnConfig.prefix; + String cdnPrefix = _cdnConfig.cdnPrefix; + // check fatal count! use reserve cdn + if (DartExt.isNotBlank(cdnPrefix)) { + final url = srcUrl.toString(); + final fatalCount = _cdnDetails[cdnPrefix]?.fatal ?? 0; + Log.d("selectCdn fatalCount $fatalCount replacement:$cdnPrefix "); + if (fatalCount >= _cdnConfig.fatalThreshold) { + final _fallbackPrefix = _cdnConfig.fallbackPrefix; + if (DartExt.isNotBlank(_fallbackPrefix)) { + cdnPrefix = _fallbackPrefix; + } else { + return Cdn(srcUrl, srcPrefix: "", cdnPrefix: ""); + } + } + String newUrl = cdnPrefix; + for (var prefix in prefixList) { + if (url.startsWith(prefix)) { + var urlPath = url.replaceFirst(prefix, ""); + final path = Uri.parse(urlPath).path; + final decodedPathUri = Uri.decodeFull(path); + if (isFirebaseStoragePrefix(cdnPrefix)) { + if (decodedPathUri[0] == '/') { + newUrl += '/${Uri.encodeComponent(decodedPathUri.substring(1))}'; + } else { + newUrl += Uri.encodeComponent(decodedPathUri); + } + newUrl += "?alt=media"; + return Cdn(Uri.parse(newUrl), srcPrefix: prefix, cdnPrefix: cdnPrefix); + } else { + newUrl += Uri.encodeFull(decodedPathUri); + return Cdn(Uri.parse(newUrl), srcPrefix: prefix, cdnPrefix: cdnPrefix); + } + } + } + } + return Cdn(srcUrl, srcPrefix: "", cdnPrefix: ""); + } + + /// Sends an HTTP HEAD request with the given headers to the given URL. + /// + /// This automatically initializes a new [Client] and closes that client once + /// the request is complete. If you're planning on making multiple requests to + /// the same server, you should use a single [Client] for all of those requests. + /// + /// For more fine-grained control over the request, use [Request] instead. + static Future<_http.Response> head(Uri url, {Map? headers}) => + _http.head(url, headers: headers); + + static Future<_http.Response> _cdnGet(Cdn cdn, CdnDetails cdnDetails, + {Map? headers}) async { + late _http.Response response; + try { + Log.d("_cdnGet: ${cdn.uri}"); + response = await _http.get(cdn.uri, headers: headers); + Log.d("_cdnGet response: ${cdn.uri} ${response.statusCode}"); + if (_fatalResponseCode.contains(response.statusCode)) { + _cdnDetails[cdn.cdnPrefix] = cdnDetails.copyWith( + count: cdnDetails.count + 1, + failed: cdnDetails.failed + 1, + fatal: cdnDetails.fatal + 1); + } else if (response.statusCode != 200) { + _cdnDetails[cdn.cdnPrefix] = + cdnDetails.copyWith(count: cdnDetails.count + 1, failed: cdnDetails.failed + 1); + } else { + _cdnDetails[cdn.cdnPrefix] = cdnDetails.copyWith(count: cdnDetails.count + 1); + } + return response; + } catch (error) { + Log.w("_cdnGet error: $error"); + _cdnDetails[cdn.cdnPrefix] = + cdnDetails.copyWith(count: cdnDetails.count + 1, failed: cdnDetails.failed + 1); + rethrow; + } + } + + /// Sends an HTTP GET request with the given headers to the given URL. + /// + /// This automatically initializes a new [Client] and closes that client once + /// the request is complete. If you're planning on making multiple requests to + /// the same server, you should use a single [Client] for all of those requests. + /// + /// For more fine-grained control over the request, use [Request] instead. + static Future<_http.Response> get(Uri url, {Map? headers}) async { + final cdn = selectCdn(url); + final cdnDetails = _cdnDetails[cdn.cdnPrefix] ?? CdnDetails(0, 0, 0); + return await _cdnGet(cdn, cdnDetails, headers: headers); + + // final _monitor = _cdnConfig.monitor; + // if (_monitor) { + // final metric = FirebasePerformance.instance.newHttpMetric(cdn.uri.toString(), HttpMethod.Get); + // metric.putAttribute("count", cdnDetails.count.toString()); + // metric.putAttribute("failed", cdnDetails.failed.toString()); + // metric.putAttribute("fatal", cdnDetails.fatal.toString()); + // await metric.start(); + // try { + // final response = await _cdnGet(cdn, cdnDetails, headers: headers); + // metric.responseContentType = response.headers['Content-Type']; + // metric.responsePayloadSize = response.contentLength; + // metric.httpResponseCode = response.statusCode; + // return response; + // } catch (error, stacktrace) { + // metric.responseContentType = "crash"; + // metric.responsePayloadSize = 0; + // metric.httpResponseCode = 400; // bad request + // rethrow; + // } finally { + // await metric.stop(); + // } + // } else { + // return await _cdnGet(cdn, cdnDetails, headers: headers); + // } + } + + /// Sends an HTTP POST request with the given headers and body to the given URL. + /// + /// [body] sets the body of the request. It can be a [String], a [List] or + /// a [Map]. If it's a String, it's encoded using [encoding] and + /// used as the body of the request. The content-type of the request will + /// default to "text/plain". + /// + /// If [body] is a List, it's used as a list of bytes for the body of the + /// request. + /// + /// If [body] is a Map, it's encoded as form fields using [encoding]. The + /// content-type of the request will be set to + /// `"application/x-www-form-urlencoded"`; this cannot be overridden. + /// + /// [encoding] defaults to [utf8]. + /// + /// For more fine-grained control over the request, use [Request] or + /// [StreamedRequest] instead. + static Future<_http.Response> post(Uri url, + {Map? headers, Object? body, Encoding? encoding}) => + _http.post(url, headers: headers, body: body, encoding: encoding); + + /// Sends an HTTP PUT request with the given headers and body to the given URL. + /// + /// [body] sets the body of the request. It can be a [String], a [List] or + /// a [Map]. If it's a String, it's encoded using [encoding] and + /// used as the body of the request. The content-type of the request will + /// default to "text/plain". + /// + /// If [body] is a List, it's used as a list of bytes for the body of the + /// request. + /// + /// If [body] is a Map, it's encoded as form fields using [encoding]. The + /// content-type of the request will be set to + /// `"application/x-www-form-urlencoded"`; this cannot be overridden. + /// + /// [encoding] defaults to [utf8]. + /// + /// For more fine-grained control over the request, use [Request] or + /// [StreamedRequest] instead. + static Future<_http.Response> put(Uri url, + {Map? headers, Object? body, Encoding? encoding}) => + _http.put(url, headers: headers, body: body, encoding: encoding); + + /// Sends an HTTP PATCH request with the given headers and body to the given + /// URL. + /// + /// [body] sets the body of the request. It can be a [String], a [List] or + /// a [Map]. If it's a String, it's encoded using [encoding] and + /// used as the body of the request. The content-type of the request will + /// default to "text/plain". + /// + /// If [body] is a List, it's used as a list of bytes for the body of the + /// request. + /// + /// If [body] is a Map, it's encoded as form fields using [encoding]. The + /// content-type of the request will be set to + /// `"application/x-www-form-urlencoded"`; this cannot be overridden. + /// + /// [encoding] defaults to [utf8]. + /// + /// For more fine-grained control over the request, use [Request] or + /// [StreamedRequest] instead. + static Future<_http.Response> patch(Uri url, + {Map? headers, Object? body, Encoding? encoding}) => + _http.patch(url, headers: headers, body: body, encoding: encoding); + + /// Sends an HTTP DELETE request with the given headers to the given URL. + /// + /// This automatically initializes a new [Client] and closes that client once + /// the request is complete. If you're planning on making multiple requests to + /// the same server, you should use a single [Client] for all of those requests. + /// + /// For more fine-grained control over the request, use [Request] instead. + static Future<_http.Response> delete(Uri url, + {Map? headers, Object? body, Encoding? encoding}) => + _http.delete(url, headers: headers, body: body, encoding: encoding); + + /// Sends an HTTP GET request with the given headers to the given URL and + /// returns a Future that completes to the body of the response as a [String]. + /// + /// The Future will emit a [ClientException] if the response doesn't have a + /// success status code. + /// + /// This automatically initializes a new [Client] and closes that client once + /// the request is complete. If you're planning on making multiple requests to + /// the same server, you should use a single [Client] for all of those requests. + /// + /// For more fine-grained control over the request and response, use [Request] + /// instead. + static Future read(Uri url, {Map? headers}) => + _http.read(selectCdn(url).uri, headers: headers); +} diff --git a/guru_app/packages/guru_utils/lib/http/http_model.dart b/guru_app/packages/guru_utils/lib/http/http_model.dart new file mode 100644 index 0000000..455df17 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/http/http_model.dart @@ -0,0 +1,34 @@ +/// Created by Haoyi on 2022/8/25 + +class CdnConfig { + final List prefix; + + final String cdnPrefix; + + final String fallbackPrefix; + + final int fatalThreshold; + + final bool monitor; + + CdnConfig(this.prefix, this.cdnPrefix, this.fatalThreshold, this.fallbackPrefix, this.monitor); + + factory CdnConfig.fromJson(Map json, + {String defaultStoragePrefix = '', String defaultCdnPrefix = ''}) => + CdnConfig( + (json['prefix'] as List?)?.map((e) => e as String).toList() ?? + [defaultStoragePrefix], + json['cdn'] as String? ?? defaultCdnPrefix, + json['fatal_threshold'] as int? ?? 5, + json['fallback'] as String? ?? defaultStoragePrefix, + json['monitor'] as bool? ?? false, + ); + + Map toJson() => { + 'prefix': prefix, + 'cdn': cdnPrefix, + 'fallback': fallbackPrefix, + 'fatal_threshold': fatalThreshold, + 'monitor': monitor, + }; +} diff --git a/guru_app/packages/guru_utils/lib/id/id_utils.dart b/guru_app/packages/guru_utils/lib/id/id_utils.dart new file mode 100644 index 0000000..ef70b1a --- /dev/null +++ b/guru_app/packages/guru_utils/lib/id/id_utils.dart @@ -0,0 +1,115 @@ +import 'dart:math'; + +import 'package:uuid/uuid.dart'; + + +/// Created by @Haoyi on 2021/7/7 + +class IdUtils { + static const _ORIGINAL_ALPHABET = + '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + + static const _BASE36_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'; + + static const _ORIGINAL_CODE = 'Y3JUXK9FC2SAV8ZE5HW6BQDTP7MNLG4R'; + + static final mask = (2 << log(_ORIGINAL_ALPHABET.length - 1) ~/ ln2) - 1; + + static final magicNumber = 0x9E370001; + + static final Uuid _uuid = Uuid(); + static final Random _random = Random(DateTime.now().microsecondsSinceEpoch); + + static String padBlank(String codes, int maxLength) { + print("sb.length:${codes.length} $maxLength"); + if (codes.length < maxLength) { + final count = maxLength - codes.length; + for (int index = 0; index < count; ++index) { + codes = "${_ORIGINAL_CODE[0]}$codes"; + } + } + return codes; + } + + static String numToCode(int num, int maxLen, {bool padding = true}) { + if (num < 0) { + return ""; + } + String codes = ""; + int cn = num; + while ((cn ~/ _ORIGINAL_CODE.length) > 0) { + final index = cn % _ORIGINAL_CODE.length; + codes = "$codes${_ORIGINAL_CODE[index]}"; + cn = cn ~/ _ORIGINAL_CODE.length; + } + final tailIndex = cn % _ORIGINAL_CODE.length; + if (tailIndex > 0) { + codes = "$codes${_ORIGINAL_CODE[tailIndex]}"; + } + if (padding) { + codes = padBlank(codes, maxLen); + } + return codes; + } + + static List _randomBytes(int size) { + final bytes = []; + for (var i = 0; i < size; i++) { + bytes.add((_random.nextDouble() * 256).floor()); + } + return bytes; + } + + static List uuidBytes() { + final buffer = List.filled(16, 0); + _uuid.v4buffer(buffer); + return buffer; + } + + static String keyUuid(String key) { + return _uuid.v5(Uuid.NAMESPACE_URL, key); + } + + static int randomInt(int max) { + return _random.nextInt(max); + } + + static int mergeBytesToHashCode(List data) { + return data.reduce((value, element) => element * 31 + value); + } + + static int randomUuidHashCode() { + final bytes = uuidBytes(); + bytes.shuffle(); + return mergeBytesToHashCode(bytes); + } + + static String randomBase36Text(int length) { + return List.generate(length, (index) => _BASE36_ALPHABET[_random.nextInt(36)]).join(); + } + + static String uuidV4() => _uuid.v4(); + + static String generate(int size) { + final seed = uuidBytes(); + final step = (1.6 * mask * size / _ORIGINAL_ALPHABET.length).ceil(); + var id = ''; + const faker = true; + while (faker) { + final bytes = _randomBytes(step); + seed.shuffle(); + int hashValue = 0; + for (var i = 0; i < step; i++) { + hashValue = hashValue * 17 + (bytes[i] + seed[i % 16]) * 17; + final byte = hashValue & mask; + if (byte > 0 && _ORIGINAL_ALPHABET.length > byte) { + id += _ORIGINAL_ALPHABET[byte]; + if (id.length == size) { + return id; + } + } + } + } + return ''; + } +} diff --git a/guru_app/packages/guru_utils/lib/id/identifiable.dart b/guru_app/packages/guru_utils/lib/id/identifiable.dart new file mode 100644 index 0000000..b5fd5a6 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/id/identifiable.dart @@ -0,0 +1,5 @@ +/// Created by @Haoyi on 2021/7/25 + +mixin Identifiable { + String get id; +} \ No newline at end of file diff --git a/guru_app/packages/guru_utils/lib/image/image_utils.dart b/guru_app/packages/guru_utils/lib/image/image_utils.dart new file mode 100644 index 0000000..282bd7c --- /dev/null +++ b/guru_app/packages/guru_utils/lib/image/image_utils.dart @@ -0,0 +1,125 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:ui' as Graphics show instantiateImageCodec, Codec, Image, TextStyle; +import 'dart:ui'; +import 'package:guru_utils/http/http_ex.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/uri/uri_utils.dart'; +import 'package:lottie/lottie.dart'; +import 'package:path/path.dart' as path; +import 'package:http/http.dart' as http; +import 'package:flutter/services.dart'; +import 'package:image/image.dart'; + +/// Created by Haoyi on 2022/6/8 + +class ImageUtils { + static Future loadImageFromFile( + String path, { + Size? size, + bool allowUpscaling = true, + }) async { + final data = await File(path).readAsBytes(); + Graphics.Codec codec = await Graphics.instantiateImageCodec(data, + targetWidth: size?.width.toInt(), + targetHeight: size?.height.toInt(), + allowUpscaling: allowUpscaling); + FrameInfo frameInfo = await codec.getNextFrame(); + return frameInfo.image; + } + + static Future loadImageFromAsset(String path, + {Size? size, String? package}) async { + final data = await rootBundle.load(package != null ? "packages/$package/$path" : path); + Graphics.Codec codec = await Graphics.instantiateImageCodec(data.buffer.asUint8List(), + targetWidth: size?.width.toInt(), targetHeight: size?.height.toInt()); + FrameInfo frameInfo = await codec.getNextFrame(); + return frameInfo.image; + } + + static Future loadImageFromHttp(String url, + {Size? size, Duration timeout = const Duration(seconds: 30)}) async { + final response = await HttpEx.get(Uri.parse(url)).timeout(timeout); + final codec = await Graphics.instantiateImageCodec(response.bodyBytes, + targetWidth: size?.width.toInt(), targetHeight: size?.height.toInt()); + final frameInfo = await codec.getNextFrame(); + return frameInfo.image; + } + + static Future loadLottieFromFile(String path, {Size? size}) async { + final lottieComposition = await FileLottie(File(path)).load(); + return LottieDrawable(lottieComposition); + } + + static Future toImageData(Graphics.Image image, {format = ImageByteFormat.png}) async { + final byteData = await image.toByteData(format: format); + return byteData!.buffer.asUint8List(); + } + + static Future loadLottieFromAsset(String assetName, {Size? size}) async { + final lottieComposition = await AssetLottie(assetName).load(); + return LottieDrawable(lottieComposition); + } + + static Future loadImage(String uri, + {Size? size, Duration timeout = const Duration(seconds: 30)}) async { + try { + switch (UriUtils.parseUriType(uri)) { + case UriType.assets: + return await loadImageFromAsset(uri, size: size); + case UriType.http: + Log.d("loadImageFromHttp!! $uri"); + return await loadImageFromHttp(uri, size: size, timeout: timeout); + case UriType.file: + return await loadImageFromFile(uri, size: size); + default: + return null; + } + } catch (error) { + Log.e("loadImage!! $error"); + return null; + } + } + + static Future saveBase64ImageData(File file, String base64Data) async { + final bytes = const Base64Decoder().convert(base64Data); + await file.writeAsBytes(bytes); + return file; + } + + static Future loadImageData(String uri, + {Size? size, + Duration timeout = const Duration(seconds: 30), + format = ImageByteFormat.png}) async { + final image = await loadImage(uri, size: size, timeout: timeout); + return image != null ? toImageData(image) : null; + } + + static Future toJpegAndSaveFile(File srcFile, File dstFile, {int quality = 100}) async { + final image = await loadImageFromFile(srcFile.path); + final data = await image.toByteData(format: ImageByteFormat.png); + final img = decodeImage(data!.buffer.asUint8List()); + return dstFile.writeAsBytes(encodeJpg(img!, quality: quality), flush: true); + } + + static Future toJpegAndSaveFileFromImage(Graphics.Image image, File imageFile) async { + final data = await image.toByteData(format: ImageByteFormat.png); + final img = decodeImage(data!.buffer.asUint8List()); + return imageFile.writeAsBytes(encodeJpg(img!), flush: true); + } + + static Future saveImageToFile(Graphics.Image image, File imageFile) async { + final data = await image.toByteData(format: ImageByteFormat.png); + return imageFile.writeAsBytes(data!.buffer.asUint8List(), flush: true); + } + + static Rect getBoundary(Graphics.Image image) { + return Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()); + } + + static recycleImage(Graphics.Image? image) { + try { + image?.dispose(); + } catch (_) {} + } +} diff --git a/guru_app/packages/guru_utils/lib/lifecycle/lifecycle_manager.dart b/guru_app/packages/guru_utils/lib/lifecycle/lifecycle_manager.dart new file mode 100644 index 0000000..8b42261 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/lifecycle/lifecycle_manager.dart @@ -0,0 +1,158 @@ +import 'dart:collection'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:get/get_navigation/src/routes/observers/route_observer.dart'; +import 'package:guru_utils/controller/base_controller.dart'; +import 'package:guru_utils/controller/controller.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_applifecycle_flutter/guru_applifecycle.dart'; +import 'package:guru_utils/router/router.dart'; + +/// Created by Haoyi on 2022/7/18 + +class LifecycleSnapshot { + final LifecycleState state; + final bool foreground; + final Routing routing; + + const LifecycleSnapshot(this.state, this.foreground, this.routing); + + static final LifecycleSnapshot invalid = + LifecycleSnapshot(LifecycleState.create, false, Routing()); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is LifecycleSnapshot && + runtimeType == other.runtimeType && + state == other.state && + foreground == other.foreground && + routing == other.routing; + + @override + int get hashCode => state.hashCode ^ foreground.hashCode ^ routing.hashCode; + + @override + String toString() { + return 'LifecycleSnapshot{state: $state, foreground: $foreground, routing: ${routing.current}}'; + } +} + +mixin LifecycleObserver on BaseController { + void onLifecycleChanged(LifecycleSnapshot snapshot) {} +} + +class LifecycleManager with AppLifecycleCallback, WidgetsBindingObserver { + static final LifecycleManager instance = LifecycleManager._(); + + final BehaviorSubject foregroundSubject = BehaviorSubject.seeded(true); + + final BehaviorSubject lifecycleState = + BehaviorSubject.seeded(LifecycleState.create); + + final BehaviorSubject lifecycleSnapshotSubject = BehaviorSubject(); + + LifecycleSnapshot get currentLifecycleSnapshot => lifecycleSnapshotSubject.value; + + final PublishSubject lifecycleEventSubject = PublishSubject(); + + final DoubleLinkedQueue _observer = DoubleLinkedQueue(); + + final DoubleLinkedQueue _pendingObserver = DoubleLinkedQueue(); + + Stream get observableAppLifecycle => foregroundSubject.stream; + + Stream get observableLifecycleEvent => lifecycleEventSubject.stream; + + static const debounceDuration = Duration(milliseconds: 50); + + static final verifyLifecycleDuration = debounceDuration * 2; + + bool isAppForeground() => foregroundSubject.value; + + LifecycleManager._() { + if (kIsWeb) { + onAppForeground(); + } else { + listenApplifecycle(); + } + WidgetsBinding.instance.addObserver(this); + } + + void init() async { + final state = WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed + ? LifecycleState.resumed + : LifecycleState.paused; + lifecycleState.addEx(state); + final foreground = LifecycleManager.instance.isAppForeground(); + final routing = RoutingObserver.currentRouting; + onLifecycleChanged(LifecycleSnapshot(state, foreground, routing)); + + final lifecycleStream = Rx.combineLatest3( + lifecycleState.stream, + LifecycleManager.instance.observableAppLifecycle, + RoutingObserver.observableRouting, + (state, foreground, routing) => LifecycleSnapshot(state, foreground, routing)); + + lifecycleStream.debounceTime(debounceDuration).listen((info) { + Log.w("[$runtimeType] lifecycle changed!! $info"); + onLifecycleChanged(info); + }); + } + + void postEvent(LifecycleEvent event) { + lifecycleEventSubject.addEx(event); + } + + void addLifecycleObserver(LifecycleObserver observer) { + _pendingObserver.add(observer); + // _observer.addFirst(observer); + } + + void removeLifecycleObserver(LifecycleObserver observer) { + if (!_observer.remove(observer)) { + _pendingObserver.remove(observer); + } + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + Log.d("[$runtimeType] didChangeAppLifecycleState:$state"); + if (state == AppLifecycleState.resumed) { + lifecycleState.addEx(LifecycleState.resumed); + } else if (state != AppLifecycleState.resumed) { + lifecycleState.addIfChanged(LifecycleState.paused); + } + } + + @override + void onAppBackground() { + Log.i("onAppBackground", tag: "Lifecycle"); + // _foreground = false; + foregroundSubject.addIfChanged(false); + } + + @override + void onAppForeground() { + Log.i("onAppForeground", tag: "Lifecycle"); + // _foreground = true; + foregroundSubject.addIfChanged(true); + } + + void onLifecycleChanged(LifecycleSnapshot snapshot) { + Log.w("[$runtimeType] onLifecycleChanged:$snapshot"); + lifecycleSnapshotSubject.addEx(snapshot); + for (var observer in _observer) { + observer.onLifecycleChanged(snapshot); + } + if (_pendingObserver.isNotEmpty) { + while (_pendingObserver.isNotEmpty) { + final observer = _pendingObserver.removeFirst(); + observer.onLifecycleChanged(snapshot); + _observer.addFirst(observer); + } + } + } +} diff --git a/guru_app/packages/guru_utils/lib/log/log.dart b/guru_app/packages/guru_utils/lib/log/log.dart new file mode 100644 index 0000000..0537b57 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/log/log.dart @@ -0,0 +1,558 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter/foundation.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_utils/number/number_utils.dart'; +import 'package:guru_utils/random/random_utils.dart'; +import 'package:logger/logger.dart'; +import 'package:persistent/log/persistent_log.dart'; + +import 'dart:developer' as developer; + +/// Created by Haoyi on 4/13/21 +/// +/// + +class LogLevel { + static const verbose = 0; + static const debug = 1; + static const info = 2; + static const warning = 3; + static const error = 4; + static const wtf = 5; + + static const List levels = [ + Level.trace, + Level.debug, + Level.info, + Level.warning, + Level.error, + Level.fatal, + ]; +} + +abstract class _TerminalColor { + String call(String msg) => msg; +} + +class CmdColor extends _TerminalColor { + final String cmd; + + CmdColor(this.cmd); + + @override + String call(String msg) { + return "$cmd$msg${AnsiColor.ansiDefault}"; + } +} + +/// This class handles colorizing of terminal output. +class AnsiColor extends _TerminalColor { + /// ANSI Control Sequence Introducer, signals the terminal for new settings. + static const ansiEsc = '\x1B['; + + /// Reset all colors and options for current SGRs to terminal defaults. + static const ansiDefault = '${ansiEsc}0m'; + + final int? fg; + final int? bg; + final bool color; + + AnsiColor.none() + : fg = null, + bg = null, + color = false; + + AnsiColor.fg(this.fg) + : bg = null, + color = true; + + AnsiColor.bg(this.bg) + : fg = null, + color = true; + + @override + String toString() { + if (fg != null) { + return '${ansiEsc}38;5;${fg}m'; + } else if (bg != null) { + return '${ansiEsc}48;5;${bg}m'; + } else { + return ''; + } + } + + @override + String call(String msg) { + if (!kIsWeb && color && Platform.isAndroid) { + return '${this}$msg$ansiDefault'; + } else { + return msg; + } + } + + AnsiColor toFg() => AnsiColor.fg(bg); + + AnsiColor toBg() => AnsiColor.bg(fg); + + /// Defaults the terminal's foreground color without altering the background. + String get resetForeground => color ? '${ansiEsc}39m' : ''; + + /// Defaults the terminal's background color without altering the foreground. + String get resetBackground => color ? '${ansiEsc}49m' : ''; + + static int grey(double level) => 232 + (level.clamp(0.0, 1.0) * 23).round(); +} + +class LogRecord { + final int sequence; + final DateTime time; + final Level level; + final String? tag; + final String msg; + + const LogRecord(this.sequence, this.time, this.level, this.tag, this.msg); +} + +class Log { + static final errorFgColor = AnsiColor.fg(1); + + static String _appName = "GAME"; + + static final levelFgColors = [ + AnsiColor.none(), + AnsiColor.fg(33), + AnsiColor.fg(40), + AnsiColor.fg(208), + AnsiColor.fg(1), + AnsiColor.fg(129), + ]; + + static final tagCandidateColor = [ + AnsiColor.fg(1), + AnsiColor.fg(2), + AnsiColor.fg(3), + AnsiColor.fg(4), + AnsiColor.fg(5), + AnsiColor.fg(6), + AnsiColor.fg(9), + AnsiColor.fg(10), + AnsiColor.fg(11), + AnsiColor.fg(12), + AnsiColor.fg(13), + AnsiColor.fg(14), + AnsiColor.fg(28), + AnsiColor.fg(29), + AnsiColor.fg(30), + AnsiColor.fg(31), + AnsiColor.fg(32), + AnsiColor.fg(33), + AnsiColor.fg(34), + AnsiColor.fg(35), + AnsiColor.fg(36), + AnsiColor.fg(37), + AnsiColor.fg(38), + AnsiColor.fg(39), + AnsiColor.fg(40), + AnsiColor.fg(41), + AnsiColor.fg(42), + AnsiColor.fg(43), + AnsiColor.fg(44), + AnsiColor.fg(45), + AnsiColor.fg(46), + AnsiColor.fg(47), + AnsiColor.fg(48), + AnsiColor.fg(49), + AnsiColor.fg(50), + AnsiColor.fg(51), + AnsiColor.fg(68), + AnsiColor.fg(69), + AnsiColor.fg(70), + AnsiColor.fg(71), + AnsiColor.fg(72), + AnsiColor.fg(73), + AnsiColor.fg(74), + AnsiColor.fg(75), + AnsiColor.fg(76), + AnsiColor.fg(77), + AnsiColor.fg(78), + AnsiColor.fg(79), + AnsiColor.fg(80), + AnsiColor.fg(81), + AnsiColor.fg(82), + AnsiColor.fg(83), + AnsiColor.fg(84), + AnsiColor.fg(85), + AnsiColor.fg(86), + AnsiColor.fg(87), + AnsiColor.fg(88), + AnsiColor.fg(89), + AnsiColor.fg(90), + AnsiColor.fg(91), + AnsiColor.fg(92), + AnsiColor.fg(93), + AnsiColor.fg(94), + AnsiColor.fg(95), + AnsiColor.fg(96), + AnsiColor.fg(97), + AnsiColor.fg(98), + AnsiColor.fg(99), + AnsiColor.fg(100), + AnsiColor.fg(101), + AnsiColor.fg(102), + AnsiColor.fg(103), + AnsiColor.fg(104), + AnsiColor.fg(105), + AnsiColor.fg(106), + AnsiColor.fg(107), + AnsiColor.fg(108), + AnsiColor.fg(109), + AnsiColor.fg(110), + AnsiColor.fg(111), + AnsiColor.fg(112), + AnsiColor.fg(113), + AnsiColor.fg(114), + AnsiColor.fg(115), + AnsiColor.fg(116), + AnsiColor.fg(117), + AnsiColor.fg(118), + AnsiColor.fg(119), + AnsiColor.fg(120), + AnsiColor.fg(121), + AnsiColor.fg(122), + AnsiColor.fg(123), + AnsiColor.fg(124), + AnsiColor.fg(125), + AnsiColor.fg(126), + AnsiColor.fg(127), + AnsiColor.fg(128), + AnsiColor.fg(129), + AnsiColor.fg(130), + AnsiColor.fg(131), + AnsiColor.fg(132), + AnsiColor.fg(133), + AnsiColor.fg(134), + AnsiColor.fg(135), + AnsiColor.fg(136), + AnsiColor.fg(137), + AnsiColor.fg(138), + AnsiColor.fg(139), + AnsiColor.fg(140), + AnsiColor.fg(141), + AnsiColor.fg(142), + AnsiColor.fg(143), + AnsiColor.fg(144), + AnsiColor.fg(145), + AnsiColor.fg(146), + AnsiColor.fg(147), + AnsiColor.fg(148), + AnsiColor.fg(149), + AnsiColor.fg(150), + AnsiColor.fg(151), + AnsiColor.fg(152), + AnsiColor.fg(153), + AnsiColor.fg(154), + AnsiColor.fg(155), + AnsiColor.fg(156), + AnsiColor.fg(157), + AnsiColor.fg(158), + AnsiColor.fg(159), + AnsiColor.fg(160), + AnsiColor.fg(161), + AnsiColor.fg(162), + AnsiColor.fg(163), + AnsiColor.fg(164), + AnsiColor.fg(165), + AnsiColor.fg(166), + AnsiColor.fg(167), + AnsiColor.fg(168), + AnsiColor.fg(169), + AnsiColor.fg(170), + AnsiColor.fg(171), + AnsiColor.fg(172), + AnsiColor.fg(173), + AnsiColor.fg(174), + AnsiColor.fg(175), + AnsiColor.fg(176), + AnsiColor.fg(177), + AnsiColor.fg(178), + AnsiColor.fg(179), + AnsiColor.fg(180), + AnsiColor.fg(181), + AnsiColor.fg(182), + AnsiColor.fg(183), + AnsiColor.fg(184), + AnsiColor.fg(185), + AnsiColor.fg(186), + AnsiColor.fg(187), + AnsiColor.fg(188), + AnsiColor.fg(189), + AnsiColor.fg(190), + AnsiColor.fg(191), + AnsiColor.fg(192), + AnsiColor.fg(193), + AnsiColor.fg(194), + AnsiColor.fg(195), + AnsiColor.fg(196), + AnsiColor.fg(197), + AnsiColor.fg(198), + AnsiColor.fg(199), + AnsiColor.fg(200), + AnsiColor.fg(201), + AnsiColor.fg(202), + AnsiColor.fg(203), + AnsiColor.fg(204), + AnsiColor.fg(205), + AnsiColor.fg(206), + AnsiColor.fg(207), + AnsiColor.fg(208), + AnsiColor.fg(209), + AnsiColor.fg(210), + AnsiColor.fg(211), + AnsiColor.fg(212), + AnsiColor.fg(213), + AnsiColor.fg(214), + AnsiColor.fg(215), + AnsiColor.fg(216), + AnsiColor.fg(217), + AnsiColor.fg(218), + AnsiColor.fg(219), + AnsiColor.fg(220), + AnsiColor.fg(221), + AnsiColor.fg(222), + AnsiColor.fg(223), + AnsiColor.fg(224), + AnsiColor.fg(225), + ]; + + static final levelBgColors = [ + AnsiColor.bg(0), + AnsiColor.bg(27), + AnsiColor.bg(22), + AnsiColor.bg(130), + AnsiColor.bg(1), + AnsiColor.bg(129), + ]; + + static final levelTags = [ + AnsiColor.bg(0)(" V "), + AnsiColor.bg(27)(" D "), + AnsiColor.bg(22)(" I "), + AnsiColor.bg(130)(" W "), + AnsiColor.bg(1)(" E "), + AnsiColor.bg(129)("WTF") + ]; + + static final Map tagColors = {}; + + static final Logger _logger = Logger(printer: PrettyPrinter(methodCount: 0)); + static const persistentLogName = "guru_app"; + static int recordCount = 0; + static final DoubleLinkedQueue latestLogRecords = DoubleLinkedQueue(); + static final StreamController _logStream = StreamController.broadcast(); + static bool listening = false; + + static List> dumpRecords({String? filterTag}) { + return latestLogRecords + .where((record) => filterTag == null || filterTag == _appName || record.tag == filterTag) + .map((record) => MapEntry( + "[${record.sequence}] ${record.tag == null ? "" : "#${record.tag}#"} ${DateTimeUtils.standardDateFormat.format(record.time)}", + record.msg)) + .toList(); + } + + static bool isDebug = kDebugMode; + + static int _persistentLevel = LogLevel.debug; + + static Stream get observableLogStream => _logStream.stream; + + static Future init(String appName, + {int persistentLogFileSize = 1024 * 1024 * 10, + int persistentLogCount = 7, + int persistentLevel = LogLevel.debug}) async { + Log._appName = appName; + tagColors[_appName] = CmdColor("${AnsiColor.ansiEsc}33;1m"); + _persistentLevel = persistentLevel; + await PersistentLog.createLogger( + logName: persistentLogName, + fileSizeLimit: persistentLogFileSize, + fileCount: persistentLogCount); + } + + static void setListen(bool listen) { + listening = listen; + } + + static String _toColorTag(String? tag) { + if (tag == null) { + return ""; + } + _TerminalColor? color = tagColors[tag]; + if (color == null) { + color = tagCandidateColor[RandomUtils.nextInt(tagCandidateColor.length)]; + tagColors[tag] = color; + } + return color(" #$tag#"); + + // "${AnsiColor.ansiEsc}33;1m$name${AnsiColor.ansiDefault}"; 加粗 + // "${AnsiColor.ansiEsc}33;3m$name${AnsiColor.ansiDefault}"; 斜体 + // "${AnsiColor.ansiEsc}33;4m$name${AnsiColor.ansiDefault}"; 下滑线 + return "${AnsiColor.ansiEsc}33;1m$tag${AnsiColor.ansiDefault}"; + } + + static String _toColorName(String name) { + if (!kIsWeb && Platform.isAndroid) { + // "${AnsiColor.ansiEsc}33;1m$name${AnsiColor.ansiDefault}"; 加粗 + // "${AnsiColor.ansiEsc}33;3m$name${AnsiColor.ansiDefault}"; 斜体 + // "${AnsiColor.ansiEsc}33;4m$name${AnsiColor.ansiDefault}"; 下滑线 + return "${AnsiColor.ansiEsc}33;1m$name${AnsiColor.ansiDefault}"; + } + return name; + } + + static String _toDateTime(DateTime dateTime) { + final humanDt = + "${NumberUtils.twoDigits(dateTime.hour)}:${NumberUtils.twoDigits(dateTime.minute)}:${NumberUtils.twoDigits(dateTime.second)}.${NumberUtils.threeDigits(dateTime.millisecondsSinceEpoch % 1000)}"; + if (!kIsWeb && Platform.isAndroid) { + return "${AnsiColor.ansiEsc}36;3m<$humanDt>${AnsiColor.ansiDefault}"; + } + return "<$humanDt>"; + } + + static void _log(int level, String msgData, + {String? tag, + dynamic error, + StackTrace? stackTrace, + bool syncFirebase = false, + bool persistent = false}) { + final now = DateTime.now(); + final msg = msgData.substring(0, min(4096, msgData.length)); + recordCount++; + // latestLogRecords.addFirst(MapEntry("[$recordCount] ${DateTimeUtils.humanTime}", msg)); + final logRecord = + LogRecord(recordCount, now, LogLevel.levels[level], tag, "$msg${error != null ? '!![$error]!!' : ''}"); + latestLogRecords.addFirst(logRecord); + if (latestLogRecords.length > 2000) { + latestLogRecords.removeLast(); + } + + if (stackTrace != null) { + _logger.log(LogLevel.levels[level], "[$_appName] ${tag == null ? "" : "#$tag#"} $msg", + error: error, stackTrace: stackTrace); + } else { + final color = levelFgColors[level]; + String output; + if (error != null) { + output = "${color(msg)}[${errorFgColor(error.toString())}]"; + } else { + output = color(msg); + } + + if (isDebug) { + // ignore: avoid_print + print( + "${_toColorName("[$_appName]")} ${_toDateTime(now)} ${levelTags[level]}${_toColorTag(tag)} $output"); + } + } + + if (!kIsWeb && (level >= _persistentLevel || persistent)) { + PersistentLog.log( + logName: persistentLogName, message: "$msg ${error != null ? error.toString() : ''}"); + } + + if (syncFirebase) { + // Analytics.instance.logFirebase("$msg ${error != null ? error.toString() : ''}"); + // if (error != null) { + // if (syncCrashlytics) { + // Analytics.instance + // .logException(M2BlocksException(error, cause: error), stacktrace: stackTrace); + // } + // } else { + // Analytics.instance.logFirebase(msg); + // } + } + + if (listening) { + _logStream.add(logRecord); + } + } + + static void v(String msg, + {String? tag, + dynamic error, + StackTrace? stackTrace, + bool syncFirebase = false, + bool syncCrashlytics = false, + bool persistent = false}) { + _log(LogLevel.verbose, msg, + tag: tag, + error: error, + stackTrace: stackTrace, + syncFirebase: syncFirebase, + persistent: persistent); + } + + static void d(String msg, + {String? tag, + dynamic error, + StackTrace? stackTrace, + bool syncFirebase = false, + bool syncCrashlytics = false, + bool persistent = false}) { + _log(LogLevel.debug, msg, + tag: tag, + error: error, + stackTrace: stackTrace, + syncFirebase: syncFirebase, + persistent: persistent); + } + + static void i(String msg, + {String? tag, + dynamic error, + StackTrace? stackTrace, + bool syncFirebase = false, + bool syncCrashlytics = false, + bool persistent = false}) { + _log(LogLevel.info, msg, + tag: tag, + error: error, + stackTrace: stackTrace, + syncFirebase: syncFirebase, + persistent: persistent); + } + + static void w(String msg, + {String? tag, + dynamic error, + StackTrace? stackTrace, + bool syncFirebase = false, + bool syncCrashlytics = false, + bool persistent = false}) { + _log(LogLevel.warning, msg, + tag: tag, + error: error, + stackTrace: stackTrace, + syncFirebase: syncFirebase, + persistent: persistent); + } + + static void e(String msg, + {String? tag, + dynamic error, + StackTrace? stackTrace, + bool syncFirebase = false, + bool syncCrashlytics = false, + bool persistent = false}) { + _log(LogLevel.error, msg, + tag: tag, + error: error, + stackTrace: stackTrace, + syncFirebase: syncFirebase, + persistent: persistent); + } +} diff --git a/guru_app/packages/guru_utils/lib/manifest/manifest.dart b/guru_app/packages/guru_utils/lib/manifest/manifest.dart new file mode 100644 index 0000000..39e73d3 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/manifest/manifest.dart @@ -0,0 +1,169 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:json_annotation/json_annotation.dart'; + +/// Created by Haoyi on 2021/7/1 + +part "manifest.g.dart"; + +class DetailsReservedType { + static const String igc = "igc"; +} + +class ExtraReservedField { + static const String scene = "__scene"; + static const String offerId = "__offer_id"; + static const String basePlanId = "__base_plan_id"; + static const String sales = "__sales"; + static const String rate = "__rate"; +} + +class DetailsReservedField { + static const String type = "__type"; + static const String amount = "__amount"; + static const String name = "__name"; + static const String icon = "__icon"; +} + +class ManifestStringConvert implements JsonConverter { + const ManifestStringConvert(); + + @override + Manifest fromJson(String json) { + if (json.isEmpty) { + return Manifest.empty; + } + final map = jsonDecode(json); + return Manifest.fromJson(map); + } + + @override + String toJson(Manifest manifest) { + return jsonEncode(manifest); + } +} + +const ManifestStringConvert manifestStringConvert = ManifestStringConvert(); + +@JsonSerializable(constructor: "_") +class Details { + @JsonKey(name: "bundle", defaultValue: {}) + final Map bundle; + + const Details._({this.bundle = const {}}); + + Details.define(String type, int amount, {Map params = const {}}) + : bundle = { + DetailsReservedField.type: type, + DetailsReservedField.amount: amount + }; + + factory Details.fromJson(Map json) => _$DetailsFromJson(json); + + int get amount => bundle[DetailsReservedField.amount] ?? 0; + + String get type => bundle[DetailsReservedField.type] ?? "unknown"; + + Map toJson() => _$DetailsToJson(this); + + void merge(Details details) { + bundle.addAll(details.bundle); + } + + bool containsKey(String name) { + return bundle.containsKey(name); + } + + void setInt(String key, int value) { + bundle[key] = value.toString(); + } + + void setDouble(String key, double value) { + bundle[key] = value.toString(); + } + + void setString(String key, String value) { + bundle[key] = value; + } + + void setBool(String key, bool value) { + bundle[key] = value; + } + + int? getInt(String key) { + final value = bundle[key]; + if (value == null) { + return null; + } + return int.parse(value); + } + + double? getDouble(String key) { + final value = bundle[key]; + if (value == null) { + return null; + } + return double.parse(value); + } + + String? getString(String key) { + return bundle[key]; + } + + bool? getBool(String key) { + final value = bundle[key]; + if (value == null) { + return null; + } + return value == true; + } + + Future forEach(void Function(String key, String value) f) async { + for (var entry in bundle.entries) { + f.call(entry.key, entry.value); + } + } +} + +@JsonSerializable(explicitToJson: true) +class Manifest { + @JsonKey(name: "category", defaultValue: "") + final String category; + + @JsonKey(name: "extras", defaultValue: {}) + final Map extras; + + @JsonKey(name: "details", defaultValue:
[]) + final List
details; + + String get scene => extras[ExtraReservedField.scene] ?? ""; + + String? get basePlanId => extras[ExtraReservedField.basePlanId]; + + String? get offerId => extras[ExtraReservedField.offerId]; + + const Manifest(this.category, + {this.extras = const {}, this.details = const
[]}); + + Manifest offer(String offerId, String basePlanId) { + return Manifest(category, + extras: { + ...extras, + ...{ExtraReservedField.offerId: offerId, ExtraReservedField.basePlanId: basePlanId} + }, + details: details); + } + + factory Manifest.fromJson(Map json) => _$ManifestFromJson(json); + + static const Manifest empty = Manifest("empty"); + + Map toJson() => _$ManifestToJson(this); +} + +extension DefaultManifestExtension on Manifest { + int get igcAmount => details + .map((details) => details.type == DetailsReservedType.igc ? details.amount : 0) + .reduce((value, element) => element + value); +} diff --git a/guru_app/packages/guru_utils/lib/manifest/manifest.g.dart b/guru_app/packages/guru_utils/lib/manifest/manifest.g.dart new file mode 100644 index 0000000..c79a7d3 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/manifest/manifest.g.dart @@ -0,0 +1,30 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'manifest.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Details _$DetailsFromJson(Map json) => Details._( + bundle: json['bundle'] as Map? ?? {}, + ); + +Map _$DetailsToJson(Details instance) => { + 'bundle': instance.bundle, + }; + +Manifest _$ManifestFromJson(Map json) => Manifest( + json['category'] as String? ?? '', + extras: json['extras'] as Map? ?? {}, + details: (json['details'] as List?) + ?.map((e) => Details.fromJson(e as Map)) + .toList() ?? + [], + ); + +Map _$ManifestToJson(Manifest instance) => { + 'category': instance.category, + 'extras': instance.extras, + 'details': instance.details.map((e) => e.toJson()).toList(), + }; diff --git a/guru_app/packages/guru_utils/lib/math/math_utils.dart b/guru_app/packages/guru_utils/lib/math/math_utils.dart new file mode 100644 index 0000000..3617c5b --- /dev/null +++ b/guru_app/packages/guru_utils/lib/math/math_utils.dart @@ -0,0 +1,103 @@ +import 'dart:math'; +import 'dart:ui'; +import 'package:vector_math/vector_math_64.dart'; + +/// Created by @Haoyi on 2021/7/7 +/// +class MathUtils { + static double toRadian(double angle) { + return angle * (pi / 180.0); + } + + static double toAngle(double radian) { + return radian * (180.0 / pi); + } + + static Offset transformOffset(Matrix4 matrix4, Offset offset) { + Vector3 _vector3 = Vector3(offset.dx, offset.dy, 0); + matrix4.transform3(_vector3); + return Offset(_vector3.x, _vector3.y); + } + + static Rect transformRect(Matrix4 matrix4, Rect rect) { + final offset1 = transformOffset(matrix4, rect.topLeft); + final offset2 = transformOffset(matrix4, rect.bottomRight); + return Rect.fromLTRB(min(offset1.dx, offset2.dx), min(offset1.dy, offset2.dy), max(offset1.dx, offset2.dx), max(offset1.dy, offset2.dy)); + } + + static Matrix4 createBoundaryRotationMatrix4(Rect boundary, double radians) { + final _center = boundary.center; + return Matrix4.identity() + ..translate(_center.dx, _center.dy) + ..rotateZ(radians) + ..translate(-_center.dx, -_center.dy); + } + + static const _fibonacci_array = [ + 1, + 1, + 2, + 3, + 5, + 8, + 13, + 21, + 34, + 55, + 89, + 144, + 233, + 377, + 610, + 987, + 1597, + 2584, + 4181, + 6765, + 10946, + 17711, + 28657, + 46368, + 75025, + 121393, + 196418, + 317811, + 514229, + 832040, + 1346269, + 2178309, + 3524578, + 5702887, + 9227465, + 14930352, + 24157817, + 39088169, + 63245986, + 102334155, + 165580141, + 267914296, + 433494437, + 701408733, + 1134903170, + 1836311903, + 2971215073, + 4807526976, + 7778742049, + 12586269025 + ]; + + static int fibonacci(int n, {int offset = 0}) { + final len = _fibonacci_array.length; + final idx = n + offset; + if (idx < len) { + return _fibonacci_array[idx]; + } + int n1 = _fibonacci_array[len - 2], n2 = _fibonacci_array[len - 1], sum = 0; + for (int i = _fibonacci_array.length; i <= idx; i++) { + sum = n1 + n2; + n1 = n2; + n2 = sum; + } + return sum; + } +} diff --git a/guru_app/packages/guru_utils/lib/network/network_utils.dart b/guru_app/packages/guru_utils/lib/network/network_utils.dart new file mode 100644 index 0000000..c4fb19b --- /dev/null +++ b/guru_app/packages/guru_utils/lib/network/network_utils.dart @@ -0,0 +1,79 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/log/log.dart'; + +/// Created by Haoyi on 2022/8/16 + +class ConnectivityTrack { + final ConnectivityResult oldResult; + final ConnectivityResult newResult; + + static const ConnectivityTrack invalid = + ConnectivityTrack(ConnectivityResult.none, ConnectivityResult.none); + + const ConnectivityTrack(this.newResult, this.oldResult); + + ConnectivityTrack update(ConnectivityResult newResult) => + ConnectivityTrack(newResult, this.newResult); + + @override + String toString() { + return 'ConnectivityTrack{$oldResult => $newResult}'; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ConnectivityTrack && + runtimeType == other.runtimeType && + oldResult == other.oldResult && + newResult == other.newResult; + + @override + int get hashCode => oldResult.hashCode ^ newResult.hashCode; +} + +class NetworkUtils { + static final _connectivity = Connectivity(); + + static ConnectivityResult? _mockConnectivityStatus; + + static final BehaviorSubject _connectivityTrackSubject = + BehaviorSubject.seeded(ConnectivityTrack.invalid); + + static Stream get observableConnectivityTrack => + _connectivityTrackSubject.stream; + + static void setMockConnectivityStatus(ConnectivityResult? status) { + _mockConnectivityStatus = status; + } + + static void _updateConnectivityStatus(ConnectivityResult status) { + final track = _connectivityTrackSubject.value.update(status); + _connectivityTrackSubject.addIfChanged(track); + Log.i("_updateConnectivityStatus: Network track $track"); + } + + static Future init() async { + final status = await _connectivity.checkConnectivity(); + _updateConnectivityStatus(status); + _connectivity.onConnectivityChanged.listen((newResult) { + Log.d("Network status changed! $newResult"); + _updateConnectivityStatus(newResult); + }); + } + + static Future isNetworkConnected() async { + try { + final connectivity = _mockConnectivityStatus ?? await _connectivity.checkConnectivity(); + if (connectivity == ConnectivityResult.none) { + Log.w( + "The current network status is invalid! Please try again after connecting to the network"); + return false; + } + return true; + } catch (error, stacktrace) { + return false; + } + } +} diff --git a/guru_app/packages/guru_utils/lib/number/number_utils.dart b/guru_app/packages/guru_utils/lib/number/number_utils.dart new file mode 100644 index 0000000..cdc7519 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/number/number_utils.dart @@ -0,0 +1,126 @@ +/// Created by @Haoyi on 4/13/21 +/// +class NumberUtils { + static String twoDigits(int n) { + if (n >= 10) return "$n"; + return "0$n"; + } + + static String threeDigits(int n) { + if (n >= 100) return "$n"; + if (n >= 10) return "0$n"; + return "00$n"; + } + + static String fourDigits(int n, {String placeholder = '0', int align = 0}) { + if (n >= 1000) return "$n"; + if (n >= 100) return "$placeholder$n"; + if (n >= 10) return align == 0 ? "$placeholder$placeholder$n" : "$placeholder$n$placeholder"; + return align == 0 ? "$placeholder$placeholder$placeholder$n" : "${placeholder}0$n$placeholder"; + } + + static String fiveDigits(int n) { + if (n >= 10000) return "$n"; + if (n >= 1000) return "0$n"; + if (n >= 100) return "00$n"; + if (n >= 10) return "000$n"; + return "0000$n"; + } + + static String sixDigits(int n) { + if (n >= 100000) return "$n"; + if (n >= 10000) return "0$n"; + if (n >= 1000) return "00$n"; + if (n >= 100) return "000$n"; + if (n >= 10) return "0000$n"; + return "00000$n"; + } + + static String sevenDigits(int n) { + if (n >= 1000000) return "$n"; + if (n >= 100000) return "0$n"; + if (n >= 10000) return "00$n"; + if (n >= 1000) return "000$n"; + if (n >= 100) return "0000$n"; + if (n >= 10) return "00000$n"; + return "000000$n"; + } + + static String eightDigits(int n) { + if (n >= 10000000) return "$n"; + if (n >= 1000000) return "0$n"; + if (n >= 100000) return "00$n"; + if (n >= 10000) return "000$n"; + if (n >= 1000) return "0000$n"; + if (n >= 100) return "00000$n"; + if (n >= 10) return "000000$n"; + return "0000000$n"; + } + + static String nineDigits(int n) { + if (n >= 100000000) return "$n"; + if (n >= 10000000) return "0$n"; + if (n >= 1000000) return "00$n"; + if (n >= 100000) return "000$n"; + if (n >= 10000) return "0000$n"; + if (n >= 1000) return "00000$n"; + if (n >= 100) return "000000$n"; + if (n >= 10) return "0000000$n"; + return "00000000$n"; + } + + static String tenDigits(int n) { + if (n >= 1000000000) return "$n"; + if (n >= 100000000) return "0$n"; + if (n >= 10000000) return "00$n"; + if (n >= 1000000) return "000$n"; + if (n >= 100000) return "0000$n"; + if (n >= 10000) return "00000$n"; + if (n >= 1000) return "000000$n"; + if (n >= 100) return "0000000$n"; + if (n >= 10) return "00000000$n"; + return "000000000$n"; + } + + static String nDigits(int value, int n) { + final result = value.toString(); + final supplement = n - result.length; + if (supplement > 0) { + StringBuffer zero = StringBuffer(); + for (int i = 0; i < supplement; ++i) { + zero.write("0"); + } + return "${zero.toString()}$result"; + } + return result; + } + + static int getUnsignedNumLength(int num) { + if (num < 10) return 1; + if (num < 100) return 2; + if (num < 1000) return 3; + if (num < 10000) return 4; + if (num < 100000) return 5; + if (num < 1000000) return 6; + if (num < 10000000) return 7; + if (num < 100000000) return 8; + if (num < 1000000000) return 9; + if (num < 10000000000) return 10; + int count = 11; + for (var tmp = num ~/ 100000000000; tmp != 0; tmp ~/= 10) { + count++; + } + return count; + } + + static int safeParseInt(String? numStr, {required int defNum, int? radix}) { + if (numStr == null || numStr == '') { + return defNum; + } + try { + return int.parse(numStr, radix: radix); + } catch (error) { + return defNum; + } + } +} diff --git a/guru_app/packages/guru_utils/lib/number/range.dart b/guru_app/packages/guru_utils/lib/number/range.dart new file mode 100644 index 0000000..6c76e8f --- /dev/null +++ b/guru_app/packages/guru_utils/lib/number/range.dart @@ -0,0 +1,57 @@ + +import 'package:guru_utils/datetime/datetime_utils.dart'; + +/// Created by Haoyi on 2020/8/29 +/// + +enum RangeType { + INCLUSIVE_INCLUSIVE, // [begin, end] + INCLUSIVE_EXCLUSIVE, // [begin, end) + EXCLUSIVE_INCLUSIVE, // (begin, end] + EXCLUSIVE_EXCLUSIVE // (begin, end) +} + +class Range { + final num begin; + final num end; + final T? data; + + const Range(this.begin, this.end, {this.data}); + + Range.sinceNow(Duration duration, {this.data}) + : begin = DateTimeUtils.currentTimeInSecond(), + end = DateTimeUtils.currentTimeInSecond() + duration.inSeconds; + + static Range? fromString(String text) { + if (text.isNotEmpty != true) { + return null; + } + final values = text.split(":"); + try { + if (values.length >= 2) { + return Range(int.parse(values[0]), int.parse(values[1])); + } + } catch (error) {} + return null; + } + + bool inRange(num value, {RangeType type = RangeType.INCLUSIVE_INCLUSIVE}) { + switch (type) { + case RangeType.INCLUSIVE_INCLUSIVE: + return value >= begin && value <= end; + case RangeType.INCLUSIVE_EXCLUSIVE: + return value >= begin && value < end; + case RangeType.EXCLUSIVE_INCLUSIVE: + return value > begin && value <= end; + default: + return value > begin && value < end; + } + } + + @override + String toString() { + return "$begin:$end"; + } + + num get interval => end - begin; +} diff --git a/guru_app/packages/guru_utils/lib/packages/guru_package.dart b/guru_app/packages/guru_utils/lib/packages/guru_package.dart new file mode 100644 index 0000000..8f33b12 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/packages/guru_package.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +/// Created by Haoyi on 2023/1/16 + +abstract class GuruPackage { + final String package; + + final List children = []; + + final int priority; + + final bool flattenChildrenAsyncInit; + + GuruPackage(this.package, {this.priority = 1, this.flattenChildrenAsyncInit = false}); + + void addPackage(GuruPackage package) { + children.add(package); + } + + Future initialize(); + + Future initializeAsync() async {} + + Iterable get supportedLocales; + + Iterable> get localizationsDelegates; +} + +abstract class RootPackage extends GuruPackage { + RootPackage() : super("root"); +} diff --git a/guru_app/packages/guru_utils/lib/property/app_property.dart b/guru_app/packages/guru_utils/lib/property/app_property.dart new file mode 100644 index 0000000..3e56a52 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/property/app_property.dart @@ -0,0 +1,358 @@ +library property; + +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; +import 'dart:math'; + +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/property/property_delegate.dart'; +import 'package:guru_utils/property/storage/property_storage.dart'; +import 'package:guru_utils/quiver/cache/map_cache.dart'; + +/// Created by @Haoyi on 2020/12/4 +/// + +part "property_bundle.dart"; + +part "property_key.dart"; + +extension PropertyExtension on Map { + List> toNamedMapEntries() { + final List> result = []; + for (var entry in entries) { + result.add(MapEntry(entry.key.key, entry.value.toString())); + } + return result; + } +} + +class AppProperty implements PropertyDelegate { + static late AppProperty _instance; + + static AppProperty getInstance() => _mock ?? _instance; + + static AppProperty? _mock; + + final PropertyStorage _storage; + + final MapCache _cache; + + AppProperty._(this._storage, {int cacheSize = 256}) + : _cache = MapCache.lru(maximumSize: cacheSize); + + static void setMock(AppProperty mock) { + _mock = mock; + } + + static void initialize(PropertyStorage storage, {int cacheSize = 256}) { + _instance = AppProperty._(storage, cacheSize: cacheSize); + } + + @override + Future setInt(PropertyKey key, int value) { + return _setValue(key, value.toString()); + } + + Future getAndIncrease(PropertyKey key, {int defValue = 0, String? tag}) async { + final count = await getInt(key, defValue: defValue); + await setInt(key, count + 1); + return count; + } + + Future increaseAndGet(PropertyKey key, {int defValue = 0}) async { + final count = await getInt(key, defValue: defValue); + await setInt(key, count + 1); + return count + 1; + } + + Future decreaseAndGet(PropertyKey key, {int defValue = 0}) async { + final count = await getInt(key, defValue: defValue); + await setInt(key, count - 1); + return count - 1; + } + + Future getAndDecrease(PropertyKey key, {int defValue = 0, String? tag}) async { + final count = await getInt(key, defValue: defValue); + await setInt(key, count - 1); + return count; + } + + @override + Future setDouble(PropertyKey key, double value) { + return _setValue(key, value.toString()); + } + + @override + Future setString(PropertyKey key, String value) { + return _setValue(key, value); + } + + @override + Future setBool(PropertyKey key, bool value) async { + return await _setValue(key, value ? "1" : "0"); + } + + Future _setValue(PropertyKey key, String value) { + _cache.set(key, value); + return _storage.setProperty(key, value); + } + + Future setProperties(PropertyBundle bundle) async { + _cache.addAll(bundle.data); + return await _storage.setProperties(bundle.data); + } + + Future> loadAllValues() { + return _storage.loadAllProperties(); + } + + Future _loadValue(PropertyKey key) { + return _storage.getProperty(key); + } + + Future _loadOrCreateValue(PropertyKey key, {String? Function()? ifAbsent}) { + return _storage.getOrCreateProperty(key, ifAbsent: ifAbsent); + } + + Future> _loadValues(List keys) { + return _storage.getProperties(keys); + } + + FutureOr _getValue(PropertyKey key) async { + return await _cache.get(key, ifAbsent: _loadValue); + } + + FutureOr _getValueIfExists(PropertyKey key) async { + return await _cache.get(key); + } + + Future> getValues(List keys) async { + final result = {}; + final loadKeys = []; + for (var key in keys) { + final value = await _cache.get(key); + if (value != null) { + result[key] = value; + } else { + loadKeys.add(key); + } + } + + final map = await _loadValues(loadKeys); + for (var entry in map.entries) { + _cache.set(entry.key, entry.value); + } + result.addAll(map); + return result; + } + + @override + Future getInt(PropertyKey key, {required int defValue}) async { + try { + final value = await _getValue(key); + if (value != null) { + return int.parse(value); + } + } catch (error, stacktrace) { + Log.d("getInt error!", error: error, stackTrace: stacktrace); + } + return defValue; + } + + @override + Future getDouble(PropertyKey key, {required double defValue}) async { + try { + final value = await _getValue(key); + if (value != null) { + return double.parse(value); + } + } catch (error, stacktrace) { + Log.w("getDouble error!", error: error, stackTrace: stacktrace); + } + return defValue; + } + + @override + Future getString(PropertyKey key, {required String defValue}) async { + try { + final value = await _getValue(key); + if (value != null) { + return value; + } + } catch (error, stacktrace) { + Log.w("getString error!", error: error, stackTrace: stacktrace); + } + return defValue; + } + + @override + Future getBool(PropertyKey key, {required bool defValue}) async { + try { + final value = await _getValue(key); + if (value != null) { + return value == "1"; + } + } catch (error, stacktrace) { + Log.d("getString error!", error: error, stackTrace: stacktrace); + } + return defValue; + } + + Future _getOrCreateValue(PropertyKey key, String Function() ifAbsent) async { + return await _cache.get(key, ifAbsent: (key) => _loadOrCreateValue(key, ifAbsent: ifAbsent)); + } + + Future getOrCreateInt(PropertyKey key, int ifAbsent) async { + try { + final value = await _getOrCreateValue(key, () => ifAbsent.toString()); + if (value != null) { + return int.parse(value); + } + } catch (error, stacktrace) { + Log.w("getOrCreateInt error!", error: error, stackTrace: stacktrace); + } + return ifAbsent; + } + + Future getOrCreateDouble(PropertyKey key, double ifAbsent) async { + try { + final value = await _getOrCreateValue(key, () => ifAbsent.toString()); + if (value != null) { + return double.parse(value); + } + } catch (error, stacktrace) { + Log.w("getOrCreateDouble error!", error: error, stackTrace: stacktrace); + } + return ifAbsent; + } + + Future getOrCreateString(PropertyKey key, String ifAbsent) async { + try { + final value = await _getOrCreateValue(key, () => ifAbsent); + if (value != null) { + return value; + } + } catch (error, stacktrace) { + Log.w("getOrCreateString error!", error: error, stackTrace: stacktrace); + } + return ifAbsent; + } + + Future getOrCreateBool(PropertyKey key, bool ifAbsent) async { + try { + final value = await _getOrCreateValue(key, () => ifAbsent ? "1" : "0"); + if (value != null) { + return value == "1"; + } + } catch (error, stacktrace) { + Log.d("getOrCreateBool error!", error: error, stackTrace: stacktrace); + } + return ifAbsent; + } + + @override + Future loadValuesByTag(String tag) async { + try { + final map = await _storage.getPropertiesByTag(tag); + for (var entry in map.entries) { + _cache.set(entry.key, entry.value); + } + return PropertyBundle(map: map); + } catch (error, stacktrace) { + return PropertyBundle.empty(); + } + } + + @override + Future loadValuesByUsage(int usage) async { + try { + final map = await _storage.getPropertiesByUsage(usage); + for (var entry in map.entries) { + _cache.set(entry.key, entry.value); + } + return PropertyBundle(map: map); + } catch (error, stacktrace) { + return PropertyBundle.empty(); + } + } + + @override + Future remove(PropertyKey key) async { + try { + _cache.invalidate(key); + return await _storage.removeProperty(key); + } catch (error, stacktrace) { + Log.w("remove error! $error", error: error, stackTrace: stacktrace); + } + return false; + } + + @override + Future removeAllWithTag(String tag) async { + try { + final keys = await _storage.removeAllWithTag(tag); + for (var key in keys) { + if (key is String) { + _cache.invalidate(key); + } + } + return true; + } catch (error, stacktrace) { + Log.w("removeAllWithTag error! $error", error: error, stackTrace: stacktrace); + } + return false; + } + + @override + Future getBoolOrNull(PropertyKey key, {bool? defValue}) async { + try { + final value = await _getValue(key); + if (value != null) { + return value == "1"; + } + } catch (error, stacktrace) { + Log.w("getString error!", error: error, stackTrace: stacktrace); + } + return defValue; + } + + @override + Future getDoubleOrNull(PropertyKey key, {double? defValue}) async { + try { + final value = await _getValue(key); + if (value != null) { + return double.parse(value); + } + } catch (error, stacktrace) { + Log.w("getDouble error!", error: error, stackTrace: stacktrace); + } + return defValue; + } + + @override + Future getIntOrNull(PropertyKey key, {int? defValue}) async { + try { + final value = await _getValue(key); + if (value != null) { + return int.parse(value); + } + } catch (error, stacktrace) { + Log.w("getInt error!", error: error, stackTrace: stacktrace); + } + return defValue; + } + + @override + Future getStringOrNull(PropertyKey key, {String? defValue}) async { + try { + final value = await _getValue(key); + if (value != null) { + return value; + } + } catch (error, stacktrace) { + Log.w("getString error!", error: error, stackTrace: stacktrace); + } + return defValue; + } +} diff --git a/guru_app/packages/guru_utils/lib/property/property.dart b/guru_app/packages/guru_utils/lib/property/property.dart new file mode 100644 index 0000000..397e316 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/property/property.dart @@ -0,0 +1,11 @@ +library property; + +/// Created by Haoyi on 2022/10/6 +/// +export 'app_property.dart'; +export 'property_delegate.dart'; +export 'storage/property_storage.dart'; + +export 'settings/settings_data.dart'; + +export 'runtime_property.dart'; diff --git a/guru_app/packages/guru_utils/lib/property/property_bundle.dart b/guru_app/packages/guru_utils/lib/property/property_bundle.dart new file mode 100644 index 0000000..a0549a9 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/property/property_bundle.dart @@ -0,0 +1,82 @@ +/// Created by @Haoyi on 4/19/21 + +part of "app_property.dart"; + +class PropertyBundle { + final Map data; + + PropertyBundle({Map? map}) : data = {} { + if (map != null) { + data.addAll(map); + } + } + + PropertyBundle.empty() : data = {}; + + bool containsName(PropertyKey name) { + return data.containsKey(name); + } + + void intersection(Map map) { + final keySets = map.keys.toSet(); + final currentSets = data.keys.toSet(); + + final sets = currentSets.intersection(keySets); + + data.removeWhere((key, _) => !sets.contains(key)); + } + + void setInt(PropertyKey key, int value) { + data[key] = value.toString(); + } + + void setDouble(PropertyKey key, double value) { + data[key] = value.toString(); + } + + void setString(PropertyKey key, String value) { + data[key] = value; + } + + void setBool(PropertyKey key, bool value) { + data[key] = value ? "1" : "0"; + } + + int? getInt(PropertyKey key) { + final value = data[key]; + if (value == null) { + return null; + } + return int.parse(value); + } + + double? getDouble(PropertyKey key) { + final value = data[key]; + if (value == null) { + return null; + } + return double.parse(value); + } + + String? getString(PropertyKey key) { + return data[key]; + } + + bool? getBool(PropertyKey key) { + final value = data[key]; + if (value == null) { + return null; + } + return value == "1"; + } + + Future forEach(void Function(PropertyKey key, String value) f) async { + for (var entry in data.entries) { + f.call(entry.key, entry.value); + } + } + + void clear() { + data.clear(); + } +} diff --git a/guru_app/packages/guru_utils/lib/property/property_delegate.dart b/guru_app/packages/guru_utils/lib/property/property_delegate.dart new file mode 100644 index 0000000..7068ec7 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/property/property_delegate.dart @@ -0,0 +1,39 @@ +import 'dart:async'; + +import 'app_property.dart'; + +/// Created by Haoyi on 2022/2/26 + +abstract class PropertyDelegate { + FutureOr setDouble(PropertyKey key, double value); + + FutureOr setInt(PropertyKey key, int value); + + FutureOr setBool(PropertyKey key, bool value); + + FutureOr setString(PropertyKey key, String value); + + FutureOr getDoubleOrNull(PropertyKey key, {double? defValue}); + + FutureOr getIntOrNull(PropertyKey key, {int? defValue}); + + FutureOr getBoolOrNull(PropertyKey key, {bool? defValue}); + + FutureOr getStringOrNull(PropertyKey key, {String? defValue}); + + FutureOr getDouble(PropertyKey key, {required double defValue}); + + FutureOr getInt(PropertyKey key, {required int defValue}); + + FutureOr getBool(PropertyKey key, {required bool defValue}); + + FutureOr getString(PropertyKey key, {required String defValue}); + + FutureOr remove(PropertyKey key); + + FutureOr removeAllWithTag(String tag); + + FutureOr loadValuesByTag(String tag); + + FutureOr loadValuesByUsage(int usage); +} diff --git a/guru_app/packages/guru_utils/lib/property/property_key.dart b/guru_app/packages/guru_utils/lib/property/property_key.dart new file mode 100644 index 0000000..35b19bd --- /dev/null +++ b/guru_app/packages/guru_utils/lib/property/property_key.dart @@ -0,0 +1,51 @@ +/// Created by @Haoyi on 2020/12/4 +/// + +part of "app_property.dart"; + +class PropertyUsage { + static const int general = 0; + static const int setting = 1; +} + +class PropertyKey { + static const defaultGroup = "guru"; + + final String name; + final String group; + final String tag; + final int usage; + + final String key; + + const PropertyKey.general(this.name, {this.group = defaultGroup, this.tag = ''}) + : key = "$group@$name", + usage = PropertyUsage.general; + + const PropertyKey.setting(this.name, {this.group = defaultGroup, this.tag = ''}) + : key = "$group@$name", + usage = PropertyUsage.setting; + + const PropertyKey( + {required this.name, required this.usage, required this.group, required this.tag}) + : key = "$group@$name"; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is PropertyKey && + runtimeType == other.runtimeType && + name == other.name && + group == other.group && + tag == other.tag && + usage == other.usage && + key == other.key; + + @override + int get hashCode => name.hashCode ^ group.hashCode ^ tag.hashCode ^ usage.hashCode ^ key.hashCode; + + @override + String toString() { + return '#$tag#[$key]($usage)'; + } +} diff --git a/guru_app/packages/guru_utils/lib/property/property_model.dart b/guru_app/packages/guru_utils/lib/property/property_model.dart new file mode 100644 index 0000000..ab2e2d8 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/property/property_model.dart @@ -0,0 +1,47 @@ +import 'package:guru_utils/property/app_property.dart'; + +/// Created by Haoyi on 2023/3/9 + +class UtilsPropertyKeys { + static const defaultGroup = "debug"; + + static const PropertyKey fakeInterstitialAds = + PropertyKey.general("fake_interstitial_ads", tag: UtilsPropertyTags.ads); + + static const PropertyKey fakeRewardedAds = + PropertyKey.general("fake_rewarded_ads", tag: UtilsPropertyTags.ads); + + static const PropertyKey forceGdpr = PropertyKey.setting("force_gdpr", group: defaultGroup); + + static const PropertyKey forceGdprType = + PropertyKey.setting("force_gdpr_type", tag: UtilsPropertyTags.consent, group: defaultGroup); + + static const PropertyKey consentDebugGeography = PropertyKey.setting( + "admob_consent_debug_geography", + tag: UtilsPropertyTags.consent, + group: defaultGroup); + + static const PropertyKey latestShownFundingChoicesTime = PropertyKey.setting( + "latest_shown_funding_choices_time", + tag: UtilsPropertyTags.consent, + group: defaultGroup); + + static const PropertyKey admobConsentTestDeviceId = PropertyKey.setting( + "admob_consent_test_device_id", + tag: UtilsPropertyTags.consent, + group: defaultGroup); + + static const PropertyKey forcePubmatic = + PropertyKey.setting("force_pubmatic", tag: UtilsPropertyTags.ads, group: defaultGroup); +} + +class UtilsPropertyTags { + static const String iap = "iap"; + static const String ads = "ads"; + static const String timer = "timer"; + static const String igc = "igc"; + static const String financial = "financial"; + static const String settings = "settings"; + static const String analytics = "analytics"; + static const String consent = "consent"; +} diff --git a/guru_app/packages/guru_utils/lib/property/runtime_property.dart b/guru_app/packages/guru_utils/lib/property/runtime_property.dart new file mode 100644 index 0000000..6302d27 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/property/runtime_property.dart @@ -0,0 +1,183 @@ +import 'dart:async'; + +import 'package:guru_utils/property/app_property.dart'; +import 'package:guru_utils/property/property_model.dart'; + +import 'property_delegate.dart'; + +/// Created by Haoyi on 2022/8/25 + +class RuntimeProperty implements PropertyDelegate { + final Map _properties = {}; + + final Map tagBundles = {}; + + static RuntimeProperty instance = RuntimeProperty._(); + + RuntimeProperty._(); + + List> toMapEntries() { + final List> result = []; + for (var entry in _properties.entries) { + result.add(MapEntry(entry.key, entry.value.toString())); + } + return result; + } + + List> toNamedMapEntries() { + final List> result = []; + for (var entry in _properties.entries) { + result.add(MapEntry(entry.key.key, entry.value.toString())); + } + return result; + } + + @override + bool getBool(PropertyKey key, {required bool defValue}) { + return _properties[key] ?? defValue; + } + + @override + bool? getBoolOrNull(PropertyKey key, {bool? defValue}) { + return _properties[key] ?? defValue; + } + + @override + double getDouble(PropertyKey key, {required double defValue}) { + return _properties[key] ?? defValue; + } + + @override + double? getDoubleOrNull(PropertyKey key, {double? defValue}) { + return _properties[key] ?? defValue; + } + + @override + int getInt(PropertyKey key, {required int defValue}) { + return _properties[key] ?? defValue; + } + + @override + int? getIntOrNull(PropertyKey key, {int? defValue}) { + return _properties[key] ?? defValue; + } + + @override + String getString(PropertyKey key, {required String defValue}) { + return _properties[key] ?? defValue; + } + + @override + String? getStringOrNull(PropertyKey key, {String? defValue}) { + return _properties[key] ?? defValue; + } + + @override + PropertyBundle loadValuesByTag(String tag) { + final result = tagBundles[tag]; + if (result != null) { + result.intersection(_properties); + return result; + } + return PropertyBundle(); + } + + @override + PropertyBundle loadValuesByUsage(int usage) { + PropertyBundle bundle = PropertyBundle(); + for (var entry in _properties.entries) { + if (entry.key.usage == usage) { + bundle.setString( + entry.key, entry.value is bool ? (entry.value ? "1" : "0") : entry.value.toString()); + } + } + return bundle; + } + + @override + void remove(PropertyKey key) { + _properties.remove(key); + } + + @override + void removeAllWithTag(String tag) { + final bundle = tagBundles.remove(tag); + if (bundle != null) { + _properties.removeWhere((key, value) => bundle.containsName(key)); + } + } + + @override + void setBool(PropertyKey name, bool value, {String? tag}) { + _properties[name] = value; + if (tag?.isNotEmpty == true) { + PropertyBundle? bundle = tagBundles[tag]; + if (bundle == null) { + bundle = PropertyBundle(); + tagBundles[tag!] = bundle; + } + bundle.setBool(name, value); + } + } + + @override + void setDouble(PropertyKey name, double value, {String? tag}) { + _properties[name] = value; + if (tag?.isNotEmpty == true) { + PropertyBundle? bundle = tagBundles[tag]; + if (bundle == null) { + bundle = PropertyBundle(); + tagBundles[tag!] = bundle; + } + bundle.setDouble(name, value); + } + } + + @override + void setInt(PropertyKey name, int value, {String? tag}) { + _properties[name] = value; + if (tag?.isNotEmpty == true) { + PropertyBundle? bundle = tagBundles[tag]; + if (bundle == null) { + bundle = PropertyBundle(); + tagBundles[tag!] = bundle; + } + bundle.setInt(name, value); + } + } + + @override + void setString(PropertyKey key, String value, {String? tag}) { + _properties[key] = value; + if (tag?.isNotEmpty == true) { + PropertyBundle? bundle = tagBundles[tag]; + if (bundle == null) { + bundle = PropertyBundle(); + tagBundles[tag!] = bundle; + } + bundle.setString(key, value); + } + } + + void setObject(PropertyKey key, T obj) { + _properties[key] = obj; + } + + T? getObject(PropertyKey key) { + return _properties[key]; + } +} + +extension RuntimePropertyExtension on RuntimeProperty { + set fakeRewardedAds(bool value) { + setBool(UtilsPropertyKeys.fakeRewardedAds, value); + } + + bool get fakeRewardedAds => getBool(UtilsPropertyKeys.fakeRewardedAds, defValue: false); + + set fakeInterstitialAds(bool value) { + setBool(UtilsPropertyKeys.fakeInterstitialAds, value); + } + + bool get fakeInterstitialAds => getBool(UtilsPropertyKeys.fakeInterstitialAds, defValue: false); +} diff --git a/guru_app/packages/guru_utils/lib/property/settings/settings_data.dart b/guru_app/packages/guru_utils/lib/property/settings/settings_data.dart new file mode 100644 index 0000000..147aca9 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/property/settings/settings_data.dart @@ -0,0 +1,514 @@ +import 'dart:convert'; + +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/property/app_property.dart'; +import 'package:guru_utils/property/property.dart'; + +/// Created by @Haoyi on 4/19/21 +/// + +class DeclareSettingDataException implements Exception { + final String msg; + + DeclareSettingDataException(this.msg); + + @override + String toString() { + return "[DeclareSettingDataException] $msg"; + } +} + +abstract class SettingData { + static final Map settings = {}; + + const SettingData(); + + Stream observe(); + + void init(PropertyBundle bundle); + + void set(T? data); + + T get({T? overrideDefaultValue}); + + SettingData toIntData(int Function(T) _convert, T Function(int) _revert) { + return ConvertibleSettingData(this, _convert, _revert); + } + + SettingData toBoolData(bool Function(T) _convert, T Function(bool) _revert) { + return ConvertibleSettingData(this, _convert, _revert); + } + + SettingData toStringData(String Function(T) _convert, T Function(String) _revert) { + return ConvertibleSettingData(this, _convert, _revert); + } + + SettingData toDoubleData(double Function(T) _convert, T Function(double) _revert) { + return ConvertibleSettingData(this, _convert, _revert); + } +} + +class ConstantSettingData extends SettingData { + final T value; + + const ConstantSettingData(this.value); + + @override + void init(PropertyBundle bundle) {} + + @override + Stream observe() => Stream.value(value); + + @override + void set(T? data) {} + + @override + T get({T? overrideDefaultValue}) { + return value; + } +} + +abstract class UniformSettingData extends SettingData { + final T defaultValue; + final PropertyKey name; + final BehaviorSubject subject; + + UniformSettingData(this.name, {required this.defaultValue}) + : subject = BehaviorSubject.seeded(defaultValue) { + SettingData.settings[name] = this; + } + + @override + Stream observe() => subject.stream; + + @override + void init(PropertyBundle bundle); + + @override + void set(T? data) { + subject.addEx(data ?? defaultValue); + } + + @override + T get({T? overrideDefaultValue}) { + return subject.value ?? overrideDefaultValue ?? defaultValue; + } +} + +class ConvertibleSettingData extends SettingData { + final SettingData _origin; + final Dst Function(Src) _convert; + final Src Function(Dst) _revert; + + ConvertibleSettingData(this._origin, this._convert, this._revert); + + @override + Stream observe() => _origin.observe().map((event) => _convert(event)); + + @override + void set(Dst? data) { + _origin.set(data == null ? null : _revert(data)); + } + + @override + Dst get({Dst? overrideDefaultValue}) { + return _convert(_origin.get( + overrideDefaultValue: overrideDefaultValue == null ? null : _revert(overrideDefaultValue))); + } + + @override + void init(PropertyBundle bundle) {} +} + +class SettingIntData extends UniformSettingData { + static SettingIntData _getInstance(PropertyKey key, {required int defaultValue}) { + final data = SettingData.settings[key]; + if (data != null) { + if (data is SettingIntData) { + return data; + } else { + throw DeclareSettingDataException( + "declare $key to SettingIntData error! already exists same SettingData(${data.runtimeType})!"); + } + } + return SettingIntData._(key, defaultValue: defaultValue); + } + + SettingIntData._(PropertyKey name, {required int defaultValue}) + : super(name, defaultValue: defaultValue); + + factory SettingIntData(PropertyKey name, {required int defaultValue}) => + _getInstance(name, defaultValue: defaultValue); + + @override + void set(int? data) { + super.set(data); + if (data == null) { + AppProperty.getInstance().remove(name); + } else { + AppProperty.getInstance().setInt(name, data); + } + } + + @override + void init(PropertyBundle bundle) { + super.set(bundle.getInt(name)); + } +} + +class SettingDoubleData extends UniformSettingData { + static SettingDoubleData _getInstance(PropertyKey key, {required double defaultValue}) { + final data = SettingData.settings[key]; + if (data != null) { + if (data is SettingDoubleData) { + return data; + } else { + throw DeclareSettingDataException( + "declare $key to SettingDoubleData error! already exists same SettingData(${data.runtimeType})!"); + } + } + return SettingDoubleData._(key, defaultValue: defaultValue); + } + + SettingDoubleData._(PropertyKey name, {required double defaultValue}) + : super(name, defaultValue: defaultValue); + + factory SettingDoubleData(PropertyKey name, {required double defaultValue}) => + _getInstance(name, defaultValue: defaultValue); + + @override + void init(PropertyBundle bundle) { + super.set(bundle.getDouble(name)); + } + + @override + void set(double? data) { + super.set(data); + if (data == null) { + AppProperty.getInstance().remove(name); + } else { + AppProperty.getInstance().setDouble(name, data); + } + } +} + +class SettingStringData extends UniformSettingData { + static SettingStringData _getInstance(PropertyKey key, {required String defaultValue}) { + final data = SettingData.settings[key]; + if (data != null) { + if (data is SettingStringData) { + return data; + } else { + throw DeclareSettingDataException( + "declare $key to SettingStringData error! already exists same SettingData(${data.runtimeType})!"); + } + } + return SettingStringData._(key, defaultValue: defaultValue); + } + + SettingStringData._(PropertyKey name, {required String defaultValue}) + : super(name, defaultValue: defaultValue); + + factory SettingStringData(PropertyKey name, {required String defaultValue}) => + _getInstance(name, defaultValue: defaultValue); + + @override + void init(PropertyBundle bundle) { + super.set(bundle.getString(name)); + } + + @override + void set(String? data) { + super.set(data); + if (data == null) { + AppProperty.getInstance().remove(name); + } else { + AppProperty.getInstance().setString(name, data); + } + } +} + +class SettingBoolData extends UniformSettingData { + static SettingBoolData _getInstance(PropertyKey key, {required bool defaultValue}) { + final data = SettingData.settings[key]; + if (data != null) { + if (data is SettingBoolData) { + return data; + } else { + throw DeclareSettingDataException( + "declare $key to SettingBoolData error! already exists same SettingData(${data.runtimeType})!"); + } + } + return SettingBoolData._(key, defaultValue: defaultValue); + } + + SettingBoolData._(PropertyKey name, {required bool defaultValue}) + : super(name, defaultValue: defaultValue); + + factory SettingBoolData(PropertyKey name, {required bool defaultValue}) => + _getInstance(name, defaultValue: defaultValue); + + @override + void init(PropertyBundle bundle) { + super.set(bundle.getBool(name)); + } + + @override + void set(bool? data) { + super.set(data); + if (data == null) { + AppProperty.getInstance().remove(name); + } else { + AppProperty.getInstance().setBool(name, data); + } + } +} + +class SettingJsonData extends UniformSettingData { + final T Function(Map) decoder; + + static SettingJsonData _getInstance(PropertyKey key, + {required T Function(Map) decoder, required T defaultValue}) { + final data = SettingData.settings[key]; + if (data != null) { + if (data is SettingJsonData) { + return data; + } else { + throw DeclareSettingDataException( + "declare $key to SettingJsonData error! already exists same SettingData(${data.runtimeType})!"); + } + } + return SettingJsonData._(key, decoder: decoder, defaultValue: defaultValue); + } + + factory SettingJsonData(PropertyKey name, + {required T Function(Map) decoder, required T defaultValue}) => + _getInstance(name, decoder: decoder, defaultValue: defaultValue); + + SettingJsonData._(PropertyKey name, {required this.decoder, required T defaultValue}) + : super(name, defaultValue: defaultValue); + + @override + void init(PropertyBundle bundle) { + final jsonStr = bundle.getString(name); + if (jsonStr != null) { + try { + final jsonMap = json.decode(jsonStr); + super.set(decoder.call(jsonMap)); + return; + } catch (error, stacktrace) { + Log.d("init settingJsonData error:$error, $stacktrace"); + super.set(defaultValue); + } + } + super.set(defaultValue); + } + + @override + void set(T? data) { + super.set(data); + if (data == null) { + AppProperty.getInstance().remove(name); + } else { + final jsonData = json.encode(data); + AppProperty.getInstance().setString(name, jsonData); + } + } +} + +class RuntimeSettingIntData extends UniformSettingData { + static RuntimeSettingIntData _getInstance(PropertyKey key, {required int defaultValue}) { + final data = SettingData.settings[key]; + if (data != null) { + if (data is RuntimeSettingIntData) { + return data; + } else { + throw DeclareSettingDataException( + "declare $key to SettingIntData error! already exists same SettingData(${data.runtimeType})!"); + } + } + return RuntimeSettingIntData._(key, defaultValue: defaultValue); + } + + RuntimeSettingIntData._(PropertyKey name, {required int defaultValue}) + : super(name, defaultValue: defaultValue); + + factory RuntimeSettingIntData(PropertyKey name, {required int defaultValue}) => + _getInstance(name, defaultValue: defaultValue); + + @override + void set(int? data) { + super.set(data); + if (data == null) { + RuntimeProperty.instance.remove(name); + } else { + RuntimeProperty.instance.setInt(name, data); + } + } + + @override + void init(PropertyBundle bundle) { + super.set(bundle.getInt(name)); + } +} + +class RuntimeSettingDoubleData extends UniformSettingData { + static RuntimeSettingDoubleData _getInstance(PropertyKey key, {required double defaultValue}) { + final data = SettingData.settings[key]; + if (data != null) { + if (data is RuntimeSettingDoubleData) { + return data; + } else { + throw DeclareSettingDataException( + "declare $key to RuntimeSettingDoubleData error! already exists same SettingData(${data.runtimeType})!"); + } + } + return RuntimeSettingDoubleData._(key, defaultValue: defaultValue); + } + + RuntimeSettingDoubleData._(PropertyKey name, {required double defaultValue}) + : super(name, defaultValue: defaultValue); + + factory RuntimeSettingDoubleData(PropertyKey name, {required double defaultValue}) => + _getInstance(name, defaultValue: defaultValue); + + @override + void init(PropertyBundle bundle) { + super.set(bundle.getDouble(name)); + } + + @override + void set(double? data) { + super.set(data); + if (data == null) { + RuntimeProperty.instance.remove(name); + } else { + RuntimeProperty.instance.setDouble(name, data); + } + } +} + +class RuntimeSettingStringData extends UniformSettingData { + static RuntimeSettingStringData _getInstance(PropertyKey key, {required String defaultValue}) { + final data = SettingData.settings[key]; + if (data != null) { + if (data is RuntimeSettingStringData) { + return data; + } else { + throw DeclareSettingDataException( + "declare $key to RuntimeSettingStringData error! already exists same SettingData(${data.runtimeType})!"); + } + } + return RuntimeSettingStringData._(key, defaultValue: defaultValue); + } + + RuntimeSettingStringData._(PropertyKey name, {required String defaultValue}) + : super(name, defaultValue: defaultValue); + + factory RuntimeSettingStringData(PropertyKey name, {required String defaultValue}) => + _getInstance(name, defaultValue: defaultValue); + + @override + void init(PropertyBundle bundle) { + super.set(bundle.getString(name)); + } + + @override + void set(String? data) { + super.set(data); + if (data == null) { + RuntimeProperty.instance.remove(name); + } else { + RuntimeProperty.instance.setString(name, data); + } + } +} + +class RuntimeSettingBoolData extends UniformSettingData { + static RuntimeSettingBoolData _getInstance(PropertyKey key, {required bool defaultValue}) { + final data = SettingData.settings[key]; + if (data != null) { + if (data is RuntimeSettingBoolData) { + return data; + } else { + throw DeclareSettingDataException( + "declare $key to RuntimeSettingBoolData error! already exists same SettingData(${data.runtimeType})!"); + } + } + return RuntimeSettingBoolData._(key, defaultValue: defaultValue); + } + + RuntimeSettingBoolData._(PropertyKey name, {required bool defaultValue}) + : super(name, defaultValue: defaultValue); + + factory RuntimeSettingBoolData(PropertyKey name, {required bool defaultValue}) => + _getInstance(name, defaultValue: defaultValue); + + @override + void init(PropertyBundle bundle) { + super.set(bundle.getBool(name)); + } + + @override + void set(bool? data) { + super.set(data); + if (data == null) { + RuntimeProperty.instance.remove(name); + } else { + RuntimeProperty.instance.setBool(name, data); + } + } +} + +class RuntimeSettingJsonData extends UniformSettingData { + final T Function(Map) decoder; + + static RuntimeSettingJsonData _getInstance(PropertyKey key, + {required T Function(Map) decoder, required T defaultValue}) { + final data = SettingData.settings[key]; + if (data != null) { + if (data is RuntimeSettingJsonData) { + return data; + } else { + throw DeclareSettingDataException( + "declare $key to SettingJsonData error! already exists same SettingData(${data.runtimeType})!"); + } + } + return RuntimeSettingJsonData._(key, decoder: decoder, defaultValue: defaultValue); + } + + factory RuntimeSettingJsonData(PropertyKey name, + {required T Function(Map) decoder, required T defaultValue}) => + _getInstance(name, decoder: decoder, defaultValue: defaultValue); + + RuntimeSettingJsonData._(PropertyKey name, {required this.decoder, required T defaultValue}) + : super(name, defaultValue: defaultValue); + + @override + void init(PropertyBundle bundle) { + final jsonStr = bundle.getString(name); + if (jsonStr != null) { + try { + final jsonMap = json.decode(jsonStr); + super.set(decoder.call(jsonMap)); + return; + } catch (error, stacktrace) { + Log.d("init settingJsonData error:$error, $stacktrace"); + super.set(defaultValue); + } + } + super.set(defaultValue); + } + + @override + void set(T? data) { + super.set(data); + if (data == null) { + RuntimeProperty.instance.remove(name); + } else { + final jsonData = json.encode(data); + RuntimeProperty.instance.setString(name, jsonData); + } + } +} diff --git a/guru_app/packages/guru_utils/lib/property/storage/db/property_database.dart b/guru_app/packages/guru_utils/lib/property/storage/db/property_database.dart new file mode 100644 index 0000000..def697c --- /dev/null +++ b/guru_app/packages/guru_utils/lib/property/storage/db/property_database.dart @@ -0,0 +1,242 @@ +import 'package:guru_utils/database/database.dart'; +import 'package:guru_utils/property/app_property.dart'; +import 'package:guru_utils/property/storage/property_storage.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:json_annotation/json_annotation.dart'; + +/// Created by Haoyi on 2022/8/24 + +part 'property_database.g.dart'; + +@JsonSerializable() +class PropertyEntity { + static const tbName = "properties"; + static const dbKey = "key"; + static const dbValue = "value"; + static const dbGroup = "gp"; + static const dbUsage = "usage"; + static const dbTag = "tag"; + static const dbUpdateAt = "upt"; + + @JsonKey(name: dbKey) + final String key; + + @JsonKey(name: dbValue, defaultValue: '') + final String value; + + @JsonKey(name: dbUsage, defaultValue: PropertyUsage.general) + final int usage; + + @JsonKey(name: dbGroup, defaultValue: PropertyKey.defaultGroup) + final String group; + + @JsonKey(name: dbTag, defaultValue: '') + final String tag; + + @JsonKey(name: dbUpdateAt, defaultValue: 0) + final int updateAt; + + PropertyEntity(this.key, this.value, this.usage, this.group, this.tag, this.updateAt); + + factory PropertyEntity.fromMap(Map json) => _$PropertyEntityFromJson(json); + + Map toMap() => _$PropertyEntityToJson(this); + + static Future createTable(Transaction delegate) async { + // v1 + const v1Fields = "$dbKey TEXT PRIMARY KEY," + "$dbUsage INTEGER DEFAULT ${PropertyUsage.general}," + "$dbValue TEXT DEFAULT ''," + "$dbGroup TEXT DEFAULT ${PropertyKey.defaultGroup}," + "$dbTag TEXT DEFAULT ''," + "$dbUpdateAt INTEGER DEFAULT 0"; + + const cmd = "CREATE TABLE $tbName (" + "$v1Fields" + ");"; + + Log.d("#### cmd: $cmd"); + + await delegate.execute(cmd); + await delegate.execute("CREATE INDEX property_idx ON $tbName ($dbKey);"); + await delegate.execute("CREATE INDEX property_group_idx ON $tbName ($dbGroup);"); + await delegate.execute("CREATE INDEX property_usage_idx ON $tbName ($dbUsage);"); + } +} + +mixin PropertyDatabase on PropertyStorage { + late AppDatabase appDb; + + void setDatabase(AppDatabase appDatabase) { + appDb = appDatabase; + } + + @override + Future setProperty(PropertyKey key, String value) async { + final propertyMap = { + PropertyEntity.dbKey: key.key, + PropertyEntity.dbValue: value, + PropertyEntity.dbGroup: key.group, + PropertyEntity.dbUsage: key.usage, + PropertyEntity.dbTag: key.tag, + PropertyEntity.dbUpdateAt: DateTimeUtils.currentTimeInMillis() + }; + + return (await appDb.getDb().insert(PropertyEntity.tbName, propertyMap, + conflictAlgorithm: ConflictAlgorithm.replace)) > + 0; + } + + Future _getProperty(DatabaseExecutor db, PropertyKey key) async { + final result = await db.rawQuery( + "SELECT * FROM ${PropertyEntity.tbName} WHERE ${PropertyEntity.dbKey} = '${key.key}'"); + if (result.isNotEmpty) { + final value = result[0][PropertyEntity.dbValue]; + if (value is String) { + return value; + } + } + return null; + } + + @override + Future getProperty(PropertyKey key) { + return _getProperty(appDb.getDb(), key); + } + + @override + Future getOrCreateProperty(PropertyKey key, {String? Function()? ifAbsent}) { + if (ifAbsent == null) { + return _getProperty(appDb.getDb(), key); + } else { + return appDb.runInTransaction((txn) async { + final result = await _getProperty(txn, key); + if (result != null) { + return result; + } + final value = ifAbsent(); + if (value != null) { + final now = DateTimeUtils.currentTimeInMillis(); + final propertyMap = { + PropertyEntity.dbKey: key.key, + PropertyEntity.dbValue: value, + PropertyEntity.dbGroup: key.group, + PropertyEntity.dbUsage: key.usage, + PropertyEntity.dbTag: key.tag, + PropertyEntity.dbUpdateAt: now + }; + await txn.insert(PropertyEntity.tbName, propertyMap, + conflictAlgorithm: ConflictAlgorithm.replace); + return value; + } + return null; + }); + } + } + + @override + Future> getProperties(List keys) { + return appDb.runInTransaction((txn) async { + final Map result = {}; + for (var key in keys) { + final property = await txn.rawQuery( + "SELECT * FROM ${PropertyEntity.tbName} WHERE ${PropertyEntity.dbKey} = '${key.key}'"); + if (property.isNotEmpty) { + final value = property[0][PropertyEntity.dbValue]; + if (value is String) { + result[key] = value; + } + } + } + return result; + }); + } + + @override + Future setProperties(Map kv) async { + final db = appDb.getDb(); + final batch = db.batch(); + final now = DateTimeUtils.currentTimeInMillis(); + for (var entry in kv.entries) { + final key = entry.key; + final propertyMap = { + PropertyEntity.dbKey: key.key, + PropertyEntity.dbValue: entry.value, + PropertyEntity.dbGroup: key.group, + PropertyEntity.dbUsage: key.usage, + PropertyEntity.dbTag: key.tag, + PropertyEntity.dbUpdateAt: now + }; + batch.insert(PropertyEntity.tbName, propertyMap, + conflictAlgorithm: ConflictAlgorithm.replace); + } + await batch.commit(); + return true; + } + + @override + Future> getPropertiesByTag(String tag) async { + final db = appDb.getDb(); + // Log.d("getPropertiesByTag:by $tag"); + final result = await db.query(PropertyEntity.tbName, where: "${PropertyEntity.dbTag} = '$tag'"); + if (result.isNotEmpty) { + Log.d("getPropertiesByTag:$result"); + return {for (var map in result) _toKey(map): map[PropertyEntity.dbValue] as String}; + } + return {}; + } + + @override + Future> getPropertiesByUsage(int usage) async { + final db = appDb.getDb(); + // Log.d("getPropertiesByTag:by $tag"); + final result = + await db.query(PropertyEntity.tbName, where: "${PropertyEntity.dbUsage} = $usage"); + if (result.isNotEmpty) { + Log.d("getPropertiesByUsage:$result"); + return {for (var map in result) _toKey(map): map[PropertyEntity.dbValue] as String}; + } + return {}; + } + + @override + Future removeProperty(PropertyKey key) async { + final db = appDb.getDb(); + return await db.delete(PropertyEntity.tbName, where: "${PropertyEntity.dbKey} = '${key.key}'") > 0; + } + + @override + Future> removeAllWithTag(String tag) async { + return appDb.runInTransaction((txn) async { + final result = + await txn.query(PropertyEntity.tbName, where: "${PropertyEntity.dbTag} = '$tag'"); + if (result.isNotEmpty) { + final keys = result.map((map) => _toKey(map)).toList(); + await txn.delete(PropertyEntity.tbName, where: "${PropertyEntity.dbTag} = '$tag'"); + return keys; + } + return []; + }); + } + + PropertyKey _toKey(Map data) { + final group = (data[PropertyEntity.dbGroup] ?? PropertyKey.defaultGroup) as String; + return PropertyKey( + name: (data[PropertyEntity.dbKey] as String).replaceFirst("$group@", ''), + usage: data[PropertyEntity.dbUsage] as int, + group: group, + tag: data[PropertyEntity.dbTag] as String); + } + + @override + Future> loadAllProperties() async { + final db = appDb.getDb(); + final result = await db.rawQuery("SELECT * FROM ${PropertyEntity.tbName}"); + if (result.isNotEmpty) { + return {for (var map in result) _toKey(map): map[PropertyEntity.dbValue] as String}; + } + return {}; + } +} diff --git a/guru_app/packages/guru_utils/lib/property/storage/db/property_database.g.dart b/guru_app/packages/guru_utils/lib/property/storage/db/property_database.g.dart new file mode 100644 index 0000000..52a891d --- /dev/null +++ b/guru_app/packages/guru_utils/lib/property/storage/db/property_database.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'property_database.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PropertyEntity _$PropertyEntityFromJson(Map json) => + PropertyEntity( + json['key'] as String, + json['value'] as String? ?? '', + json['usage'] as int? ?? 0, + json['gp'] as String? ?? 'guru', + json['tag'] as String? ?? '', + json['upt'] as int? ?? 0, + ); + +Map _$PropertyEntityToJson(PropertyEntity instance) => + { + 'key': instance.key, + 'value': instance.value, + 'usage': instance.usage, + 'gp': instance.group, + 'tag': instance.tag, + 'upt': instance.updateAt, + }; diff --git a/guru_app/packages/guru_utils/lib/property/storage/property_storage.dart b/guru_app/packages/guru_utils/lib/property/storage/property_storage.dart new file mode 100644 index 0000000..17f7f77 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/property/storage/property_storage.dart @@ -0,0 +1,26 @@ +import 'package:guru_utils/database/database.dart'; +import 'package:guru_utils/property/app_property.dart'; + +/// Created by Haoyi on 2022/8/24 + +mixin PropertyStorage { + Future setProperty(PropertyKey key, String value); + + Future getProperty(PropertyKey key); + + Future getOrCreateProperty(PropertyKey key, {String? Function()? ifAbsent}); + + Future> getProperties(List keys); + + Future setProperties(Map kv); + + Future> getPropertiesByTag(String tag); + + Future> getPropertiesByUsage(int usage); + + Future removeProperty(PropertyKey key); + + Future> removeAllWithTag(String tag); + + Future> loadAllProperties(); +} diff --git a/guru_app/packages/guru_utils/lib/quiver/async.dart b/guru_app/packages/guru_utils/lib/quiver/async.dart new file mode 100644 index 0000000..b2c18c4 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/async.dart @@ -0,0 +1,25 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +library quiver.async; + +export 'async/collect.dart'; +export 'async/concat.dart'; +export 'async/countdown_timer.dart'; +export 'async/enumerate.dart'; +export 'async/future_stream.dart'; +export 'async/metronome.dart'; +export 'async/stream_buffer.dart'; +export 'async/stream_router.dart'; +export 'async/string.dart'; diff --git a/guru_app/packages/guru_utils/lib/quiver/async/collect.dart b/guru_app/packages/guru_utils/lib/quiver/async/collect.dart new file mode 100644 index 0000000..784c3c2 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/async/collect.dart @@ -0,0 +1,32 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Returns a stream of completion events for the input [futures]. +/// +/// Successfully completed futures yield data events, while futures completed +/// with errors yield error events. +/// +/// The iterator obtained from [futures] is only advanced once the previous +/// future completes and yields an event. Thus, lazily creating the futures is +/// supported, for example: +/// +/// collect(files.map((file) => file.readAsString())); +/// +/// If you need to modify [futures], or a backing collection thereof, before +/// the returned stream is done, pass a copy instead to avoid a +/// [ConcurrentModificationError]: +/// +/// collect(files.toList().map((file) => file.readAsString())); +Stream collect(Iterable> futures) => + Stream.fromIterable(futures).asyncMap((f) => f); diff --git a/guru_app/packages/guru_utils/lib/quiver/async/concat.dart b/guru_app/packages/guru_utils/lib/quiver/async/concat.dart new file mode 100644 index 0000000..ee938a5 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/async/concat.dart @@ -0,0 +1,84 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; + +/// Returns the concatenation of the input streams. +/// +/// When the returned stream is listened to, the [streams] are iterated through +/// asynchronously, forwarding all events (both data and error) for the current +/// stream to the returned stream before advancing the iterator and listening +/// to the next stream. If advancing the iterator throws an error, the +/// returned stream ends immediately with that error. +/// +/// Pausing and resuming the returned stream's subscriptions will pause and +/// resume the subscription of the current stream being listened to. +/// +/// Note: Events from pre-existing broadcast streams which occur before the +/// stream is reached by the iteration will be dropped. +/// +/// Example: +/// +/// concat(files.map((file) => +/// file.openRead().transform(const LineSplitter()))) +Stream concat(Iterable> streams) => _ConcatStream(streams); + +class _ConcatStream extends Stream { + _ConcatStream(Iterable> streams) : _streams = streams; + + final Iterable> _streams; + + @override + StreamSubscription listen(void onData(T data)?, + {Function? onError, void onDone()?, bool? cancelOnError}) { + cancelOnError = true == cancelOnError; + StreamSubscription? currentSubscription; + StreamController? controller; + final iterator = _streams.iterator; + + void nextStream(StreamController controller) { + late final bool hasNext; + try { + hasNext = iterator.moveNext(); + } catch (e, s) { + controller.addError(e, s); + controller.close(); + return; + } + if (hasNext) { + currentSubscription = iterator.current.listen(controller.add, + onError: controller.addError, + onDone: () => nextStream(controller), + cancelOnError: cancelOnError); + } else { + controller.close(); + } + } + + controller = StreamController( + onPause: () { + currentSubscription?.pause(); + }, + onResume: () { + currentSubscription?.resume(); + }, + onCancel: () => currentSubscription?.cancel(), + ); + + nextStream(controller); + + return controller.stream.listen(onData, + onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } +} diff --git a/guru_app/packages/guru_utils/lib/quiver/async/countdown_timer.dart b/guru_app/packages/guru_utils/lib/quiver/async/countdown_timer.dart new file mode 100644 index 0000000..b7f52a9 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/async/countdown_timer.dart @@ -0,0 +1,73 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; + +/// A simple countdown timer that fires events in regular increments until a +/// duration has passed. +/// +/// CountdownTimer implements [Stream] and sends itself as the event. From the +/// timer you can get the [remaining] and [elapsed] time, or [cancel] the +/// timer. +class CountdownTimer extends Stream { + /// Creates a new [CountdownTimer] that fires events in increments of + /// [increment], until the [duration] has passed. + /// + /// [stopwatch] is for testing purposes. If you're using CountdownTimer and + /// need to control time in a test, pass a mock or a fake. See [FakeAsync] + /// and [FakeStopwatch]. + CountdownTimer(Duration duration, this.increment, {Stopwatch? stopwatch}) + : _duration = duration, + _stopwatch = stopwatch ?? Stopwatch(), + _controller = StreamController.broadcast(sync: true) { + _timer = Timer.periodic(increment, _tick); + _stopwatch.start(); + } + + static const _THRESHOLD_MS = 4; + + final Duration _duration; + final Stopwatch _stopwatch; + + /// The duration between timer events. + final Duration increment; + final StreamController _controller; + late final Timer _timer; + + @override + StreamSubscription listen(void onData(CountdownTimer event)?, + {Function? onError, void onDone()?, bool? cancelOnError}) => + _controller.stream.listen(onData, onError: onError, onDone: onDone); + + Duration get elapsed => _stopwatch.elapsed; + + Duration get remaining => _duration - _stopwatch.elapsed; + + bool get isRunning => _stopwatch.isRunning; + + void cancel() { + _stopwatch.stop(); + _timer.cancel(); + _controller.close(); + } + + void _tick(Timer timer) { + var t = remaining; + _controller.add(this); + // timers may have a 4ms resolution + if (t.inMilliseconds < _THRESHOLD_MS) { + cancel(); + } + } +} diff --git a/guru_app/packages/guru_utils/lib/quiver/async/enumerate.dart b/guru_app/packages/guru_utils/lib/quiver/async/enumerate.dart new file mode 100644 index 0000000..b5f207f --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/async/enumerate.dart @@ -0,0 +1,22 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:guru_utils/quiver/iterables.dart' show IndexedValue; + +/// Returns a [Stream] of [IndexedValue]s where the nth value holds the nth +/// element of [stream] and its index. +Stream> enumerate(Stream stream) { + var index = 0; + return stream.map((value) => IndexedValue(index++, value)); +} diff --git a/guru_app/packages/guru_utils/lib/quiver/async/future_stream.dart b/guru_app/packages/guru_utils/lib/quiver/async/future_stream.dart new file mode 100644 index 0000000..a51910c --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/async/future_stream.dart @@ -0,0 +1,82 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; + +/// A Stream that will emit the same values as the stream returned by [future] +/// once [future] completes. +/// +/// If [future] completes to an error, the return value will emit that error +/// and then close. +/// +/// If [broadcast] is true, this will be a broadcast stream. This assumes that +/// the stream returned by [future] will be a broadcast stream as well. +/// [broadcast] defaults to false. +/// +/// # Example +/// +/// This class is useful when you need to retrieve some object via a `Future`, +/// then return a `Stream` from that object: +/// +/// var futureOfStream = getResource().then((resource) => resource.stream); +/// return FutureStream(futureOfStream); +class FutureStream extends Stream { + FutureStream(Future> future, {bool broadcast = false}) { + _future = future.then(_identity, onError: (e, stackTrace) { + // Since [controller] is synchronous, it's likely that emitting an error + // will cause it to be cancelled before we call close. + _controller?.addError(e, stackTrace); + _controller?.close(); + _controller = null; + }); + + if (broadcast == true) { + _controller = StreamController.broadcast( + sync: true, onListen: _onListen, onCancel: _onCancel); + } else { + _controller = StreamController( + sync: true, onListen: _onListen, onCancel: _onCancel); + } + } + + static T _identity(T t) => t; + + late final Future> _future; + StreamController? _controller; + StreamSubscription? _subscription; + + void _onListen() { + _future.then((stream) { + if (_controller == null) return; + _subscription = stream.listen(_controller!.add, + onError: _controller!.addError, onDone: _controller!.close); + }); + } + + void _onCancel() { + _subscription?.cancel(); + _subscription = null; + _controller = null; + } + + @override + StreamSubscription listen(void onData(T event)?, + {Function? onError, void onDone()?, bool? cancelOnError}) { + return _controller!.stream.listen(onData, + onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } + + @override + bool get isBroadcast => _controller?.stream.isBroadcast ?? false; +} diff --git a/guru_app/packages/guru_utils/lib/quiver/async/metronome.dart b/guru_app/packages/guru_utils/lib/quiver/async/metronome.dart new file mode 100644 index 0000000..25065e2 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/async/metronome.dart @@ -0,0 +1,105 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; + +import 'package:guru_utils/quiver/time/clock.dart'; + +/// A stream of [DateTime] events at [interval]s centered on [anchor]. +/// +/// This stream accounts for drift but only guarantees that events are +/// delivered on or after the interval. If the system is busy for longer than +/// two [interval]s, only one will be delivered. +/// +/// [anchor] defaults to [clock.now], which means the stream represents a +/// self-correcting periodic timer. If anchor is the epoch, then the stream is +/// synchronized to wall-clock time. It can be anchored anywhere in time, but +/// this does not delay the first delivery. +/// +/// Examples: +/// +/// new Metronome.epoch(aMinute).listen((d) => print(d)); +/// +/// Could print the following stream of events, anchored by epoch, till the +/// stream is canceled: +/// +/// 2014-05-04 14:06:00.001 +/// 2014-05-04 14:07:00.000 +/// 2014-05-04 14:08:00.003 +/// ... +/// +/// Example anchored in the future (now = 2014-05-05 20:06:00.123) +/// +/// new IsochronousStream.periodic(aMillisecond * 100, +/// anchorMs: DateTime.parse("2014-05-05 21:07:00")) +/// .listen(print); +/// +/// 2014-05-04 20:06:00.223 +/// 2014-05-04 20:06:00.324 +/// 2014-05-04 20:06:00.423 +/// ... +class Metronome extends Stream { + Metronome.epoch(Duration interval, {Clock clock = const Clock()}) + : this._(interval, clock: clock, anchor: _epoch); + + Metronome.periodic(Duration interval, {Clock clock = const Clock(), DateTime? anchor}) + : this._(interval, clock: clock, anchor: anchor); + + Metronome._(this.interval, {this.clock = const Clock(), this.anchor}) + : _intervalMs = interval.inMilliseconds, + _anchorMs = (anchor ?? clock.now()).millisecondsSinceEpoch { + _controller = StreamController.broadcast( + sync: true, + onCancel: () { + _timer!.cancel(); + }, + onListen: () { + _startTimer(clock.now()); + }); + } + + static final DateTime _epoch = DateTime.fromMillisecondsSinceEpoch(0); + + final Clock clock; + final Duration interval; + final DateTime? anchor; + + Timer? _timer; + late final StreamController _controller; + final int _intervalMs; + final int _anchorMs; + + @override + bool get isBroadcast => true; + + @override + StreamSubscription listen(void onData(DateTime event)?, + {Function? onError, void onDone()?, bool? cancelOnError}) => + _controller.stream + .listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); + + void _startTimer(DateTime now) { + var delay = _intervalMs - ((now.millisecondsSinceEpoch - _anchorMs) % _intervalMs); + _timer = Timer(Duration(milliseconds: delay), _tickDate); + } + + void _tickDate() { + // Hey now, what's all this hinky clock.now() calls? Simple, if the workers + // on the receiving end of _controller.add() take a non-zero amount of time + // to do their thing (e.g. rendering a large scene with canvas), the next + // timer must be adjusted to account for the lapsed time. + _controller.add(clock.now()); + _startTimer(clock.now()); + } +} diff --git a/guru_app/packages/guru_utils/lib/quiver/async/stream_buffer.dart b/guru_app/packages/guru_utils/lib/quiver/async/stream_buffer.dart new file mode 100644 index 0000000..f81b184 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/async/stream_buffer.dart @@ -0,0 +1,184 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; + +/// Underflow errors happen when the socket feeding a buffer is finished while +/// there are still blocked readers. Each reader will complete with this error. +class UnderflowError extends Error { + /// The [message] describes the underflow. + UnderflowError([this.message]); + + final String? message; + + @override + String toString() { + if (message != null) { + return 'StreamBuffer Underflow: $message'; + } + return 'StreamBuffer Underflow'; + } +} + +/// Allow orderly reading of elements from a datastream, such as Socket, which +/// might not receive `List` bytes regular chunks. +/// +/// Example usage: +/// +/// StreamBuffer buffer = StreamBuffer(); +/// Socket.connect('127.0.0.1', 5555).then((sock) => sock.pipe(buffer)); +/// buffer.read(100).then((bytes) { +/// // do something with 100 bytes; +/// }); +/// +/// Throws [UnderflowError] if [throwOnError] is true. Useful for unexpected +/// [Socket] disconnects. +class StreamBuffer implements StreamConsumer> { + /// Create a stream buffer with optional, soft [limit] to the amount of data + /// the buffer will hold before pausing the underlying stream. A limit of 0 + /// means no buffer limits. + StreamBuffer({bool throwOnError = false, int limit = 0}) + : _throwOnError = throwOnError, + _limit = limit; + + int _offset = 0; + int _counter = 0; // sum(_chunks[*].length) - _offset + final List _chunks = []; + final List<_ReaderInWaiting>> _readers = []; + StreamSubscription>? _sub; + + final bool _throwOnError; + + Stream>? _currentStream; + + int _limit = 0; + + set limit(int limit) { + _limit = limit; + if (_sub != null) { + if (!limited || _counter < limit) { + _sub!.resume(); + } else { + _sub!.pause(); + } + } + } + + int get limit => _limit; + + bool get limited => _limit > 0; + + /// The amount of unread data buffered. + int get buffered => _counter; + + List _consume(int size) { + var follower = 0; + var ret = List.filled(size, null); + var leftToRead = size; + while (leftToRead > 0) { + var chunk = _chunks.first; + var listCap = (chunk is List) ? chunk.length - _offset : 1; + var subsize = leftToRead > listCap ? listCap : leftToRead; + if (chunk is List) { + ret.setRange(follower, follower + subsize, + chunk.getRange(_offset, _offset + subsize).cast()); + } else { + ret[follower] = chunk; + } + follower += subsize; + _offset += subsize; + _counter -= subsize; + leftToRead -= subsize; + if (!(chunk is List && _offset < chunk.length)) { + _offset = 0; + _chunks.removeAt(0); + } + } + if (limited && _sub!.isPaused && _counter < limit) { + _sub!.resume(); + } + return ret.cast(); + } + + /// Read fully [size] bytes from the stream and return in the future. + /// + /// Throws [ArgumentError] if size is larger than optional buffer [limit]. + Future> read(int size) { + if (limited && size > limit) { + throw ArgumentError('Cannot read $size with limit $limit'); + } + + // If we have enough data to consume and there are no other readers, then + // we can return immediately. + if (size <= buffered && _readers.isEmpty) { + return Future>.value(_consume(size)); + } + final completer = Completer>(); + _readers.add(_ReaderInWaiting>(size, completer)); + return completer.future; + } + + @override + Future addStream(Stream> stream) { + var lastStream = _currentStream ?? stream; + _sub?.cancel(); + _currentStream = stream; + + final streamDone = Completer(); + _sub = stream.listen((items) { + _chunks.addAll(items); + _counter += items is List ? items.length : 1; + if (limited && _counter >= limit) { + _sub!.pause(); + } + + while (_readers.isNotEmpty && _readers.first.size <= _counter) { + var waiting = _readers.removeAt(0); + waiting.completer.complete(_consume(waiting.size)); + } + }, onDone: () { + // User is piping in a new stream + if (stream == lastStream && _throwOnError) { + _closed(UnderflowError()); + } + streamDone.complete(); + }, onError: (e, stack) { + _closed(e, stack); + }); + return streamDone.future; + } + + void _closed(e, [StackTrace? stack]) { + for (final reader in _readers) { + if (!reader.completer.isCompleted) { + reader.completer.completeError(e, stack); + } + } + _readers.clear(); + } + + @override + Future close() { + final Future? ret = _sub?.cancel(); + _sub = null; + return ret ?? Future.value(null); + } +} + +class _ReaderInWaiting { + _ReaderInWaiting(this.size, this.completer); + + int size; + Completer completer; +} diff --git a/guru_app/packages/guru_utils/lib/quiver/async/stream_router.dart b/guru_app/packages/guru_utils/lib/quiver/async/stream_router.dart new file mode 100644 index 0000000..89aef0c --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/async/stream_router.dart @@ -0,0 +1,82 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; + +/// Splits a [Stream] of events into multiple Streams based on a set of +/// predicates. +/// +/// Using StreamRouter differs from [Stream.where] because events are only sent +/// to one Stream. If more than one predicate matches the event, the event is +/// sent to the stream created by the earlier call to [route]. Events not +/// matched by a call to [route] are sent to the [defaultStream]. +/// +/// Example: +/// +/// import 'dart:html'; +/// import 'package:quiver/async.dart'; +/// +/// var router = StreamRouter(window.onClick); +/// var onRightClick = router.route((e) => e.button == 2); +/// var onAltClick = router.route((e) => e.altKey); +/// var onOtherClick router.defaultStream; +class StreamRouter { + /// Create a new StreamRouter that listens to the [incoming] stream. + StreamRouter(Stream incoming) : _incoming = incoming { + _subscription = _incoming.listen(_handle, onDone: close); + } + + final Stream _incoming; + late final StreamSubscription _subscription; + + final List<_Route> _routes = <_Route>[]; + final StreamController _defaultController = + StreamController.broadcast(); + + /// Events that match [predicate] are sent to the stream created by this + /// method, and not sent to any other router streams. + Stream route(bool predicate(T event)) { + var controller = StreamController.broadcast(); + _routes.add(_Route(predicate, controller)); + return controller.stream; + } + + Stream get defaultStream => _defaultController.stream; + + Future close() { + return Future.wait(_routes.map((r) => r.controller.close())).then((_) { + _subscription.cancel(); + }); + } + + void _handle(T event) { + StreamController controller = _defaultController; + for (final _Route route in _routes) { + if (route.predicate(event)) { + controller = route.controller; + break; + } + } + controller.add(event); + } +} + +typedef _Predicate = bool Function(T event); + +class _Route { + _Route(this.predicate, this.controller); + + final _Predicate predicate; + final StreamController controller; +} diff --git a/guru_app/packages/guru_utils/lib/quiver/async/string.dart b/guru_app/packages/guru_utils/lib/quiver/async/string.dart new file mode 100644 index 0000000..bef771a --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/async/string.dart @@ -0,0 +1,21 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:convert' show Encoding, utf8; + +/// Converts a [Stream] of byte lists to a [String]. +Future byteStreamToString(Stream> stream, + {Encoding encoding = utf8}) { + return stream.transform(encoding.decoder).join(); +} diff --git a/guru_app/packages/guru_utils/lib/quiver/cache.dart b/guru_app/packages/guru_utils/lib/quiver/cache.dart new file mode 100644 index 0000000..7be4020 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/cache.dart @@ -0,0 +1,18 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +library quiver.cache; + +export 'cache/cache.dart'; +export 'cache/map_cache.dart'; diff --git a/guru_app/packages/guru_utils/lib/quiver/cache/cache.dart b/guru_app/packages/guru_utils/lib/quiver/cache/cache.dart new file mode 100644 index 0000000..b632bb1 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/cache/cache.dart @@ -0,0 +1,65 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; + +/// A function that produces a value for [key], for when a [Cache] needs to +/// populate an entry. +/// +/// The loader function should either return a value synchronously or a +/// [Future] which completes with the value asynchronously. +typedef Loader = FutureOr Function(K key); + +typedef MemoryLoader = V? Function(K key); + +typedef RemoveCallback = void Function(T); + +/// A semi-persistent mapping of keys to values. +/// +/// All access to a Cache is asynchronous because implementations may store +/// their entries in remote systems, isolates, or otherwise have to do async IO +/// to read and write. +abstract class Cache { + /// Returns the value associated with [key]. + /// + /// If [ifAbsent] is specified and no value is associated with the key, a + /// mapping is added and the value is returned. Otherwise, returns null. + Future get(K key, {Loader ifAbsent}); + + /// Sets the value associated with [key]. The Future completes when the + /// operation is complete. + Future set(K key, V value); + + /// Removes the value associated with [key]. The Future completes when the + /// operation is complete. + Future invalidate(K key); +} + +abstract class MemoryCache { + /// Returns the value associated with [key]. + /// + /// If [ifAbsent] is specified and no value is associated with the key, a + /// mapping is added and the value is returned. Otherwise, returns null. + V? get(K key, {MemoryLoader ifAbsent}); + + /// Sets the value associated with [key]. The Future completes when the + /// operation is complete. + void set(K key, V value); + + /// Removes the value associated with [key]. The Future completes when the + /// operation is complete. + void invalidate(K key); + + void clear(RemoveCallback onRemove); +} diff --git a/guru_app/packages/guru_utils/lib/quiver/cache/map_cache.dart b/guru_app/packages/guru_utils/lib/quiver/cache/map_cache.dart new file mode 100644 index 0000000..caa2556 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/cache/map_cache.dart @@ -0,0 +1,156 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:collection'; + +import 'package:guru_utils/quiver/collection/lru_map.dart' show LruMap; + +import 'cache.dart'; + +/// A [Cache] that's backed by a [Map]. +class MapCache implements Cache { + /// Creates a new [MapCache], optionally using [map] as the backing [Map]. + MapCache({Map? map}) : _map = map ?? HashMap(); + + /// Creates a new [MapCache], using [LruMap] as the backing [Map]. + /// + /// When [maximumSize] is specified, the cache is limited to the specified + /// number of pairs, otherwise it is limited to 100. + factory MapCache.lru({int? maximumSize, RemoveCallback? onRemoved}) { + // TODO(cbracken): inline the default value here for readability. + // https://github.com/google/quiver-dart/issues/653 + return MapCache(map: LruMap(maximumSize: maximumSize, onRemoved: onRemoved)); + } + + final Map _map; + + /// Map of outstanding ifAbsent calls used to prevent concurrent loads of the + /// same key. + final _outstanding = >{}; + + @override + Future get(K key, {Loader? ifAbsent}) async { + if (_map.containsKey(key)) { + return _map[key]; + } + // If this key is already loading then return the existing future. + if (_outstanding.containsKey(key)) { + return _outstanding[key]; + } + if (ifAbsent != null) { + var futureOr = ifAbsent(key); + _outstanding[key] = futureOr; + V? v; + try { + v = await futureOr; + } finally { + // Always remove key from [_outstanding] to prevent returning the + // failed result again. + _outstanding.remove(key); + } + if (v != null) { + _map[key] = v; + } + return v; + } + return null; + } + + @override + Future set(K key, V value) async { + _map[key] = value; + } + + Future addAll(Map map) async { + _map.addAll(map); + return; + } + + @override + Future invalidate(K key) async { + _map.remove(key); + } + + @override + void clear(RemoveCallback onRemove) { + for (var data in _map.values) { + onRemove.call(data); + } + _map.clear(); + } + + void forEach(void Function(K key, V value) action) { + _map.forEach(action); + } +} + +class MemoryMapCache implements MemoryCache { + /// Creates a new [MapCache], optionally using [map] as the backing [Map]. + MemoryMapCache({Map? map}) : _map = map ?? HashMap(); + + /// Creates a new [MapCache], using [LruMap] as the backing [Map]. + /// + /// When [maximumSize] is specified, the cache is limited to the specified + /// number of pairs, otherwise it is limited to 100. + factory MemoryMapCache.lru({int? maximumSize, RemoveCallback? onRemoved}) { + // TODO(cbracken): inline the default value here for readability. + // https://github.com/google/quiver-dart/issues/653 + return MemoryMapCache(map: LruMap(maximumSize: maximumSize, onRemoved: onRemoved)); + } + + final Map _map; + + @override + V? get(K key, {MemoryLoader? ifAbsent}) { + if (_map.containsKey(key)) { + return _map[key]; + } + if (ifAbsent != null) { + var v = ifAbsent(key); + if (v != null) { + _map[key] = v; + } + return v; + } + return null; + } + + @override + void set(K key, V value) { + _map[key] = value; + } + + void addAll(Map map) { + _map.addAll(map); + return; + } + + @override + void invalidate(K key) { + _map.remove(key); + } + + @override + void clear(RemoveCallback onRemove) { + for (var data in _map.values) { + onRemove.call(data); + } + _map.clear(); + } + + void forEach(void Function(K key, V value) action) { + _map.forEach(action); + } +} diff --git a/guru_app/packages/guru_utils/lib/quiver/check.dart b/guru_app/packages/guru_utils/lib/quiver/check.dart new file mode 100644 index 0000000..39cd92c --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/check.dart @@ -0,0 +1,82 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// A simple set of pre/post-condition checkers based on the +/// [Guava](https://code.google.com/p/guava-libraries/) Preconditions +/// class in Java. +/// +/// These checks are stronger than 'assert' statements, which can be +/// switched off, so they must only be used in situations where we actively +/// want the program to break when the check fails. +/// +/// ## Performance +/// Performance may be an issue with these checks if complex logic is computed +/// in order to make the method call. You should be careful with its use in +/// these cases - this library is aimed at improving maintainability and +/// readability rather than performance. They are also useful when the program +/// should fail early - for example, null-checking a parameter that might not +/// be used until the end of the method call. +/// +/// ## Error messages +/// The message parameter can be either a `() => Object` or any other `Object`. +/// The object will be converted to an error message by calling its +/// `toString()`. The `Function` should be preferred if the message is complex +/// to construct (i.e., it uses `String` interpolation), because it is only +/// called when the check fails. +/// +/// If the message parameter is `null` or returns `null`, a default error +/// message will be used. +library quiver.check; + +/// Throws an [ArgumentError] if the given [expression] is `false`. +void checkArgument(bool expression, {message}) { + if (!expression) { + throw ArgumentError(_resolveMessage(message, null)); + } +} + +/// Throws a [RangeError] if the given [index] is not a valid index for a list +/// with [size] elements. Otherwise, returns the [index] parameter. +int checkListIndex(int index, int size, {message}) { + if (index < 0 || index >= size) { + throw RangeError(_resolveMessage( + message, 'index $index not valid for list of size $size')); + } + return index; +} + +/// Throws an [ArgumentError] if the given [reference] is `null`. Otherwise, +/// returns the [reference] parameter. +/// +/// Users of Dart SDK 2.1 or later should prefer [ArgumentError.checkNotNull]. +@Deprecated('Use ArgumentError.checkNotNull. Will be removed in 4.0.0') +T checkNotNull(T reference, {message}) { + if (reference == null) { + throw ArgumentError(_resolveMessage(message, 'null pointer')); + } + return reference; +} + +/// Throws a [StateError] if the given [expression] is `false`. +void checkState(bool expression, {message}) { + if (!expression) { + throw StateError(_resolveMessage(message, 'failed precondition')!); + } +} + +String? _resolveMessage(message, String? defaultMessage) { + if (message is Function) message = message(); + if (message == null) return defaultMessage; + return message.toString(); +} diff --git a/guru_app/packages/guru_utils/lib/quiver/collection.dart b/guru_app/packages/guru_utils/lib/quiver/collection.dart new file mode 100644 index 0000000..17ad5d0 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/collection.dart @@ -0,0 +1,27 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Collection classes and related utilities. +library quiver.collection; + +export 'collection/bimap.dart'; +export 'collection/delegates/iterable.dart'; +export 'collection/delegates/list.dart'; +export 'collection/delegates/map.dart'; +export 'collection/delegates/queue.dart'; +export 'collection/delegates/set.dart'; +export 'collection/lru_map.dart'; +export 'collection/multimap.dart'; +export 'collection/treeset.dart' hide debugGetNode, AvlNode; +export 'collection/utils.dart'; diff --git a/guru_app/packages/guru_utils/lib/quiver/collection/bimap.dart b/guru_app/packages/guru_utils/lib/quiver/collection/bimap.dart new file mode 100644 index 0000000..47e06ce --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/collection/bimap.dart @@ -0,0 +1,175 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:collection'; + +/// A bi-directional map whose key-value pairs form a one-to-one +/// correspondence. BiMaps support an `inverse` property which gives access to +/// an inverted view of the map, such that there is a mapping (v, k) for each +/// pair (k, v) in the original map. Since a one-to-one key-value invariant +/// applies, it is an error to insert duplicate values into this map. +abstract class BiMap implements Map { + /// Creates a BiMap instance with the default implementation. + factory BiMap() => HashBiMap(); + + /// Adds an association between key and value. + /// + /// Throws [ArgumentError] if an association involving [value] exists in the + /// map; otherwise, the association is inserted, overwriting any existing + /// association for the key. + @override + void operator []=(K key, V value); + + /// Replaces any existing associations(s) involving key and value. + /// + /// If an association involving [key] or [value] exists in the map, it is + /// removed. + void replace(K key, V value); + + /// Returns the inverse of this map, with key-value pairs (v, k) for each pair + /// (k, v) in this map. + BiMap get inverse; +} + +/// A hash-table based implementation of BiMap. +class HashBiMap implements BiMap { + HashBiMap() : this._from(HashMap(), HashMap()); + HashBiMap._from(this._map, this._inverse); + + final Map _map; + final Map _inverse; + BiMap? _cached; + + @override + V? operator [](Object? key) => _map[key]; + + @override + void operator []=(K key, V value) { + _add(key, value, false); + } + + @override + void replace(K key, V value) { + _add(key, value, true); + } + + @override + void addAll(Map other) => other.forEach((k, v) => _add(k, v, false)); + + @override + bool containsKey(Object? key) => _map.containsKey(key); + + @override + bool containsValue(Object? value) => _inverse.containsKey(value); + + @override + void forEach(void f(K key, V value)) => _map.forEach(f); + + @override + bool get isEmpty => _map.isEmpty; + + @override + bool get isNotEmpty => _map.isNotEmpty; + + @override + Iterable get keys => _map.keys; + + @override + int get length => _map.length; + + @override + Iterable get values => _inverse.keys; + + @override + BiMap get inverse => _cached ??= HashBiMap._from(_inverse, _map); + + @override + void addEntries(Iterable> entries) { + for (final entry in entries) { + _add(entry.key, entry.value, false); + } + } + + @override + Map cast() { + // TODO(cbracken): Dart 2.0 requires this method to be implemented. + throw UnimplementedError('cast'); + } + + @override + Iterable> get entries => _map.entries; + + @override + Map map(MapEntry transform(K key, V value)) => + _map.map(transform); + + @override + V putIfAbsent(K key, V ifAbsent()) { + if (containsKey(key)) { + return _map[key]!; + } + return _add(key, ifAbsent(), false); + } + + @override + V? remove(Object? key) { + _inverse.remove(_map[key]); + return _map.remove(key); + } + + @override + void removeWhere(bool test(K key, V value)) { + _inverse.removeWhere((v, k) => test(k, v)); + _map.removeWhere(test); + } + + @override + V update(K key, V update(V value), {V ifAbsent()?}) { + var value = _map[key]; + if (value != null) { + return _add(key, update(value), true); + } else { + if (ifAbsent == null) { + throw ArgumentError.value(key, 'key', 'Key not in map'); + } + return _add(key, ifAbsent(), false); + } + } + + @override + void updateAll(V update(K key, V value)) { + for (final key in keys) { + _add(key, update(key, _map[key]!), true); + } + } + + @override + void clear() { + _map.clear(); + _inverse.clear(); + } + + V _add(K key, V value, bool replace) { + var oldValue = _map[key]; + if (containsKey(key) && oldValue == value) return value; + if (_inverse.containsKey(value)) { + if (!replace) throw ArgumentError('Mapping for $value exists'); + _map.remove(_inverse[value]); + } + _inverse.remove(oldValue); + _map[key] = value; + _inverse[value] = key; + return value; + } +} diff --git a/guru_app/packages/guru_utils/lib/quiver/collection/delegates/iterable.dart b/guru_app/packages/guru_utils/lib/quiver/collection/delegates/iterable.dart new file mode 100644 index 0000000..33fe292 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/collection/delegates/iterable.dart @@ -0,0 +1,123 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// An implementation of [Iterable] that delegates all methods to another +/// [Iterable]. For instance you can create a FruitIterable like this : +/// +/// class FruitIterable extends DelegatingIterable { +/// final Iterable _fruits = []; +/// +/// Iterable get delegate => _fruits; +/// +/// // custom methods +/// } +abstract class DelegatingIterable implements Iterable { + Iterable get delegate; + + @override + bool any(bool test(E element)) => delegate.any(test); + + @override + Iterable cast() { + // TODO(cbracken): Dart 2.0 requires this method to be implemented. + throw UnimplementedError('cast'); + } + + @override + bool contains(Object? element) => delegate.contains(element); + + @override + E elementAt(int index) => delegate.elementAt(index); + + @override + bool every(bool test(E element)) => delegate.every(test); + + @override + Iterable expand(Iterable f(E element)) => delegate.expand(f); + + @override + E get first => delegate.first; + + @override + E firstWhere(bool test(E element), {E orElse()?}) => delegate.firstWhere(test, orElse: orElse); + + @override + T fold(T initialValue, T combine(T previousValue, E element)) => delegate.fold(initialValue, combine); + + @override + Iterable followedBy(Iterable other) => delegate.followedBy(other); + + @override + void forEach(void f(E element)) => delegate.forEach(f); + + @override + bool get isEmpty => delegate.isEmpty; + + @override + bool get isNotEmpty => delegate.isNotEmpty; + + @override + Iterator get iterator => delegate.iterator; + + @override + String join([String separator = '']) => delegate.join(separator); + + @override + E get last => delegate.last; + + @override + E lastWhere(bool test(E element), {E orElse()?}) => delegate.lastWhere(test, orElse: orElse); + + @override + int get length => delegate.length; + + @override + Iterable map(T f(E e)) => delegate.map(f); + + @override + E reduce(E combine(E value, E element)) => delegate.reduce(combine); + + @override + E get single => delegate.single; + + @override + E singleWhere(bool test(E element), {E orElse()?}) => delegate.singleWhere(test, orElse: orElse); + + @override + Iterable skip(int n) => delegate.skip(n); + + @override + Iterable skipWhile(bool test(E value)) => delegate.skipWhile(test); + + @override + Iterable take(int n) => delegate.take(n); + + @override + Iterable takeWhile(bool test(E value)) => delegate.takeWhile(test); + + @override + List toList({bool growable = true}) => delegate.toList(growable: growable); + + @override + Set toSet() => delegate.toSet(); + + @override + Iterable where(bool test(E element)) => delegate.where(test); + + @override + Iterable whereType() { + // TODO(cbracken): Dart 2.0 requires this method to be implemented. + throw UnimplementedError('whereType'); + } +} diff --git a/guru_app/packages/guru_utils/lib/quiver/collection/delegates/list.dart b/guru_app/packages/guru_utils/lib/quiver/collection/delegates/list.dart new file mode 100644 index 0000000..2c19a4c --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/collection/delegates/list.dart @@ -0,0 +1,151 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show Random; + +import 'iterable.dart'; + +/// An implementation of [List] that delegates all methods to another [List]. +/// For instance you can create a FruitList like this : +/// +/// class FruitList extends DelegatingList { +/// final List _fruits = []; +/// +/// List get delegate => _fruits; +/// +/// // custom methods +/// } +abstract class DelegatingList extends DelegatingIterable + implements List { + @override + List get delegate; + + @override + E operator [](int index) => delegate[index]; + + @override + void operator []=(int index, E value) { + delegate[index] = value; + } + + @override + List operator +(List other) => delegate + other; + + @override + void add(E value) => delegate.add(value); + + @override + void addAll(Iterable iterable) => delegate.addAll(iterable); + + @override + Map asMap() => delegate.asMap(); + + @override + DelegatingList cast() { + // TODO(cbracken): Dart 2.0 requires this method to be implemented. + throw UnimplementedError('cast'); + } + + @override + void clear() => delegate.clear(); + + @override + void fillRange(int start, int end, [E? fillValue]) => + delegate.fillRange(start, end, fillValue); + + @override + set first(E element) { + if (isEmpty) throw RangeError.index(0, this); + this[0] = element; + } + + @override + Iterable getRange(int start, int end) => delegate.getRange(start, end); + + @override + int indexOf(E element, [int start = 0]) => delegate.indexOf(element, start); + + @override + int indexWhere(bool test(E element), [int start = 0]) => + delegate.indexWhere(test, start); + + @override + void insert(int index, E element) => delegate.insert(index, element); + + @override + void insertAll(int index, Iterable iterable) => + delegate.insertAll(index, iterable); + + @override + set last(E element) { + if (isEmpty) throw RangeError.index(0, this); + this[length - 1] = element; + } + + @override + int lastIndexOf(E element, [int? start]) => + delegate.lastIndexOf(element, start); + + @override + int lastIndexWhere(bool test(E element), [int? start]) => + delegate.lastIndexWhere(test, start); + + @override + set length(int newLength) { + delegate.length = newLength; + } + + @override + bool remove(Object? value) => delegate.remove(value); + + @override + E removeAt(int index) => delegate.removeAt(index); + + @override + E removeLast() => delegate.removeLast(); + + @override + void removeRange(int start, int end) => delegate.removeRange(start, end); + + @override + void removeWhere(bool test(E element)) => delegate.removeWhere(test); + + @override + void replaceRange(int start, int end, Iterable iterable) => + delegate.replaceRange(start, end, iterable); + + @override + void retainWhere(bool test(E element)) => delegate.retainWhere(test); + + @override + Iterable get reversed => delegate.reversed; + + @override + void setAll(int index, Iterable iterable) => + delegate.setAll(index, iterable); + + @override + void setRange(int start, int end, Iterable iterable, + [int skipCount = 0]) => + delegate.setRange(start, end, iterable, skipCount); + + @override + void shuffle([Random? random]) => delegate.shuffle(random); + + @override + void sort([int compare(E a, E b)?]) => delegate.sort(compare); + + @override + List sublist(int start, [int? end]) => delegate.sublist(start, end); +} diff --git a/guru_app/packages/guru_utils/lib/quiver/collection/delegates/map.dart b/guru_app/packages/guru_utils/lib/quiver/collection/delegates/map.dart new file mode 100644 index 0000000..8ea9870 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/collection/delegates/map.dart @@ -0,0 +1,119 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// An implementation of [Map] that delegates all methods to another [Map]. +/// For instance you can create a FruitMap like this : +/// +/// class FruitMap extends DelegatingMap { +/// final Map _fruits = {}; +/// +/// Map get delegate => _fruits; +/// +/// // custom methods +/// } +abstract class DelegatingMap implements Map { + Map get delegate; + + @override + V? operator [](Object? key) => delegate[key]; + + @override + void operator []=(K key, V value) { + delegate[key] = value; + } + + @override + void addAll(Map other) => delegate.addAll(other); + + @override + void addEntries(Iterable entries) { + // TODO(cbracken): Dart 2.0 requires this method to be implemented. + // Change Iterable to Iterable> when + // the MapEntry class has been added. + throw UnimplementedError('addEntries'); + } + + @override + Map cast() { + // TODO(cbracken): Dart 2.0 requires this method to be implemented. + throw UnimplementedError('cast'); + } + + @override + void clear() => delegate.clear(); + + @override + bool containsKey(Object? key) => delegate.containsKey(key); + + @override + bool containsValue(Object? value) => delegate.containsValue(value); + + @override + Iterable> get entries { + // TODO(cbracken): Dart 2.0 requires this method to be implemented. + // Change Iterable to Iterable> when + // the MapEntry class has been added. + throw UnimplementedError('entries'); + } + + @override + void forEach(void f(K key, V value)) => delegate.forEach(f); + + @override + bool get isEmpty => delegate.isEmpty; + + @override + bool get isNotEmpty => delegate.isNotEmpty; + + @override + Iterable get keys => delegate.keys; + + @override + int get length => delegate.length; + + @override + Map map(Object transform(K key, V value)) { + // TODO(cbracken): Dart 2.0 requires this method to be implemented. + // Change Object to MapEntry when + // the MapEntry class has been added. + throw UnimplementedError('map'); + } + + @override + V putIfAbsent(K key, V ifAbsent()) => delegate.putIfAbsent(key, ifAbsent); + + @override + V? remove(Object? key) => delegate.remove(key); + + @override + void removeWhere(bool test(K key, V value)) { + // TODO(cbracken): Dart 2.0 requires this method to be implemented. + throw UnimplementedError('removeWhere'); + } + + @override + V update(K key, V update(V value), {V ifAbsent()?}) { + // TODO(cbracken): Dart 2.0 requires this method to be implemented. + throw UnimplementedError('update'); + } + + @override + void updateAll(V update(K key, V value)) { + // TODO(cbracken): Dart 2.0 requires this method to be implemented. + throw UnimplementedError('updateAll'); + } + + @override + Iterable get values => delegate.values; +} diff --git a/guru_app/packages/guru_utils/lib/quiver/collection/delegates/queue.dart b/guru_app/packages/guru_utils/lib/quiver/collection/delegates/queue.dart new file mode 100644 index 0000000..7c72b67 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/collection/delegates/queue.dart @@ -0,0 +1,69 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:collection' show Queue; + +import 'iterable.dart'; + +/// An implementation of [Queue] that delegates all methods to another [Queue]. +/// For instance you can create a FruitQueue like this : +/// +/// class FruitQueue extends DelegatingQueue { +/// final Queue _fruits = Queue(); +/// +/// Queue get delegate => _fruits; +/// +/// // custom methods +/// } +abstract class DelegatingQueue extends DelegatingIterable + implements Queue { + @override + Queue get delegate; + + @override + void add(E value) => delegate.add(value); + + @override + void addAll(Iterable iterable) => delegate.addAll(iterable); + + @override + void addFirst(E value) => delegate.addFirst(value); + + @override + void addLast(E value) => delegate.addLast(value); + + @override + DelegatingQueue cast() { + // TODO(cbracken): Dart 2.0 requires this method to be implemented. + throw UnimplementedError('cast'); + } + + @override + void clear() => delegate.clear(); + + @override + bool remove(Object? object) => delegate.remove(object); + + @override + E removeFirst() => delegate.removeFirst(); + + @override + E removeLast() => delegate.removeLast(); + + @override + void removeWhere(bool test(E element)) => delegate.removeWhere(test); + + @override + void retainWhere(bool test(E element)) => delegate.retainWhere(test); +} diff --git a/guru_app/packages/guru_utils/lib/quiver/collection/delegates/set.dart b/guru_app/packages/guru_utils/lib/quiver/collection/delegates/set.dart new file mode 100644 index 0000000..a8a2cf3 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/collection/delegates/set.dart @@ -0,0 +1,76 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'iterable.dart'; + +/// An implementation of [Set] that delegates all methods to another [Set]. +/// For instance you can create a FruitSet like this : +/// +/// class FruitSet extends DelegatingSet { +/// final Set _fruits = Set(); +/// +/// Set get delegate => _fruits; +/// +/// // custom methods +/// } +abstract class DelegatingSet extends DelegatingIterable + implements Set { + @override + Set get delegate; + + @override + bool add(E value) => delegate.add(value); + + @override + void addAll(Iterable elements) => delegate.addAll(elements); + + @override + DelegatingSet cast() { + // TODO(cbracken): Dart 2.0 requires this method to be implemented. + throw UnimplementedError('cast'); + } + + @override + void clear() => delegate.clear(); + + @override + bool containsAll(Iterable other) => delegate.containsAll(other); + + @override + Set difference(Set other) => delegate.difference(other); + + @override + Set intersection(Set other) => delegate.intersection(other); + + @override + E? lookup(Object? object) => delegate.lookup(object); + + @override + bool remove(Object? value) => delegate.remove(value); + + @override + void removeAll(Iterable elements) => delegate.removeAll(elements); + + @override + void removeWhere(bool test(E element)) => delegate.removeWhere(test); + + @override + void retainAll(Iterable elements) => delegate.retainAll(elements); + + @override + void retainWhere(bool test(E element)) => delegate.retainWhere(test); + + @override + Set union(Set other) => delegate.union(other); +} diff --git a/guru_app/packages/guru_utils/lib/quiver/collection/lru_map.dart b/guru_app/packages/guru_utils/lib/quiver/collection/lru_map.dart new file mode 100644 index 0000000..7f8ba51 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/collection/lru_map.dart @@ -0,0 +1,400 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:collection'; + +import 'package:guru_utils/quiver/iterables.dart' show GeneratingIterable; +import 'package:guru_utils/quiver/cache/cache.dart'; + +/// An implementation of a [Map] which has a maximum size and uses a [Least +/// Recently Used](http://en.wikipedia.org/wiki/Cache_algorithms#LRU) algorithm +/// to remove items from the [Map] when the [maximumSize] is reached and new +/// items are added. +/// +/// It is safe to access the [keys] and [values] collections without affecting +/// the "used" ordering - as well as using [forEach]. Other types of access, +/// including bracket, and [putIfAbsent], promotes the key-value pair to the +/// MRU position. +abstract class LruMap implements Map { + /// Creates a [LruMap] instance with the default implementation. + factory LruMap({int? maximumSize, RemoveCallback? onRemoved}) = + LinkedLruHashMap; + + /// Maximum size of the [Map]. If [length] exceeds this value at any time, n + /// entries accessed the earliest are removed, where n is [length] - + /// [maximumSize]. + int get maximumSize; + + set maximumSize(int size); +} + +/// Simple implementation of a linked-list entry that contains a [key] and +/// [value]. +class _LinkedEntry { + _LinkedEntry(this.key, this.value); + + K key; + V value; + + _LinkedEntry? next; + _LinkedEntry? previous; +} + +/// A linked hash-table based implementation of [LruMap]. +class LinkedLruHashMap implements LruMap { + RemoveCallback? onRemoved; + + /// Create a new LinkedLruHashMap with a [maximumSize]. + factory LinkedLruHashMap({int? maximumSize, RemoveCallback? onRemoved}) => + LinkedLruHashMap._fromMap(HashMap>(), maximumSize: maximumSize); + + LinkedLruHashMap._fromMap(this._entries, {int? maximumSize, this.onRemoved}) + // This pattern is used instead of a default value because we want to + // be able to respect null values coming in from MapCache.lru. + : _maximumSize = maximumSize ?? _DEFAULT_MAXIMUM_SIZE; + + static const _DEFAULT_MAXIMUM_SIZE = 100; + + final Map> _entries; + + int _maximumSize; + + _LinkedEntry? _head; + _LinkedEntry? _tail; + + /// Adds all key-value pairs of [other] to this map. + /// + /// The operation is equivalent to doing `this[key] = value` for each key and + /// associated value in [other]. It iterates over [other], which must + /// therefore not change during the iteration. + /// + /// If a key of [other] is already in this map, its value is overwritten. If + /// the number of unique keys is greater than [maximumSize] then the least + /// recently use keys are evicted. For keys written to by [other], the least + /// recently user order is determined by [other]'s iteration order. + @override + void addAll(Map other) => other.forEach((k, v) => this[k] = v); + + @override + void addEntries(Iterable> entries) { + for (final entry in entries) { + this[entry.key] = entry.value; + } + } + + @override + LinkedLruHashMap cast() { + // TODO(cbracken): Dart 2.0 requires this method to be implemented. + throw UnimplementedError('cast'); + } + + @override + void clear() { + _entries.clear(); + _head = _tail = null; + } + + @override + bool containsKey(Object? key) => _entries.containsKey(key); + + @override + bool containsValue(Object? value) => values.contains(value); + + @override + Iterable> get entries => + _entries.values.map((entry) => MapEntry(entry.key, entry.value)); + + /// Applies [action] to each key-value pair of the map in order of MRU to + /// LRU. + /// + /// Calling `action` must not add or remove keys from the map. + @override + void forEach(void action(K key, V value)) { + var head = _head; + while (head != null) { + action(head.key, head.value); + head = head.next; + } + } + + @override + int get length => _entries.length; + + @override + bool get isEmpty => _entries.isEmpty; + + @override + bool get isNotEmpty => _entries.isNotEmpty; + + /// Creates an [Iterable] around the entries of the map. + Iterable<_LinkedEntry> _iterable() { + if (_head == null) { + return const Iterable.empty(); + } + return GeneratingIterable<_LinkedEntry>(() => _head!, (n) => n.next); + } + + /// The keys of [this] - in order of MRU to LRU. + /// + /// The returned iterable does *not* have efficient `length` or `contains`. + @override + Iterable get keys => _iterable().map((e) => e.key); + + /// The values of [this] - in order of MRU to LRU. + /// + /// The returned iterable does *not* have efficient `length` or `contains`. + @override + Iterable get values => _iterable().map((e) => e.value); + + @override + Map map(Object transform(K key, V value)) { + // TODO(cbracken): Dart 2.0 requires this method to be implemented. + // Change Object to MapEntry when + // the MapEntry class has been added. + throw UnimplementedError('map'); + } + + @override + int get maximumSize => _maximumSize; + + @override + set maximumSize(int maximumSize) { + // TODO(cbracken): Remove when mixed-mode execution is unsupported. + ArgumentError.checkNotNull(maximumSize, 'maximumSize'); + while (length > maximumSize) { + _removeLru(); + } + _maximumSize = maximumSize; + } + + /// Look up the value associated with [key], or add a new value if it isn't + /// there. The pair is promoted to the MRU position. + /// + /// Otherwise calls [ifAbsent] to get a new value, associates [key] to that + /// [value], and then returns the new [value], with the key-value pair in the + /// MRU position. If this causes [length] to exceed [maximumSize], then the + /// LRU position is removed. + @override + V putIfAbsent(K key, V ifAbsent()) { + final entry = _entries.putIfAbsent(key, () => _createEntry(key, ifAbsent())); + if (length > maximumSize) { + _removeLru(); + } + _promoteEntry(entry); + return entry.value; + } + + /// Get the value for a [key] in the [Map]. + /// The [key] will be promoted to the 'Most Recently Used' position. + /// + /// *NOTE*: Calling `[]` inside an iteration over keys/values is currently + /// unsupported; use [keys] or [values] if you need information about entries + /// without modifying their position. + @override + V? operator [](Object? key) { + final entry = _entries[key]; + if (entry != null) { + _promoteEntry(entry); + return entry.value; + } else { + return null; + } + } + + /// If [key] already exists, promotes it to the MRU position & assigns + /// [value]. + /// + /// Otherwise, adds [key] and [value] to the MRU position. If [length] + /// exceeds [maximumSize] while adding, removes the LRU position. + @override + void operator []=(K key, V value) { + // Add this item to the MRU position. + _insertMru(_createEntry(key, value)); + + // Remove the LRU item if the size would be exceeded by adding this item. + if (length > maximumSize) { + assert(length == maximumSize + 1); + _removeLru(); + } + } + + @override + V? remove(Object? key) { + final entry = _entries.remove(key); + if (entry == null) { + return null; + } else { + onRemoved?.call(entry.value); + } + if (entry == _head && entry == _tail) { + _head = _tail = null; + } else if (entry == _head) { + _head = _head!.next; + _head?.previous = null; + } else if (entry == _tail) { + _tail = _tail!.previous; + _tail?.next = null; + } else { + entry.previous!.next = entry.next; + entry.next!.previous = entry.previous; + } + return entry.value; + } + + @override + void removeWhere(bool test(K key, V value)) { + var keysToRemove = []; + _entries.forEach((key, entry) { + if (test(key, entry.value)) keysToRemove.add(key); + }); + keysToRemove.forEach(remove); + } + + @override + // TODO(cbracken): Use the `MapBase.mapToString()` static method when the + // minimum SDK version of this package has been bumped to 2.0.0 or greater. + String toString() { + // Detect toString() cycles. + if (_isToStringVisiting(this)) { + return '{...}'; + } + + var result = StringBuffer(); + try { + _toStringVisiting.add(this); + result.write('{'); + bool first = true; + forEach((k, v) { + if (!first) { + result.write(', '); + } + first = false; + result.write('$k: $v'); + }); + result.write('}'); + } finally { + assert(identical(_toStringVisiting.last, this)); + _toStringVisiting.removeLast(); + } + + return result.toString(); + } + + @override + V update(K key, V update(V value), {V ifAbsent()?}) { + V newValue; + if (containsKey(key)) { + newValue = update(this[key]!); + } else { + if (ifAbsent == null) { + throw ArgumentError.value(key, 'key', 'Key not in map'); + } + newValue = ifAbsent(); + } + + // Add this item to the MRU position. + _insertMru(_createEntry(key, newValue)); + + // Remove the LRU item if the size would be exceeded by adding this item. + if (length > maximumSize) { + assert(length == maximumSize + 1); + _removeLru(); + } + return newValue; + } + + @override + void updateAll(V update(K key, V value)) { + _entries.forEach((key, entry) { + var newValue = _createEntry(key, update(key, entry.value)); + _entries[key] = newValue; + }); + } + + /// Moves [entry] to the MRU position, shifting the linked list if necessary. + void _promoteEntry(_LinkedEntry entry) { + // If this entry is already in the MRU position we are done. + if (entry == _head) { + return; + } + + if (entry.previous != null) { + // If already existed in the map, link previous to next. + entry.previous!.next = entry.next; + + // If this was the tail element, assign a new tail. + if (_tail == entry) { + _tail = entry.previous; + } + } + // If this entry is not the end of the list then link the next entry to the previous entry. + if (entry.next != null) { + entry.next!.previous = entry.previous; + } + + // Replace head with this element. + if (_head != null) { + _head!.previous = entry; + } + entry.previous = null; + entry.next = _head; + _head = entry; + + // Add a tail if this is the first element. + if (_tail == null) { + assert(length == 1); + _tail = _head; + } + } + + /// Creates and returns an entry from [key] and [value]. + _LinkedEntry _createEntry(K key, V value) { + return _LinkedEntry(key, value); + } + + /// If [entry] does not exist, inserts it into the backing map. If it does, + /// replaces the existing [_LinkedEntry.value] with [entry.value]. Then, in + /// either case, promotes [entry] to the MRU position. + void _insertMru(_LinkedEntry entry) { + // Insert a new entry if necessary (only 1 hash lookup in entire function). + // Otherwise, just updates the existing value. + final value = entry.value; + _promoteEntry(_entries.putIfAbsent(entry.key, () => entry)..value = value); + } + + /// Removes the LRU position, shifting the linked list if necessary. + void _removeLru() { + // Remove the tail from the internal map. + final entry = _entries.remove(_tail!.key); + + if (entry != null) { + onRemoved?.call(entry.value); + } + + // Remove the tail element itself. + _tail = _tail!.previous; + _tail?.next = null; + + // If we removed the last element, clear the head too. + if (_tail == null) { + _head = null; + } + } +} + +/// A collection used to identify cyclic maps during toString() calls. +final List _toStringVisiting = []; + +/// Check if we are currently visiting `o` in a toString() call. +bool _isToStringVisiting(o) => _toStringVisiting.any((e) => identical(o, e)); diff --git a/guru_app/packages/guru_utils/lib/quiver/collection/multimap.dart b/guru_app/packages/guru_utils/lib/quiver/collection/multimap.dart new file mode 100644 index 0000000..dc496e2 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/collection/multimap.dart @@ -0,0 +1,920 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show Random; + +/// An associative container that maps a key to multiple values. +/// +/// Key lookups return mutable collections that are views of the multimap. +/// Updates to the multimap are reflected in these collections and similarly, +/// modifications to the returned collections are reflected in the multimap. +abstract class Multimap { + /// Constructs a new list-backed multimap. + factory Multimap() => ListMultimap(); + + /// Constructs a new list-backed multimap. For each element e of [iterable], + /// adds an association from [key](e) to [value](e). [key] and [value] each + /// default to the identity function. + factory Multimap.fromIterable(Iterable iterable, + {K key(element), V value(element)}) = ListMultimap.fromIterable; + + /// Returns whether this multimap contains the given [value]. + bool containsValue(Object? value); + + /// Returns whether this multimap contains the given [key]. + bool containsKey(Object? key); + + /// Returns whether this multimap contains the given association between [key] + /// and [value]. + bool contains(Object? key, Object? value); + + /// Returns the values for the given [key]. An empty iterable is returned if + /// [key] is not mapped. The returned collection is a view on the multimap. + /// Updates to the collection modify the multimap and likewise, modifications + /// to the multimap are reflected in the returned collection. + Iterable operator [](Object? key); + + /// Adds an association from the given key to the given value. + void add(K key, V value); + + /// Adds an association from the given key to each of the given values. + void addValues(K key, Iterable values); + + /// Adds all associations of [other] to this multimap. + /// + /// The operation is equivalent to doing `this[key] = value` for each key and + /// associated value in other. It iterates over [other], which must therefore + /// not change during the iteration. + void addAll(Multimap other); + + /// Removes the association between the given [key] and [value]. Returns + /// `true` if the association existed, `false` otherwise. + bool remove(Object? key, V? value); + + /// Removes the association for the given [key]. Returns the collection of + /// removed values, or an empty iterable if [key] was unmapped. + Iterable removeAll(Object? key); + + /// Removes all entries of this multimap that satisfy the given [predicate]. + void removeWhere(bool predicate(K key, V value)); + + /// Removes all data from the multimap. + void clear(); + + /// Applies [f] to each {key, `Iterable`} pair of the multimap. + /// + /// It is an error to add or remove keys from the map during iteration. + void forEachKey(void f(K key, Iterable value)); + + /// Applies [f] to each {key, value} pair of the multimap. + /// + /// It is an error to add or remove keys from the map during iteration. + void forEach(void f(K key, V value)); + + /// The keys of [this]. + Iterable get keys; + + /// The values of [this]. + Iterable get values; + + /// Returns a view of this multimap as a map. + Map> asMap(); + + /// The number of keys in the multimap. + int get length; + + /// Returns true if there is no key in the multimap. + bool get isEmpty; + + /// Returns true if there is at least one key in the multimap. + bool get isNotEmpty; +} + +/// Abstract base class for multimap implementations. +abstract class _BaseMultimap> + implements Multimap { + _BaseMultimap(); + + /// Constructs a new multimap. For each element e of [iterable], adds an + /// association from [key](e) to [value](e). [key] and [value] each default + /// to the identity function. + _BaseMultimap.fromIterable(Iterable iterable, + {K key(element)?, V value(element)?}) { + key ??= _id; + value ??= _id; + for (final element in iterable) { + add(key(element), value(element)); + } + } + + static T _id(x) => x; + + final Map _map = {}; + + C _create(); + void _add(C iterable, V value); + void _addAll(C iterable, Iterable value); + void _clear(C iterable); + bool _remove(C iterable, V? value); + void _removeWhere(C iterable, K key, bool predicate(K key, V value)); + Iterable _wrap(Object? key, C iterable); + + @override + bool containsValue(Object? value) => values.contains(value); + @override + bool containsKey(Object? key) => _map.keys.contains(key); + @override + bool contains(Object? key, Object? value) => + _map[key]?.contains(value) == true; + + @override + Iterable operator [](Object? key) { + return _wrap(key, _map[key] ?? _create()); + } + + @override + void add(K key, V value) { + C collection = _map.putIfAbsent(key, _create); + _add(collection, value); + } + + @override + void addValues(K key, Iterable values) { + C collection = _map.putIfAbsent(key, _create); + _addAll(collection, values); + } + + /// Adds all associations of [other] to this multimap. + /// + /// The operation is equivalent to doing `this[key] = value` for each key and + /// associated value in other. It iterates over [other], which must therefore + /// not change during the iteration. + /// + /// This implementation iterates through each key of [other] and adds the + /// associated values to this instance via [addValues]. + @override + void addAll(Multimap other) => other.forEachKey(addValues); + + @override + bool remove(Object? key, V? value) { + if (!_map.containsKey(key)) return false; + bool removed = _remove(_map[key]!, value); + if (removed && _map[key]!.isEmpty) _map.remove(key); + return removed; + } + + @override + Iterable removeAll(Object? key) { + // Cast to dynamic to remove warnings + var values = _map.remove(key) as dynamic; + var retValues = _create() as dynamic; + if (values != null) { + retValues.addAll(values); + values.clear(); + } + return retValues; + } + + @override + void removeWhere(bool predicate(K key, V value)) { + final emptyKeys = Set(); + // TODO(cbracken): iterate over entries + _map.forEach((K key, C values) { + _removeWhere(values, key, predicate); + if (_map[key]!.isEmpty) emptyKeys.add(key); + }); + emptyKeys.forEach(_map.remove); + } + + @override + void clear() { + _map.forEach((K key, C value) => _clear(value)); + _map.clear(); + } + + @override + void forEachKey(void f(K key, C value)) => _map.forEach(f); + + @override + void forEach(void f(K key, V value)) { + _map.forEach((K key, Iterable values) { + for (final V value in values) { + f(key, value); + } + }); + } + + @override + Iterable get keys => _map.keys; + @override + Iterable get values => _map.values.expand((x) => x); + Iterable> get _groupedValues => _map.values; + @override + int get length => _map.length; + @override + bool get isEmpty => _map.isEmpty; + @override + bool get isNotEmpty => _map.isNotEmpty; +} + +/// A multimap implementation that uses [List]s to store the values associated +/// with each key. +class ListMultimap extends _BaseMultimap> { + ListMultimap(); + + /// Constructs a new list-backed multimap. For each element e of [iterable], + /// adds an association from [key](e) to [value](e). [key] and [value] each + /// default to the identity function. + ListMultimap.fromIterable(Iterable iterable, + {K key(element)?, V value(element)?}) + : super.fromIterable(iterable, key: key, value: value); + + @override + List _create() => []; + @override + void _add(List iterable, V value) { + iterable.add(value); + } + + @override + void _addAll(List iterable, Iterable value) => iterable.addAll(value); + @override + void _clear(List iterable) => iterable.clear(); + @override + bool _remove(List iterable, V? value) => iterable.remove(value); + @override + void _removeWhere(List iterable, K key, bool predicate(K key, V value)) => + iterable.removeWhere((value) => predicate(key, value)); + @override + List _wrap(Object? key, List iterable) => + _WrappedList(_map, key, iterable); + @override + List operator [](Object? key) => super[key] as List; + @override + List removeAll(Object? key) => super.removeAll(key) as List; + @override + Map> asMap() => _WrappedMap>(this); +} + +/// A multimap implementation that uses [Set]s to store the values associated +/// with each key. +class SetMultimap extends _BaseMultimap> { + SetMultimap(); + + /// Constructs a new set-backed multimap. For each element e of [iterable], + /// adds an association from [key](e) to [value](e). [key] and [value] each + /// default to the identity function. + SetMultimap.fromIterable(Iterable iterable, + {K key(element)?, V value(element)?}) + : super.fromIterable(iterable, key: key, value: value); + + @override + Set _create() => Set(); + @override + void _add(Set iterable, V value) { + iterable.add(value); + } + + @override + void _addAll(Set iterable, Iterable value) => iterable.addAll(value); + @override + void _clear(Set iterable) => iterable.clear(); + @override + bool _remove(Set iterable, V? value) => iterable.remove(value); + @override + void _removeWhere(Set iterable, K key, bool predicate(K key, V value)) => + iterable.removeWhere((value) => predicate(key, value)); + @override + Set _wrap(Object? key, Set iterable) => + _WrappedSet(_map, key, iterable); + @override + Set operator [](Object? key) => super[key] as Set; + @override + Set removeAll(Object? key) => super.removeAll(key) as Set; + @override + Map> asMap() => _WrappedMap>(this); +} + +/// A [Map] that delegates its operations to an underlying multimap. +class _WrappedMap> implements Map { + _WrappedMap(this._multimap); + + final _BaseMultimap _multimap; + + @override + C? operator [](Object? key) => _multimap[key] as C; // Always non-null. + + @override + void operator []=(K key, C value) { + throw UnsupportedError('Insert unsupported on map view'); + } + + @override + void addAll(Map other) { + throw UnsupportedError('Insert unsupported on map view'); + } + + @override + C putIfAbsent(K key, C ifAbsent()) { + throw UnsupportedError('Insert unsupported on map view'); + } + + @override + void clear() => _multimap.clear(); + @override + bool containsKey(Object? key) => _multimap.containsKey(key); + @override + bool containsValue(Object? value) => _multimap.containsValue(value); + @override + void forEach(void f(K key, C value)) => _multimap.forEachKey(f); + @override + bool get isEmpty => _multimap.isEmpty; + @override + bool get isNotEmpty => _multimap.isNotEmpty; + @override + Iterable get keys => _multimap.keys; + @override + int get length => _multimap.length; + @override + C? remove(Object? key) => _multimap.removeAll(key) as C; // Always non-null. + @override + Iterable get values => _multimap._groupedValues as Iterable; + + @override + Map cast() { + // TODO(cbracken): Dart 2.0 requires this method to be implemented. + throw UnimplementedError('cast'); + } + + @override + Iterable> get entries => _multimap._map.entries; + + @override + void addEntries(Iterable> entries) { + throw UnsupportedError('Insert unsupported on map view'); + } + + @override + Map map(MapEntry transform(K key, C value)) => + _multimap._map.map(transform); + + @override + C update(K key, C update(C value), {C ifAbsent()?}) { + throw UnsupportedError('Update unsupported on map view'); + } + + @override + void updateAll(C update(K key, C value)) { + throw UnsupportedError('Update unsupported on map view'); + } + + @override + void removeWhere(bool test(K key, C value)) { + var keysToRemove = []; + // TODO(cbracken): iterate over entries. + for (final key in keys) { + if (test(key, this[key] as C)) keysToRemove.add(key); + } + keysToRemove.forEach(_multimap.removeAll); + } +} + +/// Iterable wrapper that syncs to an underlying map. +class _WrappedIterable> implements Iterable { + _WrappedIterable(this._map, this._key, this._delegate); + + final K _key; + final Map _map; + C _delegate; + + void _addToMap() => _map[_key] = _delegate; + + /// Ensures we hold an up-to-date delegate. In the case where all mappings + /// for _key are removed from the multimap, the Iterable referenced by + /// _delegate is removed from the underlying map. At that point, any new + /// addition via the multimap triggers the creation of a new Iterable, and + /// the empty delegate we hold would be stale. As such, we check the + /// underlying map and update our delegate when the one we hold is empty. + void _syncDelegate() { + if (_delegate.isEmpty) { + var updated = _map[_key]; + if (updated != null) { + _delegate = updated; + } + } + } + + @override + bool any(bool test(V element)) { + _syncDelegate(); + return _delegate.any(test); + } + + @override + Iterable cast() { + // TODO(cbracken): Dart 2.0 requires this method to be implemented. + throw UnimplementedError('cast'); + } + + @override + bool contains(Object? element) { + _syncDelegate(); + return _delegate.contains(element); + } + + @override + V elementAt(int index) { + _syncDelegate(); + return _delegate.elementAt(index); + } + + @override + bool every(bool test(V element)) { + _syncDelegate(); + return _delegate.every(test); + } + + @override + Iterable expand(Iterable f(V element)) { + _syncDelegate(); + return _delegate.expand(f); + } + + @override + V get first { + _syncDelegate(); + return _delegate.first; + } + + @override + V firstWhere(bool test(V element), {V orElse()?}) { + _syncDelegate(); + return _delegate.firstWhere(test, orElse: orElse); + } + + @override + T fold(T initialValue, T combine(T previousValue, V element)) { + _syncDelegate(); + return _delegate.fold(initialValue, combine); + } + + @override + Iterable followedBy(Iterable other) { + _syncDelegate(); + return _delegate.followedBy(other); + } + + @override + void forEach(void f(V element)) { + _syncDelegate(); + _delegate.forEach(f); + } + + @override + bool get isEmpty { + _syncDelegate(); + return _delegate.isEmpty; + } + + @override + bool get isNotEmpty { + _syncDelegate(); + return _delegate.isNotEmpty; + } + + @override + Iterator get iterator { + _syncDelegate(); + return _delegate.iterator; + } + + @override + String join([String separator = '']) { + _syncDelegate(); + return _delegate.join(separator); + } + + @override + V get last { + _syncDelegate(); + return _delegate.last; + } + + @override + V lastWhere(bool test(V element), {V orElse()?}) { + _syncDelegate(); + return _delegate.lastWhere(test, orElse: orElse); + } + + @override + int get length { + _syncDelegate(); + return _delegate.length; + } + + @override + Iterable map(T f(V element)) { + _syncDelegate(); + return _delegate.map(f); + } + + @override + V reduce(V combine(V value, V element)) { + _syncDelegate(); + return _delegate.reduce(combine); + } + + @override + V get single { + _syncDelegate(); + return _delegate.single; + } + + @override + V singleWhere(bool test(V element), {V orElse()?}) { + _syncDelegate(); + return _delegate.singleWhere(test, orElse: orElse); + } + + @override + Iterable skip(int n) { + _syncDelegate(); + return _delegate.skip(n); + } + + @override + Iterable skipWhile(bool test(V value)) { + _syncDelegate(); + return _delegate.skipWhile(test); + } + + @override + Iterable take(int n) { + _syncDelegate(); + return _delegate.take(n); + } + + @override + Iterable takeWhile(bool test(V value)) { + _syncDelegate(); + return _delegate.takeWhile(test); + } + + @override + List toList({bool growable = true}) { + _syncDelegate(); + return _delegate.toList(growable: growable); + } + + @override + Set toSet() { + _syncDelegate(); + return _delegate.toSet(); + } + + @override + String toString() { + _syncDelegate(); + return _delegate.toString(); + } + + @override + Iterable where(bool test(V element)) { + _syncDelegate(); + return _delegate.where(test); + } + + @override + Iterable whereType() { + // TODO(cbracken): Dart 2.0 requires this method to be implemented. + throw UnimplementedError('whereType'); + } +} + +class _WrappedList extends _WrappedIterable> + implements List { + _WrappedList(Map> map, K key, List delegate) + : super(map, key, delegate); + + @override + V operator [](int index) => elementAt(index); + + @override + void operator []=(int index, V value) { + _syncDelegate(); + _delegate[index] = value; + } + + @override + List operator +(List other) { + _syncDelegate(); + return _delegate + other; + } + + @override + void add(V value) { + _syncDelegate(); + var wasEmpty = _delegate.isEmpty; + _delegate.add(value); + if (wasEmpty) _addToMap(); + } + + @override + void addAll(Iterable iterable) { + _syncDelegate(); + var wasEmpty = _delegate.isEmpty; + _delegate.addAll(iterable); + if (wasEmpty) _addToMap(); + } + + @override + Map asMap() { + _syncDelegate(); + return _delegate.asMap(); + } + + @override + List cast() { + // TODO(cbracken): Dart 2.0 requires this method to be implemented. + throw UnimplementedError('cast'); + } + + @override + void clear() { + _syncDelegate(); + _delegate.clear(); + _map.remove(_key); + } + + @override + void fillRange(int start, int end, [V? fillValue]) { + _syncDelegate(); + _delegate.fillRange(start, end, fillValue); + } + + @override + set first(V value) { + if (isEmpty) throw RangeError.index(0, this); + this[0] = value; + } + + @override + Iterable getRange(int start, int end) { + _syncDelegate(); + return _delegate.getRange(start, end); + } + + @override + int indexOf(V element, [int start = 0]) { + _syncDelegate(); + return _delegate.indexOf(element, start); + } + + @override + int indexWhere(bool test(V element), [int start = 0]) { + _syncDelegate(); + return _delegate.indexWhere(test, start); + } + + @override + void insert(int index, V element) { + _syncDelegate(); + var wasEmpty = _delegate.isEmpty; + _delegate.insert(index, element); + if (wasEmpty) _addToMap(); + } + + @override + void insertAll(int index, Iterable iterable) { + _syncDelegate(); + var wasEmpty = _delegate.isEmpty; + _delegate.insertAll(index, iterable); + if (wasEmpty) _addToMap(); + } + + @override + set last(V value) { + if (isEmpty) throw RangeError.index(0, this); + this[length - 1] = value; + } + + @override + int lastIndexOf(V element, [int? start]) { + _syncDelegate(); + return _delegate.lastIndexOf(element, start); + } + + @override + int lastIndexWhere(bool test(V element), [int? start]) { + _syncDelegate(); + return _delegate.lastIndexWhere(test, start); + } + + @override + set length(int newLength) { + _syncDelegate(); + var wasEmpty = _delegate.isEmpty; + _delegate.length = newLength; + if (wasEmpty) _addToMap(); + } + + @override + bool remove(Object? value) { + _syncDelegate(); + bool removed = _delegate.remove(value); + if (_delegate.isEmpty) _map.remove(_key); + return removed; + } + + @override + V removeAt(int index) { + _syncDelegate(); + V removed = _delegate.removeAt(index); + if (_delegate.isEmpty) _map.remove(_key); + return removed; + } + + @override + V removeLast() { + _syncDelegate(); + V removed = _delegate.removeLast(); + if (_delegate.isEmpty) _map.remove(_key); + return removed; + } + + @override + void removeRange(int start, int end) { + _syncDelegate(); + _delegate.removeRange(start, end); + if (_delegate.isEmpty) _map.remove(_key); + } + + @override + void removeWhere(bool test(V element)) { + _syncDelegate(); + _delegate.removeWhere(test); + if (_delegate.isEmpty) _map.remove(_key); + } + + @override + void replaceRange(int start, int end, Iterable iterable) { + _syncDelegate(); + _delegate.replaceRange(start, end, iterable); + if (_delegate.isEmpty) _map.remove(_key); + } + + @override + void retainWhere(bool test(V element)) { + _syncDelegate(); + _delegate.retainWhere(test); + if (_delegate.isEmpty) _map.remove(_key); + } + + @override + Iterable get reversed { + _syncDelegate(); + return _delegate.reversed; + } + + @override + void setAll(int index, Iterable iterable) { + _syncDelegate(); + _delegate.setAll(index, iterable); + } + + @override + void setRange(int start, int end, Iterable iterable, [int skipCount = 0]) { + _syncDelegate(); + } + + @override + void shuffle([Random? random]) { + _syncDelegate(); + _delegate.shuffle(random); + } + + @override + void sort([int compare(V a, V b)?]) { + _syncDelegate(); + _delegate.sort(compare); + } + + @override + List sublist(int start, [int? end]) { + _syncDelegate(); + return _delegate.sublist(start, end); + } +} + +class _WrappedSet extends _WrappedIterable> + implements Set { + _WrappedSet(Map> map, K key, Set delegate) + : super(map, key, delegate); + + @override + bool add(V value) { + _syncDelegate(); + var wasEmpty = _delegate.isEmpty; + bool wasAdded = _delegate.add(value); + if (wasEmpty) _addToMap(); + return wasAdded; + } + + @override + void addAll(Iterable elements) { + _syncDelegate(); + var wasEmpty = _delegate.isEmpty; + _delegate.addAll(elements); + if (wasEmpty) _addToMap(); + } + + @override + Set cast() { + // TODO(cbracken): Dart 2.0 requires this method to be implemented. + throw UnimplementedError('cast'); + } + + @override + void clear() { + _syncDelegate(); + _delegate.clear(); + _map.remove(_key); + } + + @override + bool containsAll(Iterable other) { + _syncDelegate(); + return _delegate.containsAll(other); + } + + @override + Set difference(Set other) { + _syncDelegate(); + return _delegate.difference(other); + } + + @override + Set intersection(Set other) { + _syncDelegate(); + return _delegate.intersection(other); + } + + @override + V? lookup(Object? object) { + _syncDelegate(); + return _delegate.lookup(object); + } + + @override + bool remove(Object? value) { + _syncDelegate(); + bool removed = _delegate.remove(value); + if (_delegate.isEmpty) _map.remove(_key); + return removed; + } + + @override + void removeAll(Iterable elements) { + _syncDelegate(); + _delegate.removeAll(elements); + if (_delegate.isEmpty) _map.remove(_key); + } + + @override + void removeWhere(bool test(V element)) { + _syncDelegate(); + _delegate.removeWhere(test); + if (_delegate.isEmpty) _map.remove(_key); + } + + @override + void retainAll(Iterable elements) { + _syncDelegate(); + _delegate.retainAll(elements); + if (_delegate.isEmpty) _map.remove(_key); + } + + @override + void retainWhere(bool test(V element)) { + _syncDelegate(); + _delegate.retainWhere(test); + if (_delegate.isEmpty) _map.remove(_key); + } + + @override + Set union(Set other) { + _syncDelegate(); + return _delegate.union(other); + } +} diff --git a/guru_app/packages/guru_utils/lib/quiver/collection/treeset.dart b/guru_app/packages/guru_utils/lib/quiver/collection/treeset.dart new file mode 100644 index 0000000..f13852b --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/collection/treeset.dart @@ -0,0 +1,1045 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:collection'; + +int _defaultCompare(a, b) { + return a.compareTo(b); +} + +/// A [Set] of items stored in a binary tree according to [comparator]. +/// Supports bidirectional iteration. +abstract class TreeSet extends IterableBase implements Set { + /// Create a new [TreeSet] with an ordering defined by [comparator] or the + /// default `(a, b) => a.compareTo(b)`. + factory TreeSet({Comparator comparator = _defaultCompare}) { + return AvlTreeSet(comparator: comparator); + } + + TreeSet._(this.comparator); + + final Comparator comparator; + + @override + int get length; + + @override + bool get isEmpty => length == 0; + + @override + bool get isNotEmpty => length != 0; + + /// Returns a [TreeIterator] that iterates over this tree. + @override + TreeIterator get iterator; + + /// Returns a [TreeIterator] that iterates over this tree, in + /// reverse. + TreeIterator get reverseIterator; + + /// Returns a [TreeIterator] that starts at [anchor]. By default, + /// the iterator includes the anchor with the first movement; set [inclusive] + /// to false if you want to exclude the anchor. Set [reversed] to true to + /// change the direction of of moveNext and movePrevious. + /// + /// Note: This iterator allows you to walk the entire set. It does not + /// present a subview. + TreeIterator fromIterator(V anchor, + {bool reversed = false, bool inclusive = true}); + + /// Search the tree for the matching [object] or the [nearestOption] + /// if missing. See [TreeSearch]. + V nearest(V object, {TreeSearch nearestOption = TreeSearch.NEAREST}); + + @override + Set cast(); + +// TODO(codefu): toString or not toString, that is the question. +} + +/// Controls the results for [TreeSet.searchNearest]() +enum TreeSearch { + /// If result not found, always chose the smaller element + // ignore: constant_identifier_names + LESS_THAN, + + /// If result not found, chose the nearest based on comparison + // ignore: constant_identifier_names + NEAREST, + + /// If result not found, always chose the greater element + // ignore: constant_identifier_names + GREATER_THAN +} + +/// A node in the [TreeSet]. +abstract class _TreeNode { + /// TreeNodes are always allocated as leafs. + _TreeNode({required this.object}); + + _TreeNode get left; + bool get hasLeft; + + _TreeNode get right; + bool get hasRight; + + // TODO(codefu): Remove need for [parent]; this is just an implementation + // note. + _TreeNode get parent; + bool get hasParent; + + V object; + + /// Return the minimum node for the subtree + _TreeNode get minimumNode { + var node = this; + while (node.hasLeft) { + node = node.left; + } + return node; + } + + /// Return the maximum node for the subtree + _TreeNode get maximumNode { + var node = this; + while (node.hasRight) { + node = node.right; + } + return node; + } + + /// Return the next greatest element (or null) + _TreeNode? get successor { + var node = this; + if (node.hasRight) { + return node.right.minimumNode; + } + while ( + node.hasParent && node.parent.hasRight && node == node.parent.right) { + node = node.parent; + } + return node.hasParent ? node.parent : null; + } + + /// Return the next smaller element (or null) + _TreeNode? get predecessor { + var node = this; + if (node.hasLeft) { + return node.left.maximumNode; + } + while (node.hasParent && node.parent.hasLeft && node.parent.left == node) { + node = node.parent; + } + return node.hasParent ? node.parent : null; + } +} + +/// AVL implementation of a self-balancing binary tree. Optimized for lookup +/// operations. +/// +/// Notes: Adapted from "Introduction to Algorithms", second edition, +/// by Thomas H. Cormen, Charles E. Leiserson, +/// Ronald L. Rivest, Clifford Stein. +/// chapter 13.2 +class AvlTreeSet extends TreeSet { + AvlTreeSet({Comparator comparator = _defaultCompare}) + : super._(comparator); + + int _length = 0; + AvlNode? _root; + // Modification count to the tree, monotonically increasing + int _modCount = 0; + + @override + int get length => _length; + + /// Add the element to the tree. + @override + bool add(V element) { + if (_root == null) { + AvlNode node = AvlNode(object: element); + _root = node; + ++_length; + ++_modCount; + return true; + } + + AvlNode x = _root!; + while (true) { + int compare = comparator(element, x.object); + if (compare == 0) { + return false; + } else if (compare < 0) { + if (!x.hasLeft) { + AvlNode node = AvlNode(object: element).._parent = x; + x + .._left = node + .._balanceFactor -= 1; + break; + } + x = x.left; + } else { + if (!x.hasRight) { + AvlNode node = AvlNode(object: element).._parent = x; + x + .._right = node + .._balanceFactor += 1; + break; + } + x = x.right; + } + } + + ++_modCount; + + // AVL balancing act (for height balanced trees) + // Now that we've inserted, we've unbalanced some trees, we need + // to follow the tree back up to the _root double checking that the tree + // is still balanced and _maybe_ perform a single or double rotation. + // Note: Left additions == -1, Right additions == +1 + // Balanced Node = { -1, 0, 1 }, out of balance = { -2, 2 } + // Single rotation when Parent & Child share signed balance, + // Double rotation when sign differs! + AvlNode node = x; + while (node._balanceFactor != 0 && node.hasParent) { + // Find out which side of the parent we're on + if (node.parent._left == node) { + node.parent._balanceFactor -= 1; + } else { + node.parent._balanceFactor += 1; + } + + node = node.parent; + if (node._balanceFactor == 2) { + // Heavy on the right side - Test for which rotation to perform + if (node.right._balanceFactor == 1) { + // Single (left) rotation; this will balance everything to zero + _rotateLeft(node); + node._balanceFactor = node.parent._balanceFactor = 0; + node = node.parent; + } else { + // Double (Right/Left) rotation + // node will now be old node.right.left + _rotateRightLeft(node); + node = node.parent; // Update to new parent (old grandchild) + if (node._balanceFactor == 1) { + node.right._balanceFactor = 0; + node.left._balanceFactor = -1; + } else if (node._balanceFactor == 0) { + node.right._balanceFactor = 0; + node.left._balanceFactor = 0; + } else { + node.right._balanceFactor = 1; + node.left._balanceFactor = 0; + } + node._balanceFactor = 0; + } + break; // out of loop, we're balanced + } else if (node._balanceFactor == -2) { + // Heavy on the left side - Test for which rotation to perform + if (node.left._balanceFactor == -1) { + _rotateRight(node); + node._balanceFactor = node.parent._balanceFactor = 0; + node = node.parent; + } else { + // Double (Left/Right) rotation + // node will now be old node.left.right + _rotateLeftRight(node); + node = node.parent; + if (node._balanceFactor == -1) { + node.right._balanceFactor = 1; + node.left._balanceFactor = 0; + } else if (node._balanceFactor == 0) { + node.right._balanceFactor = 0; + node.left._balanceFactor = 0; + } else { + node.right._balanceFactor = 0; + node.left._balanceFactor = -1; + } + node._balanceFactor = 0; + } + break; // out of loop, we're balanced + } + } // end of while (balancing) + _length++; + return true; + } + + /// Test to see if an element is stored in the tree + AvlNode? _getNode(V element) { + AvlNode? x = _root; + while (x != null) { + int compare = comparator(element, x.object); + if (compare == 0) { + // This only means our node matches; we need to search for the exact + // element. We could have been glutons and used a hashmap to back. + return x; + } else if (compare < 0) { + x = x._left; + } else { + x = x._right; + } + } + return null; + } + + /// This function will right rotate/pivot N with its left child, placing + /// it on the right of its left child. + /// + /// N Y + /// / \ / \ + /// Y A Z N + /// / \ ==> / \ / \ + /// Z B D CB A + /// / \ + /// D C + /// + /// Assertion: must have a left element + void _rotateRight(AvlNode node) { + AvlNode? y = node.left; + + // turn Y's right subtree(B) into N's left subtree. + node._left = y._right; + if (node.hasLeft) { + node.left._parent = node; + } + y._parent = node._parent; + if (y.hasParent) { + if (node.parent._left == node) { + node.parent._left = y; + } else { + node.parent._right = y; + } + } else { + _root = y; + } + y._right = node; + node._parent = y; + } + + /// This function will left rotate/pivot N with its right child, placing + /// it on the left of its right child. + /// + /// N Y + /// / \ / \ + /// A Y N Z + /// / \ ==> / \ / \ + /// B Z A BC D + /// / \ + /// C D + /// + /// Assertion: must have a right element + void _rotateLeft(AvlNode node) { + AvlNode? y = node.right; + + // turn Y's left subtree(B) into N's right subtree. + node._right = y._left; + if (node.hasRight) { + node.right._parent = node; + } + y._parent = node._parent; + if (y.hasParent) { + if (node.parent._left == node) { + y.parent._left = y; + } else { + y.parent._right = y; + } + } else { + _root = y; + } + y._left = node; + node._parent = y; + } + + /// This function will double rotate node with right/left operations. + /// node is S. + /// + /// S G + /// / \ / \ + /// A C S C + /// / \ ==> / \ / \ + /// G B A DC B + /// / \ + /// D C + void _rotateRightLeft(AvlNode node) { + _rotateRight(node.right); + _rotateLeft(node); + } + + /// This function will double rotate node with left/right operations. + /// node is S. + /// + /// S G + /// / \ / \ + /// C A C S + /// / \ ==> / \ / \ + /// B G B CD A + /// / \ + /// C D + void _rotateLeftRight(AvlNode node) { + _rotateLeft(node.left); + _rotateRight(node); + } + + @override + bool addAll(Iterable items) { + bool modified = false; + for (final item in items) { + if (add(item)) { + modified = true; + } + } + return modified; + } + + @override + AvlTreeSet cast() { + // TODO(codefu): Dart 2.0 requires this method to be implemented. + throw UnimplementedError('cast'); + } + + @override + void clear() { + _length = 0; + _root = null; + ++_modCount; + } + + @override + bool containsAll(Iterable items) { + for (final item in items) { + if (!contains(item)) return false; + } + return true; + } + + @override + bool remove(Object? item) { + if (item is! V) return false; + + AvlNode? x = _getNode(item); + if (x != null) { + _removeNode(x); + return true; + } + return false; + } + + void _removeNode(AvlNode node) { + AvlNode? y; + AvlNode? w; + + ++_modCount; + --_length; + + // note: if you read wikipedia, it states remove the node if its a leaf, + // otherwise, replace it with its predecessor or successor. We're not. + if (!node.hasRight || !node.right.hasLeft) { + // simple solutions + if (node.hasRight) { + y = node.right; + y._parent = node._parent; + y._balanceFactor = node._balanceFactor - 1; + y._left = node._left; + if (y.hasLeft) { + y.left._parent = y; + } + } else if (node.hasLeft) { + y = node.left; + y._parent = node._parent; + y._balanceFactor = node._balanceFactor + 1; + } else { + y = null; + } + if (_root == node) { + _root = y; + } else if (node.parent._left == node) { + node.parent._left = y; + if (y == null) { + // account for leaf deletions changing the balance + node.parent._balanceFactor += 1; + y = node.parent; // start searching from here; + } + } else { + node.parent._right = y; + if (y == null) { + node.parent._balanceFactor -= 1; + y = node.parent; + } + } + w = y; + } else { + // This node is not a leaf; we should find the successor node, swap + //it with this* and then update the balance factors. + y = node.successor as AvlNode; + y._left = node._left; + if (y.hasLeft) { + y.left._parent = y; + } + + w = y.parent; + w._left = y._right; + if (w.hasLeft) { + w.left._parent = w; + } + // known: we're removing from the left + w._balanceFactor += 1; + + // known due to test for n->r->l above + y._right = node._right; + y.right._parent = y; + y._balanceFactor = node._balanceFactor; + + y._parent = node._parent; + if (_root == node) { + _root = y; + } else if (node.parent._left == node) { + node.parent._left = y; + } else { + node.parent._right = y; + } + } + + // Safe to kill node now; its free to go. + node._balanceFactor = 0; + node._left = node._right = node._parent = null; + + // Re-balance to the top, ending early if OK + _rebalance(w); + } + + void _rebalance(AvlNode? node) { + while (node != null) { + if (node._balanceFactor == -1 || node._balanceFactor == 1) { + // The height of node hasn't changed; done! + break; + } + if (node._balanceFactor == 2) { + // Heavy on the right side; figure out which rotation to perform + if (node.right._balanceFactor == -1) { + _rotateRightLeft(node); + node = node.parent; // old grand-child! + if (node._balanceFactor == 1) { + node.right._balanceFactor = 0; + node.left._balanceFactor = -1; + } else if (node._balanceFactor == 0) { + node.right._balanceFactor = 0; + node.left._balanceFactor = 0; + } else { + node.right._balanceFactor = 1; + node.left._balanceFactor = 0; + } + node._balanceFactor = 0; + } else { + // single left-rotation + _rotateLeft(node); + if (node.parent._balanceFactor == 0) { + node.parent._balanceFactor = -1; + node._balanceFactor = 1; + break; + } else { + node.parent._balanceFactor = 0; + node._balanceFactor = 0; + node = node.parent; + continue; + } + } + } else if (node._balanceFactor == -2) { + // Heavy on the left + if (node.left._balanceFactor == 1) { + _rotateLeftRight(node); + node = node.parent; // old grand-child! + if (node._balanceFactor == -1) { + node.right._balanceFactor = 1; + node.left._balanceFactor = 0; + } else if (node._balanceFactor == 0) { + node.right._balanceFactor = 0; + node.left._balanceFactor = 0; + } else { + node.right._balanceFactor = 0; + node.left._balanceFactor = -1; + } + node._balanceFactor = 0; + } else { + _rotateRight(node); + if (node.parent._balanceFactor == 0) { + node.parent._balanceFactor = 1; + node._balanceFactor = -1; + break; + } else { + node.parent._balanceFactor = 0; + node._balanceFactor = 0; + node = node.parent; + continue; + } + } + } + + // continue up the tree for testing + if (node.hasParent) { + // The concept of balance here is reverse from addition; since + // we are taking away weight from one side or the other (thus + // the balance changes in favor of the other side) + if (node.parent.hasLeft && node.parent.left == node) { + node.parent._balanceFactor += 1; + } else { + node.parent._balanceFactor -= 1; + } + } + node = node.hasParent ? node.parent : null; + } + } + + /// See [Set.removeAll] + @override + void removeAll(Iterable items) { + items.forEach(remove); + } + + /// See [Set.retainAll] + @override + void retainAll(Iterable elements) { + List chosen = []; + for (final target in elements) { + if (target is V && contains(target)) { + chosen.add(target); + } + } + clear(); + addAll(chosen); + } + + /// See [Set.retainWhere] + @override + void retainWhere(bool Function(V element) test) { + List chosen = []; + for (final target in this) { + if (test(target)) { + chosen.add(target); + } + } + clear(); + addAll(chosen); + } + + /// See [Set.removeWhere] + @override + void removeWhere(bool Function(V element) test) { + List damned = []; + for (final target in this) { + if (test(target)) { + damned.add(target); + } + } + removeAll(damned); + } + + /// See [IterableBase.first] + @override + V get first { + _TreeNode? min = _root?.minimumNode; + if (min != null) { + return min.object; + } + throw StateError('No first element'); + } + + /// See [IterableBase.last] + @override + V get last { + _TreeNode? max = _root?.maximumNode; + if (max != null) { + return max.object; + } + throw StateError('No last element'); + } + + /// See [Set.lookup] + @override + V? lookup(Object? element) { + if (element is! V || _root == null) return null; + AvlNode? x = _root; + int compare = 0; + while (x != null) { + compare = comparator(element, x.object); + if (compare == 0) { + return x.object; + } else if (compare < 0) { + x = x._left; + } else { + x = x._right; + } + } + return null; + } + + @override + V nearest(V object, {TreeSearch nearestOption = TreeSearch.NEAREST}) { + AvlNode? found = _searchNearest(object, option: nearestOption); + if (found != null) { + return found.object; + } + throw StateError('No nearest element'); + } + + /// Search the tree for the matching element, or the 'nearest' node. + /// NOTE: [BinaryTree.comparator] needs to have finer granularity than -1,0,1 + /// in order for this to return something that's meaningful. + AvlNode? _searchNearest(V? element, + {TreeSearch option = TreeSearch.NEAREST}) { + if (element == null || _root == null) { + return null; + } + AvlNode? x = _root; + late AvlNode previous; + int compare = 0; + while (x != null) { + previous = x; + compare = comparator(element, x.object); + if (compare == 0) { + return x; + } else if (compare < 0) { + x = x._left; + } else { + x = x._right; + } + } + + if (option == TreeSearch.GREATER_THAN) { + return (compare < 0 ? previous : previous.successor) as AvlNode?; + } else if (option == TreeSearch.LESS_THAN) { + return (compare < 0 ? previous.predecessor : previous) as AvlNode?; + } + // Default: nearest absolute value + // Fell off the tree looking for the exact match; now we need + // to find the nearest element. + x = (compare < 0 ? previous.predecessor : previous.successor) + as AvlNode?; + if (x == null) { + return previous; + } + int otherCompare = comparator(element, x.object); + if (compare < 0) { + return compare.abs() < otherCompare ? previous : x; + } + return otherCompare.abs() < compare ? x : previous; + } + + // + // [IterableBase] Methods + // + + /// See [IterableBase.iterator] + @override + TreeIterator get iterator => TreeIterator._(this); + + /// See [TreeSet.reverseIterator] + @override + TreeIterator get reverseIterator => TreeIterator._(this, reversed: true); + + /// See [TreeSet.fromIterator] + @override + TreeIterator fromIterator(V anchor, + {bool reversed = false, bool inclusive = true}) => + TreeIterator._(this, + anchorObject: anchor, reversed: reversed, inclusive: inclusive); + + /// See [IterableBase.contains] + @override + bool contains(Object? object) { + if (object is! V) { + return false; + } + return _getNode(object) != null; + } + + // + // [Set] methods + // + + /// See [Set.intersection] + @override + Set intersection(Set other) { + TreeSet set = TreeSet(comparator: comparator); + + // Optimized for sorted sets + if (other is TreeSet) { + var i1 = iterator; + var i2 = other.iterator; + var hasMore1 = i1.moveNext(); + var hasMore2 = i2.moveNext(); + while (hasMore1 && hasMore2) { + var c = comparator(i1.current, i2.current); + if (c == 0) { + set.add(i1.current); + hasMore1 = i1.moveNext(); + hasMore2 = i2.moveNext(); + } else if (c < 0) { + hasMore1 = i1.moveNext(); + } else { + hasMore2 = i2.moveNext(); + } + } + return set; + } + + // Non-optimized version. + for (final target in this) { + if (other.contains(target)) { + set.add(target); + } + } + return set; + } + + /// See [Set.union] + @override + Set union(Set other) { + TreeSet set = TreeSet(comparator: comparator); + + if (other is TreeSet) { + Iterator i1 = iterator; + var i2 = other.iterator; + var hasMore1 = i1.moveNext(); + var hasMore2 = i2.moveNext(); + while (hasMore1 && hasMore2) { + var c = comparator(i1.current, i2.current); + if (c == 0) { + set.add(i1.current); + hasMore1 = i1.moveNext(); + hasMore2 = i2.moveNext(); + } else if (c < 0) { + set.add(i1.current); + hasMore1 = i1.moveNext(); + } else { + set.add(i2.current); + hasMore2 = i2.moveNext(); + } + } + if (hasMore1 || hasMore2) { + i1 = hasMore1 ? i1 : i2; + do { + set.add(i1.current); + } while (i1.moveNext()); + } + return set; + } + + // Non-optimized version. + return set + ..addAll(this) + ..addAll(other); + } + + /// See [Set.difference] + @override + Set difference(Set other) { + TreeSet set = TreeSet(comparator: comparator); + + if (other is TreeSet) { + var i1 = iterator; + var i2 = other.iterator; + var hasMore1 = i1.moveNext(); + var hasMore2 = i2.moveNext(); + while (hasMore1 && hasMore2) { + var c = comparator(i1.current, i2.current); + if (c == 0) { + hasMore1 = i1.moveNext(); + hasMore2 = i2.moveNext(); + } else if (c < 0) { + set.add(i1.current); + hasMore1 = i1.moveNext(); + } else { + hasMore2 = i2.moveNext(); + } + } + if (hasMore1) { + do { + set.add(i1.current); + } while (i1.moveNext()); + } + return set; + } + + // Non-optimized version. + for (final target in this) { + if (!other.contains(target)) { + set.add(target); + } + } + return set; + } +} + +AvlNode? debugGetNode(AvlTreeSet treeset, V object) { + return treeset._getNode(object); +} + +/// This iterator either starts at the beginning or end (see [TreeSet.iterator] +/// and [TreeSet.reverseIterator]) or from an anchor point in the set (see +/// [TreeSet.fromIterator]). When using fromIterator, the initial anchor point +/// is included in the first movement (either [moveNext] or [movePrevious]) but +/// can optionally be excluded in the constructor. +class TreeIterator implements Iterator { + TreeIterator._(this.tree, + {this.reversed = false, this.inclusive = true, V? anchorObject}) + : _anchorObject = anchorObject, + _modCountGuard = tree._modCount { + final anchor = _anchorObject; + + if (anchor == null || tree.isEmpty) { + // If the anchor is far left or right, we're just a normal iterator. + _state = reversed ? _right : _left; + _moveNext = reversed ? _movePreviousNormal : _moveNextNormal; + _movePrevious = reversed ? _moveNextNormal : _movePreviousNormal; + return; + } + + _state = _walk; + // Else we've got an anchor we have to worry about initializing from. + // This isn't known till the caller actually performs a previous/next. + _moveNext = () { + _current = tree._searchNearest(anchor, + option: reversed ? TreeSearch.LESS_THAN : TreeSearch.GREATER_THAN); + _moveNext = reversed ? _movePreviousNormal : _moveNextNormal; + _movePrevious = reversed ? _moveNextNormal : _movePreviousNormal; + if (_current == null) { + _state = reversed ? _left : _right; + } else if (tree.comparator(_current!.object, anchor) == 0 && !inclusive) { + _moveNext(); + } + return _state == _walk; + }; + + _movePrevious = () { + _current = tree._searchNearest(anchor, + option: reversed ? TreeSearch.GREATER_THAN : TreeSearch.LESS_THAN); + _moveNext = reversed ? _movePreviousNormal : _moveNextNormal; + _movePrevious = reversed ? _moveNextNormal : _movePreviousNormal; + if (_current == null) { + _state = reversed ? _right : _left; + } else if (tree.comparator(_current!.object, anchor) == 0 && !inclusive) { + _movePrevious(); + } + return _state == _walk; + }; + } + + static const _left = -1; + static const _walk = 0; + static const _right = 1; + + final bool reversed; + final AvlTreeSet tree; + final int _modCountGuard; + final V? _anchorObject; + final bool inclusive; + + late bool Function() _moveNext; + late bool Function() _movePrevious; + + late int _state; + _TreeNode? _current; + + @override + V get current { + // Prior to NNBD, this returned null when iteration was complete. In order + // to avoid a hard breaking change, we return "null as V" in that case so + // that if strong checking is not enabled or V is nullable, the existing + // behavior is preserved. + if (_state == _walk && _current != null) { + return _current?.object as V; + } + return null as V; + } + + @override + bool moveNext() => _moveNext(); + + bool movePrevious() => _movePrevious(); + + bool _moveNextNormal() { + if (_modCountGuard != tree._modCount) { + throw ConcurrentModificationError(tree); + } + if (_state == _right || tree.isEmpty) return false; + switch (_state) { + case _left: + _current = tree._root!.minimumNode; + _state = _walk; + return true; + case _walk: + default: + _current = _current!.successor; + if (_current == null) { + _state = _right; + } + return _state == _walk; + } + } + + bool _movePreviousNormal() { + if (_modCountGuard != tree._modCount) { + throw ConcurrentModificationError(tree); + } + if (_state == _left || tree.isEmpty) return false; + switch (_state) { + case _right: + _current = tree._root!.maximumNode; + _state = _walk; + return true; + case _walk: + default: + _current = _current!.predecessor; + if (_current == null) { + _state = _left; + } + return _state == _walk; + } + } +} + +/// Private class used to track element insertions in the [TreeSet]. +class AvlNode extends _TreeNode { + AvlNode({required super.object}); + + AvlNode? _left; + AvlNode? _right; + // TODO(codefu): Remove need for [parent]; this is just an implementation note + AvlNode? _parent; + int _balanceFactor = 0; + + @override + AvlNode get left => _left!; + + @override + bool get hasLeft => _left != null; + + @override + AvlNode get right => _right!; + + @override + bool get hasRight => _right != null; + + @override + AvlNode get parent => _parent!; + + @override + bool get hasParent => _parent != null; + + int get balance => _balanceFactor; + + @override + String toString() => '(b:$balance o: $object l:$hasLeft r:$hasRight)'; +} \ No newline at end of file diff --git a/guru_app/packages/guru_utils/lib/quiver/collection/utils.dart b/guru_app/packages/guru_utils/lib/quiver/collection/utils.dart new file mode 100644 index 0000000..72ba755 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/collection/utils.dart @@ -0,0 +1,80 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Checks [List]s [a] and [b] for equality. +/// +/// Returns `true` if [a] and [b] are both null, or they are the same length +/// and every element of [a] is equal to the corresponding element at the same +/// index in [b]. +bool listsEqual(List? a, List? b) { + if (a == b) return true; + if (a == null || b == null) return false; + if (a.length != b.length) return false; + + for (int i = 0; i < a.length; i++) { + if (a[i] != b[i]) return false; + } + + return true; +} + +/// Checks [Map]s [a] and [b] for equality. +/// +/// Returns `true` if [a] and [b] are both null, or they are the same length +/// and every key `k` in [a] exists in [b] and the values `a[k] == b[k]`. +bool mapsEqual(Map? a, Map? b) { + if (a == b) return true; + if (a == null || b == null) return false; + if (a.length != b.length) return false; + + for (final k in a.keys) { + var bValue = b[k]; + if (bValue == null && !b.containsKey(k)) return false; + if (bValue != a[k]) return false; + } + + return true; +} + +/// Checks [Set]s [a] and [b] for equality. +/// +/// Returns `true` if [a] and [b] are both null, or they are the same length and +/// every element in [b] exists in [a]. +bool setsEqual(Set? a, Set? b) { + if (a == b) return true; + if (a == null || b == null) return false; + if (a.length != b.length) return false; + + return a.containsAll(b); +} + +/// Returns the index of the first item in [elements] where [package:x2blocks/utils/cate] +/// evaluates to true. +/// +/// Returns -1 if there are no items where [predicate] evaluates to true. +int indexOf(Iterable elements, bool predicate(T element)) { + if (elements is List) { + for (int i = 0; i < elements.length; i++) { + if (predicate(elements[i])) return i; + } + return -1; + } + + int i = 0; + for (final element in elements) { + if (predicate(element)) return i; + i++; + } + return -1; +} diff --git a/guru_app/packages/guru_utils/lib/quiver/core.dart b/guru_app/packages/guru_utils/lib/quiver/core.dart new file mode 100644 index 0000000..9b1cd49 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/core.dart @@ -0,0 +1,20 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Simple code with broad use cases. +library quiver.core; + +export 'core/hash.dart'; +export 'core/optional.dart'; +export 'core/utils.dart'; diff --git a/guru_app/packages/guru_utils/lib/quiver/core/hash.dart b/guru_app/packages/guru_utils/lib/quiver/core/hash.dart new file mode 100644 index 0000000..4c9e091 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/core/hash.dart @@ -0,0 +1,43 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Generates a hash code for multiple [objects]. +int hashObjects(Iterable objects) => + _finish(objects.fold(0, (h, i) => _combine(h, i.hashCode))); + +/// Generates a hash code for two objects. +int hash2(a, b) => _finish(_combine(_combine(0, a.hashCode), b.hashCode)); + +/// Generates a hash code for three objects. +int hash3(a, b, c) => _finish( + _combine(_combine(_combine(0, a.hashCode), b.hashCode), c.hashCode)); + +/// Generates a hash code for four objects. +int hash4(a, b, c, d) => _finish(_combine( + _combine(_combine(_combine(0, a.hashCode), b.hashCode), c.hashCode), + d.hashCode)); + +// Jenkins hash functions + +int _combine(int hash, int value) { + hash = 0x1fffffff & (hash + value); + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); +} + +int _finish(int hash) { + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); +} diff --git a/guru_app/packages/guru_utils/lib/quiver/core/optional.dart b/guru_app/packages/guru_utils/lib/quiver/core/optional.dart new file mode 100644 index 0000000..8a3f2cc --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/core/optional.dart @@ -0,0 +1,127 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:collection'; + +/// A value that might be absent. +/// +/// Use Optional as an alternative to allowing fields, parameters or return +/// values to be null. It signals that a value is not required and provides +/// convenience methods for dealing with the absent case. +// +// TODO(cbracken): Consider making this Optional. +// See: https://github.com/google/quiver-dart/issues/666 +class Optional extends IterableBase { + /// Constructs an empty Optional. + const Optional.absent() : _value = null; + + /// Constructs an Optional of the given [value]. + /// + /// Throws [ArgumentError] if [value] is null. + Optional.of(T value) : _value = value { + // TODO(cbracken): Delete and make this ctor const once mixed-mode + // execution is no longer around. + ArgumentError.checkNotNull(value); + } + + /// Constructs an Optional of the given [value]. + /// + /// If [value] is null, returns [absent()]. + const Optional.fromNullable(T? value) : _value = value; + + final T? _value; + + /// True when this optional contains a value. + bool get isPresent => _value != null; + + /// True when this optional contains no value. + bool get isNotPresent => _value == null; + + /// Gets the Optional value. + /// + /// Throws [StateError] if [value] is null. + T get value { + if (_value == null) { + throw StateError('value called on absent Optional.'); + } + return _value!; + } + + /// Executes a function if the Optional value is present. + void ifPresent(void ifPresent(T value)) { + if (isPresent) { + ifPresent(_value!); + } + } + + /// Execution a function if the Optional value is absent. + void ifAbsent(void ifAbsent()) { + if (!isPresent) { + ifAbsent(); + } + } + + /// Gets the Optional value with a default. + /// + /// The default is returned if the Optional is [absent()]. + /// + /// Throws [ArgumentError] if [defaultValue] is null. + T or(T defaultValue) { + return _value ?? defaultValue; + } + + /// Gets the Optional value, or [null] if there is none. + T? get orNull => _value; + + /// Transforms the Optional value. + /// + /// If the Optional is [absent()], returns [absent()] without applying the transformer. + /// + /// The transformer must not return [null]. If it does, an [ArgumentError] is thrown. + Optional transform(S transformer(T value)) { + return _value == null + ? Optional.absent() + : Optional.of(transformer(_value!)); + } + + /// Transforms the Optional value. + /// + /// If the Optional is [absent()], returns [absent()] without applying the transformer. + /// + /// Returns [absent()] if the transformer returns [null]. + Optional transformNullable(S? transformer(T value)) { + return _value == null + ? Optional.absent() + : Optional.fromNullable(transformer(_value!)); + } + + @override + Iterator get iterator => + isPresent ? [_value!].iterator : Iterable.empty().iterator; + + /// Delegates to the underlying [value] hashCode. + @override + int get hashCode => _value.hashCode; + + /// Delegates to the underlying [value] operator==. + @override + bool operator ==(Object o) => o is Optional && o._value == _value; + + @override + String toString() { + return _value == null + ? 'Optional { absent }' + : 'Optional { value: $_value }'; + } +} diff --git a/guru_app/packages/guru_utils/lib/quiver/core/utils.dart b/guru_app/packages/guru_utils/lib/quiver/core/utils.dart new file mode 100644 index 0000000..43bcae6 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/core/utils.dart @@ -0,0 +1,33 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Returns the first non-null argument. +/// +/// If all arguments are null, throws an [ArgumentError]. +/// +/// Users of Dart SDK 2.1 or later should prefer: +/// +/// var value = o1 ?? o2 ?? o3 ?? o4; +/// ArgumentError.checkNotNull(value); +/// +/// If [o1] is an [Optional], this can be accomplished with `o1.or(o2)`. +@Deprecated('Use ?? and ArgumentError.checkNotNull. Will be removed in 4.0.0') +dynamic firstNonNull(o1, o2, [o3, o4]) { + // TODO(cbracken): make this generic. + if (o1 != null) return o1; + if (o2 != null) return o2; + if (o3 != null) return o3; + if (o4 != null) return o4; + throw ArgumentError('All arguments were null'); +} diff --git a/guru_app/packages/guru_utils/lib/quiver/iterables.dart b/guru_app/packages/guru_utils/lib/quiver/iterables.dart new file mode 100644 index 0000000..05ea0fd --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/iterables.dart @@ -0,0 +1,27 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +library quiver.iterables; + +export 'iterables/concat.dart'; +export 'iterables/count.dart'; +export 'iterables/cycle.dart'; +export 'iterables/enumerate.dart'; +export 'iterables/generating_iterable.dart'; +export 'iterables/infinite_iterable.dart'; +export 'iterables/merge.dart'; +export 'iterables/min_max.dart'; +export 'iterables/partition.dart'; +export 'iterables/range.dart'; +export 'iterables/zip.dart'; diff --git a/guru_app/packages/guru_utils/lib/quiver/iterables/concat.dart b/guru_app/packages/guru_utils/lib/quiver/iterables/concat.dart new file mode 100644 index 0000000..7e93e50 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/iterables/concat.dart @@ -0,0 +1,19 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Returns the concatenation of the input iterables. +/// +/// The returned iterable is a lazily-evaluated view on the input iterables. +Iterable concat(Iterable> iterables) => + iterables.expand((x) => x); diff --git a/guru_app/packages/guru_utils/lib/quiver/iterables/count.dart b/guru_app/packages/guru_utils/lib/quiver/iterables/count.dart new file mode 100644 index 0000000..644ae13 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/iterables/count.dart @@ -0,0 +1,51 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'infinite_iterable.dart'; + +/// Returns an infinite [Iterable] of [num]s, starting from [start] and +/// increasing by [step]. +/// +/// Calling [Iterator.current] before [Iterator.moveNext] throws [StateError]. +Iterable count([num start = 0, num step = 1]) => _Count(start, step); + +class _Count extends InfiniteIterable { + _Count(this.start, this.step); + + final num start, step; + + @override + Iterator get iterator => _CountIterator(start, step); + + // TODO(justin): return an infinite list for toList() and a special Set + // implementation for toSet()? +} + +class _CountIterator implements Iterator { + _CountIterator(this._start, this._step); + + final num _start, _step; + num? _current; + + @override + num get current { + return _current as num; + } + + @override + bool moveNext() { + _current = (_current == null) ? _start : _current! + _step; + return true; + } +} diff --git a/guru_app/packages/guru_utils/lib/quiver/iterables/cycle.dart b/guru_app/packages/guru_utils/lib/quiver/iterables/cycle.dart new file mode 100644 index 0000000..3755990 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/iterables/cycle.dart @@ -0,0 +1,57 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'infinite_iterable.dart'; + +/// Returns an [Iterable] that infinitely cycles through the elements of +/// [iterable]. If [iterable] is empty, the returned Iterable will also be empty. +Iterable cycle(Iterable iterable) => _Cycle(iterable); + +class _Cycle extends InfiniteIterable { + _Cycle(this._iterable); + + final Iterable _iterable; + + @override + Iterator get iterator => _CycleIterator(_iterable); + + @override + bool get isEmpty => _iterable.isEmpty; + + @override + bool get isNotEmpty => _iterable.isNotEmpty; + + // TODO(justin): add methods that can be answered by the wrapped iterable +} + +class _CycleIterator implements Iterator { + _CycleIterator(Iterable _iterable) + : _iterable = _iterable, + _iterator = _iterable.iterator; + + final Iterable _iterable; + Iterator _iterator; + + @override + T get current => _iterator.current; + + @override + bool moveNext() { + if (!_iterator.moveNext()) { + _iterator = _iterable.iterator; + return _iterator.moveNext(); + } + return true; + } +} diff --git a/guru_app/packages/guru_utils/lib/quiver/iterables/enumerate.dart b/guru_app/packages/guru_utils/lib/quiver/iterables/enumerate.dart new file mode 100644 index 0000000..4a107d5 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/iterables/enumerate.dart @@ -0,0 +1,95 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:collection'; + +/// Returns an [Iterable] of [IndexedValue]s where the nth value holds the nth +/// element of [iterable] and its index. +Iterable> enumerate(Iterable iterable) => + EnumerateIterable(iterable); + +class IndexedValue { + IndexedValue(this.index, this.value); + + final int index; + final V value; + + @override + bool operator ==(other) => + other is IndexedValue && other.index == index && other.value == value; + + @override + int get hashCode => index * 31 + value.hashCode; + + @override + String toString() => '($index, $value)'; +} + +/// An [Iterable] of [IndexedValue]s where the nth value holds the nth +/// element of [iterable] and its index. See [enumerate]. +// This was inspired by MappedIterable internal to Dart collections. +class EnumerateIterable extends IterableBase> { + EnumerateIterable(this._iterable); + + final Iterable _iterable; + + @override + Iterator> get iterator => + EnumerateIterator(_iterable.iterator); + + // Length related functions are independent of the mapping. + @override + int get length => _iterable.length; + + @override + bool get isEmpty => _iterable.isEmpty; + + // Index based lookup can be done before transforming. + @override + IndexedValue get first => IndexedValue(0, _iterable.first); + + @override + IndexedValue get last => IndexedValue(length - 1, _iterable.last); + + @override + IndexedValue get single => IndexedValue(0, _iterable.single); + + @override + IndexedValue elementAt(int index) => + IndexedValue(index, _iterable.elementAt(index)); +} + +/// The [Iterator] returned by [EnumerateIterable.iterator]. +class EnumerateIterator extends Iterator> { + EnumerateIterator(this._iterator); + + final Iterator _iterator; + int _index = 0; + IndexedValue? _current; + + @override + IndexedValue get current { + return _current as IndexedValue; + } + + @override + bool moveNext() { + if (_iterator.moveNext()) { + _current = IndexedValue(_index++, _iterator.current); + return true; + } + _current = null; + return false; + } +} diff --git a/guru_app/packages/guru_utils/lib/quiver/iterables/generating_iterable.dart b/guru_app/packages/guru_utils/lib/quiver/iterables/generating_iterable.dart new file mode 100644 index 0000000..a50e47b --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/iterables/generating_iterable.dart @@ -0,0 +1,80 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:collection'; + +typedef _Initial = T Function(); +typedef _Next = T? Function(T value); + +Iterable generate(initial(), next(o)) => GeneratingIterable(initial, next); + +/// An Iterable whose first value is the result of [initial] and whose +/// subsequent values are generated by passing the current value to the [next] +/// function. +/// +/// The class is useful for creating lazy iterables from object hierarchies and +/// graphs. +/// +/// The initial value and [next] function are required to generate a sequence +/// that eventually terminates, otherwise calling methods that expect a finite +/// sequence, like `length` or `last`, will cause an infinite loop. +/// +/// Example: +/// +/// class Node { +/// Node parent; +/// +/// /// An iterable of node and all ancestors up to the root. +/// Iterable ancestors = +/// GeneratingIterable(() => this, (n) => n.parent); +/// +/// /// An iterable of the root and the path of nodes to this. The +/// /// reverse of ancestors. +/// Iterable path = ancestors.toList().reversed(); +/// } +/// +class GeneratingIterable extends IterableBase { + GeneratingIterable(this.initial, this.next); + + final _Initial initial; + final _Next next; + + @override + Iterator get iterator => _GeneratingIterator(initial(), next); +} + +class _GeneratingIterator implements Iterator { + _GeneratingIterator(this.object, this.next); + + final _Next next; + T? object; + bool started = false; + + @override + T get current { + final cur = started ? object : null; + return cur as T; + } + + @override + bool moveNext() { + if (object == null) return false; + if (started) { + object = next(object!); + } else { + started = true; + } + return object != null; + } +} diff --git a/guru_app/packages/guru_utils/lib/quiver/iterables/infinite_iterable.dart b/guru_app/packages/guru_utils/lib/quiver/iterables/infinite_iterable.dart new file mode 100644 index 0000000..663b95c --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/iterables/infinite_iterable.dart @@ -0,0 +1,65 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:collection'; + +/// A base class for [Iterable]s of infinite length that throws +/// [UnsupportedError] for methods that would require the Iterable to +/// terminate. +abstract class InfiniteIterable extends IterableBase { + @override + bool get isEmpty => false; + + @override + bool get isNotEmpty => true; + + @override + T get last => throw UnsupportedError('last'); + + @override + int get length => throw UnsupportedError('length'); + + @override + T get single => throw StateError('single'); + + @override + bool every(bool test(T element)) => throw UnsupportedError('every'); + + @override + T1 fold(T1 initialValue, T1 combine(T1 previousValue, T element)) => + throw UnsupportedError('fold'); + + @override + void forEach(void f(T element)) => throw UnsupportedError('forEach'); + + @override + String join([String separator = '']) => throw UnsupportedError('join'); + + @override + T lastWhere(bool test(T value), {T orElse()?}) => + throw UnsupportedError('lastWhere'); + + @override + T reduce(T combine(T value, T element)) => throw UnsupportedError('reduce'); + + @override + T singleWhere(bool test(T value), {T orElse()?}) => + throw UnsupportedError('singleWhere'); + + @override + List toList({bool growable = true}) => throw UnsupportedError('toList'); + + @override + Set toSet() => throw UnsupportedError('toSet'); +} diff --git a/guru_app/packages/guru_utils/lib/quiver/iterables/merge.dart b/guru_app/packages/guru_utils/lib/quiver/iterables/merge.dart new file mode 100644 index 0000000..8385ca0 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/iterables/merge.dart @@ -0,0 +1,100 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:collection'; + +/// Returns the result of merging an [Iterable] of [Iterable]s, according to +/// the order specified by the [compare] function. This function assumes the +/// provided iterables are already sorted according to the provided [compare] +/// function. It will not check for this condition or sort the iterables. +/// +/// The compare function must act as a [Comparator]. If [compare] is omitted, +/// [Comparable.compare] is used. +/// +/// If any of the [iterables] contain null elements, an exception will be +/// thrown. +Iterable merge(Iterable> iterables, + [Comparator? compare]) { + if (iterables.isEmpty) return []; + if (iterables.every((i) => i.isEmpty)) return []; + return _Merge(iterables, compare ?? _compareAny); +} + +int _compareAny(T a, T b) { + return Comparable.compare(a as Comparable, b as Comparable); +} + +class _Merge extends IterableBase { + _Merge(this._iterables, this._compare); + + final Iterable> _iterables; + final Comparator _compare; + + @override + Iterator get iterator => _MergeIterator( + _iterables.map((i) => i.iterator).toList(growable: false), _compare); + + @override + String toString() => toList().toString(); +} + +/// Like [Iterator] but one element ahead. +class _IteratorPeeker { + _IteratorPeeker(Iterator iterator) + : _iterator = iterator, + _hasCurrent = iterator.moveNext(); + + final Iterator _iterator; + bool _hasCurrent; + + void moveNext() { + _hasCurrent = _iterator.moveNext(); + } + + T get current => _iterator.current; +} + +class _MergeIterator implements Iterator { + _MergeIterator(List> iterators, this._compare) + : _peekers = iterators.map((i) => _IteratorPeeker(i)).toList(); + + final List<_IteratorPeeker> _peekers; + final Comparator _compare; + T? _current; + + @override + bool moveNext() { + // Pick the peeker that's peeking at the puniest piece + _IteratorPeeker? minIter; + for (final p in _peekers) { + if (p._hasCurrent) { + if (minIter == null || _compare(p.current, minIter.current) < 0) { + minIter = p; + } + } + } + + if (minIter == null) { + return false; + } + _current = minIter.current; + minIter.moveNext(); + return true; + } + + @override + T get current { + return _current as T; + } +} diff --git a/guru_app/packages/guru_utils/lib/quiver/iterables/min_max.dart b/guru_app/packages/guru_utils/lib/quiver/iterables/min_max.dart new file mode 100644 index 0000000..f412101 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/iterables/min_max.dart @@ -0,0 +1,74 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Returns the maximum value in [i], according to the order specified by the +/// [compare] function, or `null` if [i] is empty. +/// +/// The compare function must act as a [Comparator]. If [compare] is omitted, +/// [Comparable.compare] is used. If [i] contains null elements, an exception +/// will be thrown. +T? max(Iterable i, [Comparator? compare]) { + if (i.isEmpty) return null; + final Comparator _compare = compare ?? _compareAny; + return i.reduce((a, b) => _compare(a, b) > 0 ? a : b); +} + +/// Returns the minimum value in [i], according to the order specified by the +/// [compare] function, or `null` if [i] is empty. +/// +/// The compare function must act as a [Comparator]. If [compare] is omitted, +/// [Comparable.compare] is used. If [i] contains null elements, an exception +/// will be thrown. +T? min(Iterable i, [Comparator? compare]) { + if (i.isEmpty) return null; + final Comparator _compare = compare ?? _compareAny; + return i.reduce((a, b) => _compare(a, b) < 0 ? a : b); +} + +/// Returns the minimum and maximum values in [i], according to the order +/// specified by the [compare] function, in an [Extent] instance. Always +/// returns an [Extent], but [Extent.min] and [Extent.max] may be `null` if [i] +/// is empty. +/// +/// The compare function must act as a [Comparator]. If [compare] is omitted, +/// [Comparable.compare] is used. If [i] contains null elements, an exception +/// will be thrown. +/// +/// If [i] is empty, an [Extent] is returned with [:null:] values for [:min:] +/// and [:max:], since there are no valid values for them. +Extent extent(Iterable i, [Comparator? compare]) { + if (i.isEmpty) return Extent(null, null); + final Comparator _compare = compare ?? _compareAny; + var iterator = i.iterator; + var hasNext = iterator.moveNext(); + if (!hasNext) return Extent(null, null); + var max = iterator.current; + var min = iterator.current; + while (iterator.moveNext()) { + if (_compare(max, iterator.current) < 0) max = iterator.current; + if (_compare(min, iterator.current) > 0) min = iterator.current; + } + return Extent(min, max); +} + +class Extent { + Extent(this.min, this.max); + + final T? min; + final T? max; +} + +int _compareAny(T a, T b) { + return Comparable.compare(a as Comparable, b as Comparable); +} diff --git a/guru_app/packages/guru_utils/lib/quiver/iterables/partition.dart b/guru_app/packages/guru_utils/lib/quiver/iterables/partition.dart new file mode 100644 index 0000000..ca8c5e9 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/iterables/partition.dart @@ -0,0 +1,58 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:collection'; + +/// Partitions the input iterable into lists of the specified size. +Iterable> partition(Iterable iterable, int size) { + return iterable.isEmpty ? [] : _Partition(iterable, size); +} + +class _Partition extends IterableBase> { + _Partition(this._iterable, this._size) { + if (_size <= 0) throw ArgumentError(_size); + } + + final Iterable _iterable; + final int _size; + + @override + Iterator> get iterator => + _PartitionIterator(_iterable.iterator, _size); +} + +class _PartitionIterator implements Iterator> { + _PartitionIterator(this._iterator, this._size); + + final Iterator _iterator; + final int _size; + List? _current; + + @override + List get current { + return _current as List; + } + + @override + bool moveNext() { + var newValue = []; + var count = 0; + while (count < _size && _iterator.moveNext()) { + newValue.add(_iterator.current); + count++; + } + _current = (count > 0) ? newValue : null; + return _current != null; + } +} diff --git a/guru_app/packages/guru_utils/lib/quiver/iterables/range.dart b/guru_app/packages/guru_utils/lib/quiver/iterables/range.dart new file mode 100644 index 0000000..37e73a4 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/iterables/range.dart @@ -0,0 +1,43 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Returns an [Iterable] sequence of [num]s. +/// +/// If only one argument is provided, [startOrStop] is the upper bound for the +/// sequence. If two or more arguments are provided, [stop] is the upper bound. +/// +/// The sequence starts at 0 if one argument is provided, or [startOrStop] if +/// two or more arguments are provided. The sequence increments by 1, or [step] +/// if provided. [step] can be negative, in which case the sequence counts down +/// from the starting point and [stop] must be less than the starting point so +/// that it becomes the lower bound. +Iterable range(num startOrStop, [num? stop, num? step]) sync* { + final start = stop == null ? 0 : startOrStop; + stop ??= startOrStop; + step ??= 1; + + if (step == 0) throw ArgumentError('step cannot be 0'); + if (step > 0 && stop < start) { + throw ArgumentError('if step is positive, stop must be greater than start'); + } + if (step < 0 && stop > start) { + throw ArgumentError('if step is negative, stop must be less than start'); + } + + for (num value = start; + step < 0 ? value > stop : value < stop; + value += step) { + yield value; + } +} diff --git a/guru_app/packages/guru_utils/lib/quiver/iterables/zip.dart b/guru_app/packages/guru_utils/lib/quiver/iterables/zip.dart new file mode 100644 index 0000000..f05336c --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/iterables/zip.dart @@ -0,0 +1,25 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Returns an [Iterable] of [List]s where the nth element in the returned +/// iterable contains the nth element from every Iterable in [iterables]. The +/// returned Iterable is as long as the shortest Iterable in the argument. If +/// [iterables] is empty, it returns an empty list. +Iterable> zip(Iterable> iterables) sync* { + if (iterables.isEmpty) return; + final iterators = iterables.map((e) => e.iterator).toList(growable: false); + while (iterators.every((e) => e.moveNext())) { + yield iterators.map((e) => e.current).toList(growable: false); + } +} diff --git a/guru_app/packages/guru_utils/lib/quiver/pattern.dart b/guru_app/packages/guru_utils/lib/quiver/pattern.dart new file mode 100644 index 0000000..3503eae --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/pattern.dart @@ -0,0 +1,142 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// This library contains utilities for working with [RegExp]s and other +/// [Pattern]s. +library quiver.pattern; + +// From the PatternCharacter rule here: +// http://ecma-international.org/ecma-262/5.1/#sec-15.10 +final _specialChars = RegExp(r'([\\\^\$\.\|\+\[\]\(\)\{\}])'); + +/// Escapes special regex characters in [str] so that it can be used as a +/// literal match inside of a [RegExp]. +/// +/// The special characters are: \ ^ $ . | + [ ] ( ) { } +/// as defined here: http://ecma-international.org/ecma-262/5.1/#sec-15.10 +String escapeRegex(String str) => str.splitMapJoin(_specialChars, + onMatch: (Match m) => '\\${m.group(0)}', onNonMatch: (s) => s); + +/// Returns a [Pattern] that matches against every pattern in [include] and +/// returns all the matches. If the input string matches against any pattern in +/// [exclude] no matches are returned. +Pattern matchAny(Iterable include, {Iterable? exclude}) => + _MultiPattern(include, exclude: exclude); + +class _MultiPattern extends Pattern { + _MultiPattern(this.include, {this.exclude}); + + final Iterable include; + final Iterable? exclude; + + @override + Iterable allMatches(String str, [int start = 0]) { + final _allMatches = []; + for (final pattern in include) { + var matches = pattern.allMatches(str, start); + if (_hasMatch(matches)) { + if (exclude != null) { + for (final excludePattern in exclude!) { + if (_hasMatch(excludePattern.allMatches(str, start))) { + return []; + } + } + } + _allMatches.addAll(matches); + } + } + return _allMatches; + } + + @override + Match? matchAsPrefix(String str, [int start = 0]) { + for (final match in allMatches(str)) { + if (match.start == start) { + return match; + } + } + return null; + } +} + +/// Returns true if [pattern] has a single match in [str] that matches the +/// whole string, not a substring. +bool matchesFull(Pattern pattern, String str) { + var match = pattern.matchAsPrefix(str); + return match != null && match.end == str.length; +} + +bool _hasMatch(Iterable matches) => matches.iterator.moveNext(); + +// TODO(justin): add more detailed documentation and explain how matching +// differs or is similar to globs in Python and various shells. +/// A [Pattern] that matches against filesystem path-like strings with +/// wildcards. +/// +/// The pattern matches strings as follows: +/// * The whole string must match, not a substring +/// * Any non wildcard is matched as a literal +/// * '*' matches one or more characters except '/' +/// * '?' matches exactly one character except '/' +/// * '**' matches one or more characters including '/' +class Glob implements Pattern { + Glob(this.pattern) : regex = _regexpFromGlobPattern(pattern); + + final RegExp regex; + final String pattern; + + @override + Iterable allMatches(String str, [int start = 0]) => + regex.allMatches(str, start); + + @override + Match? matchAsPrefix(String string, [int start = 0]) => + regex.matchAsPrefix(string, start); + + bool hasMatch(String str) => regex.hasMatch(str); + + @override + String toString() => pattern; + + @override + int get hashCode => pattern.hashCode; + + @override + bool operator ==(Object other) => other is Glob && pattern == other.pattern; +} + +RegExp _regexpFromGlobPattern(String pattern) { + var sb = StringBuffer(); + sb.write('^'); + var chars = pattern.split(''); + for (var i = 0; i < chars.length; i++) { + var c = chars[i]; + if (_specialChars.hasMatch(c)) { + sb.write('\\$c'); + } else if (c == '*') { + if ((i + 1 < chars.length) && (chars[i + 1] == '*')) { + sb.write('.*'); + i++; + } else { + sb.write('[^/]*'); + } + } else if (c == '?') { + sb.write('[^/]'); + } else { + sb.write(c); + } + } + sb.write(r'$'); + return RegExp(sb.toString()); +} diff --git a/guru_app/packages/guru_utils/lib/quiver/strings.dart b/guru_app/packages/guru_utils/lib/quiver/strings.dart new file mode 100644 index 0000000..6bad8a9 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/strings.dart @@ -0,0 +1,160 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +library quiver.strings; + +/// Returns [true] if [s] is either null, empty or is solely made of whitespace +/// characters (as defined by [String.trim]). +bool isBlank(String? s) => s == null || s.trim().isEmpty; + +/// Returns [true] if [s] is neither null, empty nor is solely made of whitespace +/// characters. +/// +/// See also: +/// +/// * [isBlank] +bool isNotBlank(String? s) => s != null && s.trim().isNotEmpty; + +/// Returns [true] if [s] is either null or empty. +bool isEmpty(String? s) => s == null || s.isEmpty; + +/// Returns [true] if [s] is a not empty string. +bool isNotEmpty(String? s) => s != null && s.isNotEmpty; + +/// Returns a string with characters from the given [s] in reverse order. +/// +/// NOTE: without full support for unicode composed character sequences, +/// sequences including zero-width joiners, etc. this function is unsafe to +/// use. No replacement is provided. +String _reverse(String s) { + if (s == '') return s; + StringBuffer sb = StringBuffer(); + var runes = s.runes.iterator..reset(s.length); + while (runes.movePrevious()) { + sb.writeCharCode(runes.current); + } + return sb.toString(); +} + +/// Loops over [s] and returns traversed characters. Takes arbitrary [from] and +/// [to] indices. Works as a substitute for [String.substring], except it never +/// throws [RangeError]. Supports negative indices. Think of an index as a +/// coordinate in an infinite in both directions vector filled with repeating +/// string [s], whose 0-th coordinate coincides with the 0-th character in [s]. +/// Then [loop] returns the sub-vector defined by the interval ([from], [to]). +/// [from] is inclusive. [to] is exclusive. +/// +/// This method throws exceptions on [null] and empty strings. +/// +/// If [to] is omitted or is [null] the traversing ends at the end of the loop. +/// +/// If [to] < [from], traverses [s] in the opposite direction. +/// +/// For example: +/// +/// loop('Hello, World!', 7) == 'World!' +/// loop('ab', 0, 6) == 'ababab' +/// loop('test.txt', -3) == 'txt' +/// loop('ldwor', -3, 2) == 'world' +String loop(String s, int from, [int? to]) { + if (s.isEmpty) { + throw ArgumentError('Input string cannot be empty'); + } + if (to != null && to < from) { + // TODO(cbracken): throw ArgumentError in this case. + return loop(_reverse(s), -from, -to); + } + int len = s.length; + int leftFrag = from >= 0 ? from ~/ len : ((from - len) ~/ len); + to ??= (leftFrag + 1) * len; + int rightFrag = to - 1 >= 0 ? to ~/ len : ((to - len) ~/ len); + int fragOffset = rightFrag - leftFrag - 1; + if (fragOffset == -1) { + return s.substring(from - leftFrag * len, to - rightFrag * len); + } + StringBuffer sink = StringBuffer(s.substring(from - leftFrag * len)); + _repeat(sink, s, fragOffset); + sink.write(s.substring(0, to - rightFrag * len)); + return sink.toString(); +} + +void _repeat(StringBuffer sink, String s, int times) { + for (int i = 0; i < times; i++) { + sink.write(s); + } +} + +/// Returns `true` if [rune] represents a digit. +/// +/// The definition of digit matches the Unicode `0x3?` range of Western +/// European digits. +bool isDigit(int rune) => rune ^ 0x30 <= 9; + +/// Returns `true` if [rune] represents a whitespace character. +/// +/// The definition of whitespace matches that used in [String.trim] which is +/// based on Unicode 6.2. This maybe be a different set of characters than the +/// environment's [RegExp] definition for whitespace, which is given by the +/// ECMAScript standard: http://ecma-international.org/ecma-262/5.1/#sec-15.10 +bool isWhitespace(int rune) => + (rune >= 0x0009 && rune <= 0x000D) || + rune == 0x0020 || + rune == 0x0085 || + rune == 0x00A0 || + rune == 0x1680 || + rune == 0x180E || + (rune >= 0x2000 && rune <= 0x200A) || + rune == 0x2028 || + rune == 0x2029 || + rune == 0x202F || + rune == 0x205F || + rune == 0x3000 || + rune == 0xFEFF; + +/// Returns a [String] of length [width] padded with the same number of +/// characters on the left and right from [fill]. On the right, characters are +/// selected from [fill] starting at the end so that the last character in +/// [fill] is the last character in the result. [fill] is repeated if +/// necessary to pad. +/// +/// Returns [input] if `input.length` is equal to or greater than width. +/// [input] can be `null` and is treated as an empty string. +/// +/// If there are an odd number of characters to pad, then the right will be +/// padded with one more than the left. +String center(String? input, int width, String fill) { + if (fill.isEmpty) { + throw ArgumentError('fill cannot be empty'); + } + input ??= ''; + if (input.length >= width) return input; + + var padding = width - input.length; + if (padding ~/ 2 > 0) { + input = loop(fill, 0, padding ~/ 2) + input; + } + return input + loop(fill, input.length - width, 0); +} + +/// Returns `true` if [a] and [b] are equal after being converted to lower +/// case, or are both null. +bool equalsIgnoreCase(String? a, String? b) => + (a == null && b == null) || + (a != null && b != null && a.toLowerCase() == b.toLowerCase()); + +/// Compares [a] and [b] after converting to lower case. +/// +/// Both [a] and [b] must not be null. +int compareIgnoreCase(String a, String b) => + a.toLowerCase().compareTo(b.toLowerCase()); diff --git a/guru_app/packages/guru_utils/lib/quiver/time.dart b/guru_app/packages/guru_utils/lib/quiver/time.dart new file mode 100644 index 0000000..5143861 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/time.dart @@ -0,0 +1,19 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +library quiver.time; + +export 'time/clock.dart'; +export 'time/duration_unit_constants.dart'; +export 'time/util.dart'; diff --git a/guru_app/packages/guru_utils/lib/quiver/time/clock.dart b/guru_app/packages/guru_utils/lib/quiver/time/clock.dart new file mode 100644 index 0000000..435a378 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/time/clock.dart @@ -0,0 +1,164 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'util.dart'; + +/// Returns current time. +typedef TimeFunction = DateTime Function(); + +/// Return current system time. +DateTime systemTime() => DateTime.now(); + +/// Provides points in time relative to the current point in time, for example: +/// now, 2 days ago, 4 weeks from now, etc. +/// +/// This class is designed with testability in mind. The current point in time +/// (or [now()]) is defined by a [TimeFunction]. By supplying your own time +/// function or by using fixed clock (see constructors), you can control +/// exactly what time a [Clock] returns and base your test expectations on +/// that. See specific constructors for how to supply time functions. +class Clock { + /// Creates a clock based on the given [timeFunc]. + /// + /// If [timeFunc] is not provided, creates [Clock] based on system clock. + /// + /// Custom [timeFunc] can be useful in unit-tests. For example, you might + /// want to control what time it is now and set date and time expectations in + /// your test cases. + const Clock([TimeFunction timeFunc = systemTime]) : _time = timeFunc; + + /// Creates [Clock] that returns fixed [time] value. Useful in unit-tests. + Clock.fixed(DateTime time) : _time = (() => time); + + final TimeFunction _time; + + /// Returns current time. + DateTime now() => _time(); + + /// Returns the point in time [Duration] amount of time ago. + DateTime agoBy(Duration duration) => now().subtract(duration); + + /// Returns the point in time [Duration] amount of time from now. + DateTime fromNowBy(Duration duration) => now().add(duration); + + /// Returns the point in time that's given amount of time ago. The + /// amount of time is the sum of individual parts. Parts are compatible with + /// ones defined in [Duration]. + DateTime ago( + {int days = 0, + int hours = 0, + int minutes = 0, + int seconds = 0, + int milliseconds = 0, + int microseconds = 0}) => + agoBy(Duration( + days: days, + hours: hours, + minutes: minutes, + seconds: seconds, + milliseconds: milliseconds, + microseconds: microseconds)); + + /// Returns the point in time that's given amount of time from now. The + /// amount of time is the sum of individual parts. Parts are compatible with + /// ones defined in [Duration]. + DateTime fromNow( + {int days = 0, + int hours = 0, + int minutes = 0, + int seconds = 0, + int milliseconds = 0, + int microseconds = 0}) => + fromNowBy(Duration( + days: days, + hours: hours, + minutes: minutes, + seconds: seconds, + milliseconds: milliseconds, + microseconds: microseconds)); + + /// Return the point in time [micros] microseconds ago. + DateTime microsAgo(int micros) => ago(microseconds: micros); + + /// Return the point in time [micros] microseconds from now. + DateTime microsFromNow(int micros) => fromNow(microseconds: micros); + + /// Return the point in time [millis] milliseconds ago. + DateTime millisAgo(int millis) => ago(milliseconds: millis); + + /// Return the point in time [millis] milliseconds from now. + DateTime millisFromNow(int millis) => fromNow(milliseconds: millis); + + /// Return the point in time [seconds] ago. + DateTime secondsAgo(int seconds) => ago(seconds: seconds); + + /// Return the point in time [seconds] from now. + DateTime secondsFromNow(int seconds) => fromNow(seconds: seconds); + + /// Return the point in time [minutes] ago. + DateTime minutesAgo(int minutes) => ago(minutes: minutes); + + /// Return the point in time [minutes] from now. + DateTime minutesFromNow(int minutes) => fromNow(minutes: minutes); + + /// Return the point in time [hours] ago. + DateTime hoursAgo(int hours) => ago(hours: hours); + + /// Return the point in time [hours] from now. + DateTime hoursFromNow(int hours) => fromNow(hours: hours); + + /// Return the point in time [days] ago. + DateTime daysAgo(int days) => ago(days: days); + + /// Return the point in time [days] from now. + DateTime daysFromNow(int days) => fromNow(days: days); + + /// Return the point in time [weeks] ago. + DateTime weeksAgo(int weeks) => ago(days: 7 * weeks); + + /// Return the point in time [weeks] from now. + DateTime weeksFromNow(int weeks) => fromNow(days: 7 * weeks); + + /// Return the point in time [months] ago on the same date. + DateTime monthsAgo(int months) { + var time = now(); + var m = (time.month - months - 1) % 12 + 1; + var y = time.year - (months + 12 - time.month) ~/ 12; + var d = clampDayOfMonth(year: y, month: m, day: time.day); + return DateTime( + y, m, d, time.hour, time.minute, time.second, time.millisecond); + } + + /// Return the point in time [months] from now on the same date. + DateTime monthsFromNow(int months) { + var time = now(); + var m = (time.month + months - 1) % 12 + 1; + var y = time.year + (months + time.month - 1) ~/ 12; + var d = clampDayOfMonth(year: y, month: m, day: time.day); + return DateTime( + y, m, d, time.hour, time.minute, time.second, time.millisecond); + } + + /// Return the point in time [years] ago on the same date. + DateTime yearsAgo(int years) { + var time = now(); + var y = time.year - years; + var d = clampDayOfMonth(year: y, month: time.month, day: time.day); + return DateTime(y, time.month, d, time.hour, time.minute, time.second, + time.millisecond); + } + + /// Return the point in time [years] from now on the same date. + DateTime yearsFromNow(int years) => yearsAgo(-years); +} diff --git a/guru_app/packages/guru_utils/lib/quiver/time/duration_unit_constants.dart b/guru_app/packages/guru_utils/lib/quiver/time/duration_unit_constants.dart new file mode 100644 index 0000000..0cffdd9 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/time/duration_unit_constants.dart @@ -0,0 +1,21 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const Duration aMicrosecond = Duration(microseconds: 1); +const Duration aMillisecond = Duration(milliseconds: 1); +const Duration aSecond = Duration(seconds: 1); +const Duration aMinute = Duration(minutes: 1); +const Duration anHour = Duration(hours: 1); +const Duration aDay = Duration(days: 1); +const Duration aWeek = Duration(days: 7); diff --git a/guru_app/packages/guru_utils/lib/quiver/time/util.dart b/guru_app/packages/guru_utils/lib/quiver/time/util.dart new file mode 100644 index 0000000..739869c --- /dev/null +++ b/guru_app/packages/guru_utils/lib/quiver/time/util.dart @@ -0,0 +1,56 @@ +// Copyright 2017 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Days in a month. This array uses 1-based month numbers, i.e. January is +/// the 1-st element in the array, not the 0-th. +const _daysInMonth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + +/// Returns the number of days in the specified month. +/// +/// This function assumes the use of the Gregorian calendar or the proleptic +/// Gregorian calendar. +int daysInMonth(int year, int month) => + (month == DateTime.february && isLeapYear(year)) ? 29 : _daysInMonth[month]; + +/// Returns true if [year] is a leap year. +/// +/// This implements the Gregorian calendar leap year rules wherein a year is +/// considered to be a leap year if it is divisible by 4, excepting years +/// divisible by 100, but including years divisible by 400. +/// +/// This function assumes the use of the Gregorian calendar or the proleptic +/// Gregorian calendar. +bool isLeapYear(int year) => + (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0)); + +/// Takes a [date] that may be outside the allowed range of dates for a given +/// [month] in a given [year] and returns the closest date that is within the +/// allowed range. +/// +/// For example: +/// +/// February 31, 2013 => February 28, 2013 +/// +/// When jumping from month to month or from leap year to common year we may +/// end up in a month that has fewer days than the month we are jumping from. +/// In that case it is impossible to preserve the exact date. So we "clamp" the +/// date value to fit within the month. For example, jumping from March 31 one +/// month back takes us to February 28 (or 29 during a leap year), as February +/// doesn't have 31-st date. +int clampDayOfMonth({ + required int year, + required int month, + required int day, +}) => + day.clamp(1, daysInMonth(year, month)); diff --git a/guru_app/packages/guru_utils/lib/random/pseudo_random.dart b/guru_app/packages/guru_utils/lib/random/pseudo_random.dart new file mode 100644 index 0000000..b1ffa90 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/random/pseudo_random.dart @@ -0,0 +1,56 @@ +/// Created by Haoyi on 2021/9/14 + +// PseudoRandom类实现了一个伪随机数生成器,可生成整数、布尔值和对列表进行洗牌操作。 +class PseudoRandom { + // 伪随机数生成器的最大值和初始值 + static const _RAND_MAX = 0x7FFFFFFF; + static const _RAND_INIT = 123459876; + + // 用于计算下一个随机数的内部状态变量 + int _next = 1; + + // 伪随机数生成器的种子值 + final int seed; + + // 构造函数,接受可选的种子值,如果没有提供,则使用默认的初始值 + PseudoRandom([int? seed]) + : seed = seed ?? _RAND_INIT, + _next = seed ?? _RAND_INIT; + + // 根据当前状态计算下一个随机整数,返回一个伪随机数 + int _rand() { + if (_next == 0) { + _next = _RAND_INIT; + } + final int quotient = _next ~/ 127773; + final int remainder = _next % 127773; + int t = 16807 * remainder - 2836 * quotient; + if (t <= 0) { + t += _RAND_MAX; + } + _next = t % (_RAND_MAX + 1); + return _next; + } + + // 生成一个伪随机整数,可以指定最大值max(可选参数),如果未提供,则使用默认的最大值 + int nextInt([int? max]) { + return _rand() % (max ?? _RAND_MAX); + } + + // 生成一个伪随机布尔值,返回true或false + bool nextBool() { + return _rand() & 0x01 == 1; + } + + // 使用Fisher-Yates洗牌算法随机打乱列表中的元素 + void shuffle(List list) { + var i = list.length; + while (i > 1) { + int pos = nextInt(i); + i -= 1; + var tmp = list[i]; + list[i] = list[pos]; + list[pos] = tmp; + } + } +} diff --git a/guru_app/packages/guru_utils/lib/random/random_utils.dart b/guru_app/packages/guru_utils/lib/random/random_utils.dart new file mode 100644 index 0000000..3436fc7 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/random/random_utils.dart @@ -0,0 +1,23 @@ +library guru.random; + +import 'dart:math'; +export 'pseudo_random.dart'; + +/// Created by Haoyi on 4/9/21 +/// + +class RandomUtils { + static final Random _random = Random(DateTime.now().microsecondsSinceEpoch); + + static int nextInt(int max) { + return _random.nextInt(max); + } + + static bool nextBool() { + return _random.nextBool(); + } + + static double nextDouble() { + return _random.nextDouble(); + } +} diff --git a/guru_app/packages/guru_utils/lib/remote/remote_config.dart b/guru_app/packages/guru_utils/lib/remote/remote_config.dart new file mode 100644 index 0000000..77b0d52 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/remote/remote_config.dart @@ -0,0 +1,66 @@ +import 'package:guru_utils/extensions/extensions.dart'; + +/// Created by Haoyi on 2023/1/26 + +abstract class IRemoteConfig { + bool? getBool(String name, {bool? defaultValue}); + + String? getString(String name, {String? defaultValue}); + + double? getDouble(String name, {double? defaultValue}); + + int? getInt(String name, {int? defaultValue}); + + Stream observeBool(String name, {bool? defaultValue}); + + Stream observeString(String name, {String? defaultValue}); + + Stream observeDouble(String name, {double? defaultValue}); + + Stream observeInt(String name, {int? defaultValue}); + + IRemoteConfig() { + RemoteConfigUtils._remoteConfigSubject.addEx(this); + } +} + +class RemoteConfigUtils { + static final BehaviorSubject _remoteConfigSubject = BehaviorSubject.seeded(null); + + static Stream get observableRemoteConfig => + _remoteConfigSubject.stream.where((config) => config != null).cast(); + + IRemoteConfig? get _remoteConfig => _remoteConfigSubject.value; + + static RemoteConfigUtils instance = RemoteConfigUtils._(); + + RemoteConfigUtils._(); + + bool? getBool(String name, {bool? defaultValue}) { + return _remoteConfig?.getBool(name, defaultValue: defaultValue); + } + + String? getString(String name, {String? defaultValue}) { + return _remoteConfig?.getString(name, defaultValue: defaultValue); + } + + double? getDouble(String name, {double? defaultValue}) { + return _remoteConfig?.getDouble(name, defaultValue: defaultValue); + } + + int? getInt(String name, {int? defaultValue}) { + return _remoteConfig?.getInt(name, defaultValue: defaultValue); + } + + Stream observeBool(String name, {bool? defaultValue}) => observableRemoteConfig + .flatMap((remoteConfig) => remoteConfig.observeBool(name, defaultValue: defaultValue)); + + Stream observeString(String name, {String? defaultValue}) => observableRemoteConfig + .flatMap((remoteConfig) => remoteConfig.observeString(name, defaultValue: defaultValue)); + + Stream observeDouble(String name, {double? defaultValue}) => observableRemoteConfig + .flatMap((remoteConfig) => remoteConfig.observeDouble(name, defaultValue: defaultValue)); + + Stream observeInt(String name, {int? defaultValue}) => observableRemoteConfig + .flatMap((remoteConfig) => remoteConfig.observeInt(name, defaultValue: defaultValue)); +} diff --git a/guru_app/packages/guru_utils/lib/router/route_center.dart b/guru_app/packages/guru_utils/lib/router/route_center.dart new file mode 100644 index 0000000..107acd8 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/router/route_center.dart @@ -0,0 +1,70 @@ +/// Created by Haoyi on 2020/4/16 +/// +/// +part of "router.dart"; + +class RouteCenter { + static final RouteCenter instance = RouteCenter._(); + + final List _matchers = []; + + RouteCenter._(); + + static void initialize({List routeMatchers = const []}) { + instance._matchers.addAll(routeMatchers); + } + + static Map> convertQueryParamsToRouteParams(Map params) { + return Map.fromEntries( + params.entries.map((entry) => MapEntry(entry.key, [entry.value])).toList()); + } + + Future dispatchUri(Uri uri) { + return openUri(uri); + } + + Future openUri(Uri uri) { + Log.d("openUri: $uri ${uri.path}"); + for (var matcher in _matchers) { + if (matcher.checker(uri)) { + return matcher.dispatcher(uri); + } + } + FeedbackManager.instance.perform(FeedbackOccasion.openPage); + return Get.toNamed(uri.toString()) ?? Future.value(); + } + + Future openPath(String path) { + FeedbackManager.instance.perform(FeedbackOccasion.openPage); + return Get.toNamed(path) ?? Future.value(); + } + + bool canPop() { + return Get.global(null).currentState?.canPop() == true; + } + + void back({dynamic result, AudioEffect? audio}) { + if (canPop()) { + if (audio != null) { + AudioEffector.instance.play(audio); + } else { + FeedbackManager.instance.perform(FeedbackOccasion.backPage); + } + Get.back(result: result); + } + } + + void until({dynamic result, AudioEffect? audio, RoutePredicate? predicate}) { + if (canPop()) { + if (audio != null) { + AudioEffector.instance.play(audio); + } else { + FeedbackManager.instance.perform(FeedbackOccasion.backPage); + } + Get.until(predicate ?? + (route) { + return (Get.isDialogOpen == false) && Get.currentRoute == '/'; + }); + } + } +} diff --git a/guru_app/packages/guru_utils/lib/router/route_matcher.dart b/guru_app/packages/guru_utils/lib/router/route_matcher.dart new file mode 100644 index 0000000..3880b43 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/router/route_matcher.dart @@ -0,0 +1,12 @@ +/// Created by @Haoyi on 2021/8/21 +/// +/// + +part of "router.dart"; + +class RouteMatcher { + final UriChecker checker; + final PathDispatcher dispatcher; + + RouteMatcher({required this.checker, required this.dispatcher}); +} diff --git a/guru_app/packages/guru_utils/lib/router/route_path.dart b/guru_app/packages/guru_utils/lib/router/route_path.dart new file mode 100644 index 0000000..51750ab --- /dev/null +++ b/guru_app/packages/guru_utils/lib/router/route_path.dart @@ -0,0 +1,30 @@ +/// Created by @Haoyi on 2021/8/21 +/// + +part of "./router.dart"; + +class RoutePath { + final String name; + final bool segmentSpecifier; + final RoutePath? parentPath; + + const RoutePath(this.name, {this.segmentSpecifier = false, this.parentPath}); + + String get _path => "${parentPath?.path() ?? ""}$mainPath"; + + String get mainPath => "/${segmentSpecifier ? ":" : ""}$name"; + + String path({String? segmentPath, Map? queryParams}) { + final requestQueryParams = queryParams?.entries.map((e) => "${e.key}=${e.value}").toList() ?? []; + String path = _path; + + if (segmentSpecifier) { + if (segmentPath != null) { + path = path.replaceAll(":$segmentSpecifier", segmentPath); + } else { + assert(false, "The route($path) need to provide segment path parameters!!"); + } + } + return "$path${queryParams?.isNotEmpty == true ? "?${requestQueryParams.join("&")}" : ''}"; + } +} diff --git a/guru_app/packages/guru_utils/lib/router/route_recorder.dart b/guru_app/packages/guru_utils/lib/router/route_recorder.dart new file mode 100644 index 0000000..c06f781 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/router/route_recorder.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:get/get_navigation/src/root/parse_route.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/log/log.dart'; + +class RouteRecorder extends NavigatorObserver { + + static final RouteRecorder instance = RouteRecorder._(); + + RouteRecorder._(); + + final _historySubject = BehaviorSubject>.seeded([]); + + final List _removeManualRoutes = []; + + Route? filter(bool Function(Route) isMatch) { + return _historySubject.value.firstWhereOrNull((element) => isMatch(element)); + } + + void removeRoute(RoutePredicate predicate, [T? result]) { + // 需要新创建一个列表,否则会报错 ConcurrentModifyException + for (var route in List.of(_historySubject.value.where((e) => predicate(e)))) { + if (_removeManualRoutes.contains(route)) { + continue; + } + _removeManualRoutes.add(route); + // TODO didPop error occur + // 在移除route前手动调用此方法去完成Future + // route.didPop(result); + // UI上的移除 + route.navigator?.removeRoute(route); + } + } + + void _checkDispose() { + final history = _historySubject.value; + int beforeLength = history.length; + List.of(_historySubject.value.where((element) => !element.isActive)).forEach((element) { + history.remove(element); + }); + if (history.length < beforeLength) { + Log.d('_checkDispose remove not active route', tag: 'RouteRecorder'); + _historySubject.add(history); + } + } + + @override + void didPush(Route route, Route? previousRoute) { + Log.d('didPush ${route.settings.name}', tag: 'RouteRecorder'); + _checkDispose(); + _historySubject.add(_historySubject.value..add(route)); + } + + @override + void didReplace({Route? newRoute, Route? oldRoute}) { + final history = _historySubject.value; + history.remove(oldRoute); + if (newRoute != null) { + history.add(newRoute); + } + _historySubject.add(history); + } + + @override + void didPop(Route route, Route? previousRoute) { + Log.d('didPop ${route.settings.name}', tag: 'RouteRecorder'); + _checkDispose(); + _historySubject.add(_historySubject.value..remove(route)); + _removeManualRoutes.remove(route); + } + + @override + void didRemove(Route route, Route? previousRoute) { + Log.d('didRemove ${route.settings.name}', tag: 'RouteRecorder'); + _historySubject.add(_historySubject.value..remove(route)); + } +} \ No newline at end of file diff --git a/guru_app/packages/guru_utils/lib/router/route_utils.dart b/guru_app/packages/guru_utils/lib/router/route_utils.dart new file mode 100644 index 0000000..d427505 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/router/route_utils.dart @@ -0,0 +1,39 @@ +/// Created by @Haoyi on 2021/8/21 + +part of "./router.dart"; + +class RouteUtils { + static int _routeId = 0; + + static String _generateRouteId() { + return NumberUtils.eightDigits(_routeId++); + } + + static String get _routeIdQueryParams => "_route_id=${_generateRouteId()}"; + + static String get anonymous => "/${IdUtils.uuidV4()}?$_routeIdQueryParams"; + + static RouteSettings buildAnonymous({ + bool canShowBanner = false, + Map? arguments, + }) { + final Map arg = {"canShowBanner": canShowBanner}; + if (arguments != null && arguments.isNotEmpty) { + arg.addAll(arguments); + } + return RouteSettings(name: IdUtils.uuidV4(), arguments: arg); + } + + static String convertUri(Uri uri) { + return "${uri.path}?${uri.query}&$_routeIdQueryParams"; + } + + static String convertPath(String path) { + return "$path?$_routeIdQueryParams"; + } + + static Map> convertQueryParamsToRouteParams(Map params) { + return Map.fromEntries( + params.entries.map((entry) => MapEntry(entry.key, [entry.value])).toList()); + } +} diff --git a/guru_app/packages/guru_utils/lib/router/router.dart b/guru_app/packages/guru_utils/lib/router/router.dart new file mode 100644 index 0000000..d6b7f71 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/router/router.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:guru_utils/audio/audio_effector.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/feedback/feedback_manager.dart'; +import 'package:guru_utils/id/id_utils.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/number/number_utils.dart'; + + +/// Created by @Haoyi on 2021/8/21 +/// + +part 'route_path.dart'; + +part 'route_utils.dart'; + +part 'route_matcher.dart'; + +part 'route_center.dart'; + +part 'routing_observer.dart'; + +typedef UriChecker = bool Function(Uri); +typedef PathDispatcher = Future Function(Uri); diff --git a/guru_app/packages/guru_utils/lib/router/routing_observer.dart b/guru_app/packages/guru_utils/lib/router/routing_observer.dart new file mode 100644 index 0000000..e88f913 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/router/routing_observer.dart @@ -0,0 +1,17 @@ +part of "router.dart"; + +/// Created by Haoyi on 2022/1/29 + +class RoutingObserver { + static final BehaviorSubject _routing = BehaviorSubject.seeded(Routing()); + + static Stream get observableRouting => _routing.stream; + + static Routing get currentRouting => _routing.value; + + static void listener(Routing? routing) { + if (routing != null) { + _routing.addEx(routing); + } + } +} diff --git a/guru_app/packages/guru_utils/lib/settings/settings.dart b/guru_app/packages/guru_utils/lib/settings/settings.dart new file mode 100644 index 0000000..7924211 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/settings/settings.dart @@ -0,0 +1,40 @@ +import 'package:flutter/foundation.dart'; +import 'package:guru_utils/audio/audio_effector.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/property/app_property.dart'; +import 'package:guru_utils/property/runtime_property.dart'; +import 'package:guru_utils/property/settings/settings_data.dart'; +import 'package:guru_utils/vibration/vibrate_model.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +export 'package:guru_utils/property/settings/settings_data.dart'; + +part 'utils_settings.dart'; + +/// Created by Haoyi on 2022/8/25 + +abstract class Settings with UtilsSettings { + static late Settings _instance; + + static Settings get() => _instance; + + bool _initialized = false; + + Settings() { + _instance = this; + } + + @mustCallSuper + Future refresh() async { + final bundle = await AppProperty.getInstance().loadValuesByUsage(PropertyUsage.setting); + for (var setting in SettingData.settings.values) { + setting.init(bundle); + } + if (!_initialized) { + _syncVersion(); + _initialized = true; + } + return bundle; + } +} diff --git a/guru_app/packages/guru_utils/lib/settings/utils_settings.dart b/guru_app/packages/guru_utils/lib/settings/utils_settings.dart new file mode 100644 index 0000000..7b75043 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/settings/utils_settings.dart @@ -0,0 +1,80 @@ +part of 'settings.dart'; + +/// Created by Haoyi on 2022/8/25 + +class UtilsSettingsKeys { + static const PropertyKey version = PropertyKey.setting("version"); + static const PropertyKey buildNumber = PropertyKey.setting("build_number"); + static const PropertyKey firstInstallTime = PropertyKey.general("first_install_time"); + static const PropertyKey firstInstallVersion = PropertyKey.general("first_install_version"); + static const PropertyKey prevInstallVersion = PropertyKey.general("prev_install_version"); + static const PropertyKey latestInstallVersion = PropertyKey.general("latest_install_version"); + static const PropertyKey previousInstalledVersion = + PropertyKey.general("previous_installed_version"); + static const PropertyKey appInstanceId = PropertyKey.general("app_instance_id"); + static const PropertyKey soundEffect = PropertyKey.setting("sound_effect", tag: "Game"); + static const PropertyKey vibration = PropertyKey.setting("vibration", tag: "Game"); + static const PropertyKey deviceId = PropertyKey.general("device_id"); + static const PropertyKey debugMode = PropertyKey.setting("debug_mode"); + static const PropertyKey latestLtDate = PropertyKey.general("latest_lt_date"); + static const PropertyKey ltDays = PropertyKey.general("lt_days"); + static const PropertyKey keepOnScreenDuration = PropertyKey.setting("keep_on_screen_duration"); +} + +mixin UtilsSettings { + final SettingIntData soundEffect = + SettingIntData(UtilsSettingsKeys.soundEffect, defaultValue: SoundEffect.on); + final SettingIntData vibration = + SettingIntData(UtilsSettingsKeys.vibration, defaultValue: VibrationState.on); + + // 0: off + // > 0: minutes + // < 0: infinite + final SettingIntData keepOnScreenDuration = + SettingIntData(UtilsSettingsKeys.keepOnScreenDuration, defaultValue: 0); + + final SettingBoolData debugMode = + SettingBoolData(UtilsSettingsKeys.debugMode, defaultValue: false); + final SettingStringData version = SettingStringData(UtilsSettingsKeys.version, defaultValue: ""); + final SettingStringData buildNumber = + SettingStringData(UtilsSettingsKeys.buildNumber, defaultValue: ""); + + Future _syncVersion() async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + final currentVersion = packageInfo.version.replaceAll(RegExp(r'-.*'), ''); + final currentBuildName = packageInfo.buildNumber; + Log.d("VERSION: ${version.get()}-${buildNumber.get()} => $currentVersion-$currentBuildName"); + if (currentVersion != version.get()) { + version.set(currentVersion); + } + + if (currentBuildName != buildNumber.get()) { + buildNumber.set(currentBuildName); + } + final versionBuildNumber = "${version.get()}-${buildNumber.get()}"; + + final firstInstallTime = await AppProperty.getInstance() + .getOrCreateInt(UtilsSettingsKeys.firstInstallTime, DateTimeUtils.currentTimeInMillis()); + RuntimeProperty.instance.setInt(UtilsSettingsKeys.firstInstallTime, firstInstallTime); + final firstInstallVersion = await AppProperty.getInstance() + .getOrCreateString(UtilsSettingsKeys.firstInstallVersion, versionBuildNumber); + final latestInstallVersion = await AppProperty.getInstance() + .getOrCreateString(UtilsSettingsKeys.latestInstallVersion, firstInstallVersion); + + if (latestInstallVersion != versionBuildNumber) { + AppProperty.getInstance() + .setString(UtilsSettingsKeys.latestInstallVersion, versionBuildNumber); + AppProperty.getInstance() + .setString(UtilsSettingsKeys.prevInstallVersion, latestInstallVersion); + final previousInstalledVersion = await AppProperty.getInstance() + .getOrCreateString(UtilsSettingsKeys.previousInstalledVersion, firstInstallVersion); + List versionList = previousInstalledVersion.split("|"); + versionList.add(versionBuildNumber); + if (versionList.length > 15) { + versionList = versionList.sublist(versionList.length - 15); + } + AppProperty.getInstance() + .setString(UtilsSettingsKeys.previousInstalledVersion, versionList.join("|")); + } + } +} diff --git a/guru_app/packages/guru_utils/lib/size/size_utils.dart b/guru_app/packages/guru_utils/lib/size/size_utils.dart new file mode 100644 index 0000000..a6f8b89 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/size/size_utils.dart @@ -0,0 +1,22 @@ +/// Created by Haoyi on 2023/4/22 + +class SizeUtils { + static const double _byte = 1; + static const double _kb = 1024; + static const double _mb = 1048576; + static const double _gb = 1073741824; + + static String byte2FitSize(final int byteSize, {int precision = 1}) { + if (byteSize < 0) { + return "0"; + } else if (byteSize < _kb) { + return "${byteSize}B"; + } else if (byteSize < _mb) { + return "${(byteSize / _kb).toStringAsFixed(precision)}KB"; + } else if (byteSize < _gb) { + return "${(byteSize / _mb).toStringAsFixed(precision)}MB"; + } else { + return "${(byteSize / _gb).toStringAsFixed(precision)}GB"; + } + } +} diff --git a/guru_app/packages/guru_utils/lib/svg/svg_parser.dart b/guru_app/packages/guru_utils/lib/svg/svg_parser.dart new file mode 100644 index 0000000..27fa5a3 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/svg/svg_parser.dart @@ -0,0 +1,57 @@ +import 'dart:ui'; +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:path_parsing/path_parsing.dart'; + +/// Created by Haoyi on 2021/7/12 + +class PathWrapper extends PathProxy { + Path _path; + + PathWrapper(this._path); + + Path get path => _path; + + void transform(Matrix4 matrix4) { + _path = _path.transform(matrix4.storage); + } + + @override + void close() { + path.close(); + } + + @override + void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3) { + path.cubicTo(x1, y1, x2, y2, x3, y3); + } + + @override + void lineTo(double x, double y) { + path.lineTo(x, y); + } + + @override + void moveTo(double x, double y) { + path.moveTo(x, y); + } +} + +class SvgParser { + final String svgPath; + final PathFillType fillType; + + Path? _path; + + Path get path { + if (_path == null) { + final p = Path()..fillType = fillType; + final pathWrapper = PathWrapper(p); + writeSvgPathDataToPath(svgPath, pathWrapper); + _path = p; + } + return _path!; + } + + SvgParser.create({required this.svgPath, this.fillType = PathFillType.evenOdd}); +} diff --git a/guru_app/packages/guru_utils/lib/timer/countdown_manager.dart b/guru_app/packages/guru_utils/lib/timer/countdown_manager.dart new file mode 100644 index 0000000..a725ce0 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/timer/countdown_manager.dart @@ -0,0 +1,38 @@ +import 'dart:async'; + +import 'package:guru_utils/log/log.dart'; + +/// Created by Haoyi on 2022/7/31 + +class CountdownManager { + static const _periodDuration = Duration(seconds: 1); + + late StreamController _elapsedTimeStream; + + Stream get observableElapsedTime => _elapsedTimeStream.stream; + + Timer? _timer; + + CountdownManager() { + _elapsedTimeStream = + StreamController.broadcast(onListen: _onFirstListen, onCancel: _onLastCancel); + } + + void _onFirstListen() { + Log.v("_onFirstListen"); + _timer = Timer.periodic(_periodDuration, (timer) { + Log.v("timer: ${timer.isActive}"); + _elapsedTimeStream.add(_periodDuration); + }); + } + + void _onLastCancel() { + Log.v("_onLastCancel"); + _timer?.cancel(); + } + + void dispose() { + _timer?.cancel(); + _elapsedTimeStream.close(); + } +} diff --git a/guru_app/packages/guru_utils/lib/timer/timer.dart b/guru_app/packages/guru_utils/lib/timer/timer.dart new file mode 100644 index 0000000..6a893bb --- /dev/null +++ b/guru_app/packages/guru_utils/lib/timer/timer.dart @@ -0,0 +1,5 @@ +/// Created by @Haoyi on 2022/1/19 +/// +library utils.timer; + +export 'timer_scheduler.dart'; \ No newline at end of file diff --git a/guru_app/packages/guru_utils/lib/timer/timer_scheduler.dart b/guru_app/packages/guru_utils/lib/timer/timer_scheduler.dart new file mode 100644 index 0000000..ae65ee3 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/timer/timer_scheduler.dart @@ -0,0 +1,62 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:ui'; + +import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_utils/id/id_utils.dart'; + +/// Created by Haoyi on 2022/1/19 + +class ActiveTimer { + final String id; + final Duration duration; + final int _start; + final Timer _timer; + final VoidCallback computation; + + ActiveTimer.create(this.duration, this.computation) + : id = IdUtils.uuidV4(), + _start = DateTimeUtils.currentTimeInMillis(), + _timer = Timer(duration, computation); + + ActiveTimer.periodic(this.duration, this.computation) + : id = IdUtils.uuidV4(), + _start = DateTimeUtils.currentTimeInMillis(), + _timer = Timer.periodic(duration, (timer) => computation()); + + int get elapsedTime => DateTimeUtils.currentTimeInMillis() - _start; + + void _dispose() { + _timer.cancel(); + } +} + +class TimerScheduler { + final HashMap timerMap = HashMap(); + + ActiveTimer delayed(Duration duration, VoidCallback callback) { + final timer = ActiveTimer.create(duration, callback); + timerMap[timer.id] = timer; + return timer; + } + + ActiveTimer periodic(Duration duration, VoidCallback callback) { + final timer = ActiveTimer.periodic(duration, callback); + timerMap[timer.id] = timer; + return timer; + } + + void removeTimer(ActiveTimer timer) { + timerMap.remove(timer.id)?._dispose(); + } + + void disposeAll() { + if (timerMap.isNotEmpty) { + final timerList = timerMap.values.toList(); + for (var timer in timerList) { + timer._dispose(); + } + timerMap.clear(); + } + } +} diff --git a/guru_app/packages/guru_utils/lib/tuple/tuple.dart b/guru_app/packages/guru_utils/lib/tuple/tuple.dart new file mode 100644 index 0000000..bc93d94 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/tuple/tuple.dart @@ -0,0 +1,414 @@ + +import 'package:guru_utils/hash/hash.dart'; + +/// Created by Haoyi on 2020/12/18 + +/// Represents a 2-tuple, or pair. +class Tuple2 { + /// Returns the first item of the tuple + final T1 item1; + + /// Returns the second item of the tuple + final T2 item2; + + /// Creates a new tuple value with the specified items. + const Tuple2(this.item1, this.item2); + + /// Create a new tuple value with the specified list [items]. + factory Tuple2.fromList(List items) { + if (items.length != 2) { + throw ArgumentError('items must have length 2'); + } + + return Tuple2(items[0] as T1, items[1] as T2); + } + + /// Returns a tuple with the first item set to the specified value. + Tuple2 withItem1(T1 v) => Tuple2(v, item2); + + /// Returns a tuple with the second item set to the specified value. + Tuple2 withItem2(T2 v) => Tuple2(item1, v); + + /// Creates a [List] containing the items of this [Tuple2]. + /// + /// The elements are in item order. The list is variable-length + /// if [growable] is true. + List toList({bool growable = false}) => List.from([item1, item2], growable: growable); + + @override + String toString() => '[$item1, $item2]'; + + @override + bool operator ==(Object other) => other is Tuple2 && other.item1 == item1 && other.item2 == item2; + + @override + int get hashCode => hash2(item1.hashCode, item2.hashCode); +} + +/// Represents a 3-tuple, or triple. +class Tuple3 { + /// Returns the first item of the tuple + final T1 item1; + + /// Returns the second item of the tuple + final T2 item2; + + /// Returns the third item of the tuple + final T3 item3; + + /// Creates a new tuple value with the specified items. + const Tuple3(this.item1, this.item2, this.item3); + + /// Create a new tuple value with the specified list [items]. + factory Tuple3.fromList(List items) { + if (items.length != 3) { + throw ArgumentError('items must have length 3'); + } + + return Tuple3(items[0] as T1, items[1] as T2, items[2] as T3); + } + + /// Returns a tuple with the first item set to the specified value. + Tuple3 withItem1(T1 v) => Tuple3(v, item2, item3); + + /// Returns a tuple with the second item set to the specified value. + Tuple3 withItem2(T2 v) => Tuple3(item1, v, item3); + + /// Returns a tuple with the third item set to the specified value. + Tuple3 withItem3(T3 v) => Tuple3(item1, item2, v); + + /// Creates a [List] containing the items of this [Tuple3]. + /// + /// The elements are in item order. The list is variable-length + /// if [growable] is true. + List toList({bool growable = false}) => List.from([item1, item2, item3], growable: growable); + + @override + String toString() => '[$item1, $item2, $item3]'; + + @override + bool operator ==(Object other) => + other is Tuple3 && other.item1 == item1 && other.item2 == item2 && other.item3 == item3; + + @override + int get hashCode => hash3(item1.hashCode, item2.hashCode, item3.hashCode); +} + +/// Represents a 4-tuple, or quadruple. +class Tuple4 { + /// Returns the first item of the tuple + final T1 item1; + + /// Returns the second item of the tuple + final T2 item2; + + /// Returns the third item of the tuple + final T3 item3; + + /// Returns the fourth item of the tuple + final T4 item4; + + /// Creates a new tuple value with the specified items. + const Tuple4(this.item1, this.item2, this.item3, this.item4); + + /// Create a new tuple value with the specified list [items]. + factory Tuple4.fromList(List items) { + if (items.length != 4) { + throw ArgumentError('items must have length 4'); + } + + return Tuple4(items[0] as T1, items[1] as T2, items[2] as T3, items[3] as T4); + } + + /// Returns a tuple with the first item set to the specified value. + Tuple4 withItem1(T1 v) => Tuple4(v, item2, item3, item4); + + /// Returns a tuple with the second item set to the specified value. + Tuple4 withItem2(T2 v) => Tuple4(item1, v, item3, item4); + + /// Returns a tuple with the third item set to the specified value. + Tuple4 withItem3(T3 v) => Tuple4(item1, item2, v, item4); + + /// Returns a tuple with the fourth item set to the specified value. + Tuple4 withItem4(T4 v) => Tuple4(item1, item2, item3, v); + + /// Creates a [List] containing the items of this [Tuple4]. + /// + /// The elements are in item order. The list is variable-length + /// if [growable] is true. + List toList({bool growable = false}) => + List.from([item1, item2, item3, item4], growable: growable); + + @override + String toString() => '[$item1, $item2, $item3, $item4]'; + + @override + bool operator ==(Object other) => + other is Tuple4 && + other.item1 == item1 && + other.item2 == item2 && + other.item3 == item3 && + other.item4 == item4; + + @override + int get hashCode => hash4(item1.hashCode, item2.hashCode, item3.hashCode, item4.hashCode); +} + +/// Represents a 5-tuple, or quintuple. +class Tuple5 { + /// Returns the first item of the tuple + final T1 item1; + + /// Returns the second item of the tuple + final T2 item2; + + /// Returns the third item of the tuple + final T3 item3; + + /// Returns the fourth item of the tuple + final T4 item4; + + /// Returns the fifth item of the tuple + final T5 item5; + + /// Creates a new tuple value with the specified items. + const Tuple5(this.item1, this.item2, this.item3, this.item4, this.item5); + + /// Create a new tuple value with the specified list [items]. + factory Tuple5.fromList(List items) { + if (items.length != 5) { + throw ArgumentError('items must have length 5'); + } + + return Tuple5( + items[0] as T1, items[1] as T2, items[2] as T3, items[3] as T4, items[4] as T5); + } + + /// Returns a tuple with the first item set to the specified value. + Tuple5 withItem1(T1 v) => + Tuple5(v, item2, item3, item4, item5); + + /// Returns a tuple with the second item set to the specified value. + Tuple5 withItem2(T2 v) => + Tuple5(item1, v, item3, item4, item5); + + /// Returns a tuple with the third item set to the specified value. + Tuple5 withItem3(T3 v) => + Tuple5(item1, item2, v, item4, item5); + + /// Returns a tuple with the fourth item set to the specified value. + Tuple5 withItem4(T4 v) => + Tuple5(item1, item2, item3, v, item5); + + /// Returns a tuple with the fifth item set to the specified value. + Tuple5 withItem5(T5 v) => + Tuple5(item1, item2, item3, item4, v); + + /// Creates a [List] containing the items of this [Tuple5]. + /// + /// The elements are in item order. The list is variable-length + /// if [growable] is true. + List toList({bool growable = false}) => + List.from([item1, item2, item3, item4, item5], growable: growable); + + @override + String toString() => '[$item1, $item2, $item3, $item4, $item5]'; + + @override + bool operator ==(Object other) => + other is Tuple5 && + other.item1 == item1 && + other.item2 == item2 && + other.item3 == item3 && + other.item4 == item4 && + other.item5 == item5; + + @override + int get hashCode => + hashObjects([item1.hashCode, item2.hashCode, item3.hashCode, item4.hashCode, item5.hashCode]); +} + +/// Represents a 6-tuple, or sextuple. +class Tuple6 { + /// Returns the first item of the tuple + final T1 item1; + + /// Returns the second item of the tuple + final T2 item2; + + /// Returns the third item of the tuple + final T3 item3; + + /// Returns the fourth item of the tuple + final T4 item4; + + /// Returns the fifth item of the tuple + final T5 item5; + + /// Returns the sixth item of the tuple + final T6 item6; + + /// Creates a new tuple value with the specified items. + const Tuple6(this.item1, this.item2, this.item3, this.item4, this.item5, this.item6); + + /// Create a new tuple value with the specified list [items]. + factory Tuple6.fromList(List items) { + if (items.length != 6) { + throw ArgumentError('items must have length 6'); + } + + return Tuple6(items[0] as T1, items[1] as T2, items[2] as T3, + items[3] as T4, items[4] as T5, items[5] as T6); + } + + /// Returns a tuple with the first item set to the specified value. + Tuple6 withItem1(T1 v) => + Tuple6(v, item2, item3, item4, item5, item6); + + /// Returns a tuple with the second item set to the specified value. + Tuple6 withItem2(T2 v) => + Tuple6(item1, v, item3, item4, item5, item6); + + /// Returns a tuple with the third item set to the specified value. + Tuple6 withItem3(T3 v) => + Tuple6(item1, item2, v, item4, item5, item6); + + /// Returns a tuple with the fourth item set to the specified value. + Tuple6 withItem4(T4 v) => + Tuple6(item1, item2, item3, v, item5, item6); + + /// Returns a tuple with the fifth item set to the specified value. + Tuple6 withItem5(T5 v) => + Tuple6(item1, item2, item3, item4, v, item6); + + /// Returns a tuple with the sixth item set to the specified value. + Tuple6 withItem6(T6 v) => + Tuple6(item1, item2, item3, item4, item5, v); + + /// Creates a [List] containing the items of this [Tuple5]. + /// + /// The elements are in item order. The list is variable-length + /// if [growable] is true. + List toList({bool growable = false}) => + List.from([item1, item2, item3, item4, item5, item6], growable: growable); + + @override + String toString() => '[$item1, $item2, $item3, $item4, $item5, $item6]'; + + @override + bool operator ==(Object other) => + other is Tuple6 && + other.item1 == item1 && + other.item2 == item2 && + other.item3 == item3 && + other.item4 == item4 && + other.item5 == item5 && + other.item6 == item6; + + @override + int get hashCode => hashObjects([ + item1.hashCode, + item2.hashCode, + item3.hashCode, + item4.hashCode, + item5.hashCode, + item6.hashCode + ]); +} + +/// Represents a 7-tuple, or septuple. +class Tuple7 { + /// Returns the first item of the tuple + final T1 item1; + + /// Returns the second item of the tuple + final T2 item2; + + /// Returns the third item of the tuple + final T3 item3; + + /// Returns the fourth item of the tuple + final T4 item4; + + /// Returns the fifth item of the tuple + final T5 item5; + + /// Returns the sixth item of the tuple + final T6 item6; + + /// Returns the seventh item of the tuple + final T7 item7; + + /// Creates a new tuple value with the specified items. + const Tuple7(this.item1, this.item2, this.item3, this.item4, this.item5, this.item6, this.item7); + + /// Create a new tuple value with the specified list [items]. + factory Tuple7.fromList(List items) { + if (items.length != 7) { + throw ArgumentError('items must have length 7'); + } + + return Tuple7(items[0] as T1, items[1] as T2, items[2] as T3, + items[3] as T4, items[4] as T5, items[5] as T6, items[6] as T7); + } + + /// Returns a tuple with the first item set to the specified value. + Tuple7 withItem1(T1 v) => + Tuple7(v, item2, item3, item4, item5, item6, item7); + + /// Returns a tuple with the second item set to the specified value. + Tuple7 withItem2(T2 v) => + Tuple7(item1, v, item3, item4, item5, item6, item7); + + /// Returns a tuple with the third item set to the specified value. + Tuple7 withItem3(T3 v) => + Tuple7(item1, item2, v, item4, item5, item6, item7); + + /// Returns a tuple with the fourth item set to the specified value. + Tuple7 withItem4(T4 v) => + Tuple7(item1, item2, item3, v, item5, item6, item7); + + /// Returns a tuple with the fifth item set to the specified value. + Tuple7 withItem5(T5 v) => + Tuple7(item1, item2, item3, item4, v, item6, item7); + + /// Returns a tuple with the sixth item set to the specified value. + Tuple7 withItem6(T6 v) => + Tuple7(item1, item2, item3, item4, item5, v, item7); + + /// Returns a tuple with the seventh item set to the specified value. + Tuple7 withItem7(T7 v) => + Tuple7(item1, item2, item3, item4, item5, item6, v); + + /// Creates a [List] containing the items of this [Tuple5]. + /// + /// The elements are in item order. The list is variable-length + /// if [growable] is true. + List toList({bool growable = false}) => + List.from([item1, item2, item3, item4, item5, item6, item7], growable: growable); + + @override + String toString() => '[$item1, $item2, $item3, $item4, $item5, $item6, $item7]'; + + @override + bool operator ==(Object other) => + other is Tuple7 && + other.item1 == item1 && + other.item2 == item2 && + other.item3 == item3 && + other.item4 == item4 && + other.item5 == item5 && + other.item6 == item6 && + other.item7 == item7; + + @override + int get hashCode => hashObjects([ + item1.hashCode, + item2.hashCode, + item3.hashCode, + item4.hashCode, + item5.hashCode, + item6.hashCode, + item7.hashCode + ]); +} diff --git a/guru_app/packages/guru_utils/lib/uri/uri_utils.dart b/guru_app/packages/guru_utils/lib/uri/uri_utils.dart new file mode 100644 index 0000000..2d07014 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/uri/uri_utils.dart @@ -0,0 +1,65 @@ +import 'package:url_launcher/url_launcher.dart' as _url_launcher; +export 'package:guru_utils/uri/uri_utils.dart'; + +/// Created by Haoyi on 4/30/21 + +enum UriType { assets, file, http, unknown } + +class UriUtils { + static bool isAssetsPath(String path) { + return path[0] == 'a' && + path[1] == 's' && + path[2] == 's' && + path[3] == 'e' && + path[4] == 't' && + path[5] == 's'; + } + + static bool isNetworkPath(String path) { + return path[0] == 'h' && path[1] == 't' && path[2] == 't' && path[3] == 'p'; + } + + static bool isFilePath(String path) { + return path[0] == '/'; + } + + static UriType parseUriType(String path) { + switch (path[0]) { + case 'p': + if (path[1] == 'a' && + path[2] == 'c' && + path[3] == 'k' && + path[4] == 'a' && + path[5] == 'g' && + path[6] == 'e') { + return UriType.assets; + } + break; + case 'a': + if (path[1] == 's' && + path[2] == 's' && + path[3] == 'e' && + path[4] == 't' && + path[5] == 's') { + return UriType.assets; + } + break; + case 'h': + if (path[1] == 't' && path[2] == 't' && path[3] == 'p') { + return UriType.http; + } + break; + case '/': + return UriType.file; + } + return UriType.unknown; + } + + static Future canLaunch(Uri uri) async { + return _url_launcher.canLaunchUrl(uri); + } + + static launchURL(Uri uri) async { + await _url_launcher.launchUrl(uri); + } +} diff --git a/guru_app/packages/guru_utils/lib/vibration/vibrate_model.dart b/guru_app/packages/guru_utils/lib/vibration/vibrate_model.dart new file mode 100644 index 0000000..96e2f01 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/vibration/vibrate_model.dart @@ -0,0 +1,40 @@ +import 'package:json_annotation/json_annotation.dart'; + +/// Created by Haoyi on 2023/1/25 + +part 'vibrate_model.g.dart'; + +class VibrationState { + static const off = 0; + static const on = 1; +} + +@JsonSerializable() +class VibrateEffect { + @JsonKey(name: "pattern", defaultValue: []) + final List pattern; + + @JsonKey(name: "intensities", defaultValue: []) + final List intensities; + + @JsonKey(name: "amplitude", defaultValue: -1) + final int amplitude; + + @JsonKey(name: "amplitude_duration", defaultValue: 500) + final int amplitudeDuration; + + @JsonKey(name: "duration", defaultValue: 500) + final int duration; + + VibrateEffect( + this.pattern, this.intensities, this.amplitude, this.amplitudeDuration, this.duration); + + factory VibrateEffect.fromJson(Map json) => _$VibrateEffectFromJson(json); + + @override + String toString() { + return 'VibrateEffect{pattern: $pattern, intensities: $intensities, amplitude: $amplitude, duration: $duration}'; + } + + Map toJson() => _$VibrateEffectToJson(this); +} diff --git a/guru_app/packages/guru_utils/lib/vibration/vibrate_model.g.dart b/guru_app/packages/guru_utils/lib/vibration/vibrate_model.g.dart new file mode 100644 index 0000000..2a7aed7 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/vibration/vibrate_model.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'vibrate_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +VibrateEffect _$VibrateEffectFromJson(Map json) => + VibrateEffect( + (json['pattern'] as List?)?.map((e) => e as int).toList() ?? [], + (json['intensities'] as List?)?.map((e) => e as int).toList() ?? + [], + json['amplitude'] as int? ?? -1, + json['amplitude_duration'] as int? ?? 500, + json['duration'] as int? ?? 500, + ); + +Map _$VibrateEffectToJson(VibrateEffect instance) => + { + 'pattern': instance.pattern, + 'intensities': instance.intensities, + 'amplitude': instance.amplitude, + 'amplitude_duration': instance.amplitudeDuration, + 'duration': instance.duration, + }; diff --git a/guru_app/packages/guru_utils/lib/vibration/vibrate_utils.dart b/guru_app/packages/guru_utils/lib/vibration/vibrate_utils.dart new file mode 100644 index 0000000..8168c10 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/vibration/vibrate_utils.dart @@ -0,0 +1,51 @@ +import 'dart:io'; + +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/vibration/vibrate_model.dart'; +import 'package:vibration/vibration.dart'; + +/// Created by Haoyi on 2023/1/25 + +class VibrateUtils { + static bool _hasVibrationCapabilities = false; + static bool _hasCustomVibrationsSupport = false; + static bool _hasAmplitudeControl = false; + + static bool _canVibrate = true; + + static Future initialize(int state) async { + _hasVibrationCapabilities = await Vibration.hasVibrator() ?? false; + _hasCustomVibrationsSupport = await Vibration.hasCustomVibrationsSupport() ?? false; + _hasAmplitudeControl = await Vibration.hasAmplitudeControl() ?? false; + Log.w( + "VibrateUtils ensureInitialized! $_hasVibrationCapabilities $_hasCustomVibrationsSupport $_hasAmplitudeControl"); + _canVibrate = state == VibrationState.on; + } + + static void enableVibrate() { + _canVibrate = true; + } + + static void disableVibrate() { + _canVibrate = false; + } + + static void vibrate(VibrateEffect effect) async { + if (_canVibrate && _hasVibrationCapabilities) { + if (_hasCustomVibrationsSupport) { + if (_hasAmplitudeControl) { + Vibration.vibrate( + pattern: effect.pattern, + intensities: effect.intensities, + amplitude: effect.amplitude, + duration: effect.amplitudeDuration); + } else { + Vibration.vibrate( + pattern: effect.pattern, intensities: effect.intensities, duration: effect.duration); + } + } else { + Vibration.vibrate(duration: effect.duration); + } + } + } +} diff --git a/guru_app/packages/guru_utils/lib/visual_feast/engine/action/frame_action.dart b/guru_app/packages/guru_utils/lib/visual_feast/engine/action/frame_action.dart new file mode 100644 index 0000000..a7ff437 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/visual_feast/engine/action/frame_action.dart @@ -0,0 +1,41 @@ +import 'dart:collection'; +import 'dart:ui'; + +/// +/// Created by @Haoyi on 2021/7/18 +/// + +class FrameAction { + final double delayed; + final VoidCallback callback; + double _elapsedInMillis = 0; + + FrameAction(this.callback, {int delayedInMillis = 0}) : delayed = delayedInMillis / 1000.0; + + bool _tick(double t) { + _elapsedInMillis += t; + return _elapsedInMillis >= delayed; + } + + void process() { + callback.call(); + } +} + +class FrameActionQueue { + final DoubleLinkedQueue actions = DoubleLinkedQueue(); + + void dispatchFrameAction(double t) { + while (actions.isNotEmpty) { + final entry = actions.firstEntry(); + if (entry != null && entry.element._tick(t)) { + entry.element.process(); + entry.remove(); + } + } + } + + void deliverFrameAction(FrameAction action) { + actions.addLast(action); + } +} diff --git a/guru_app/packages/guru_utils/lib/visual_feast/engine/layers/layers.dart b/guru_app/packages/guru_utils/lib/visual_feast/engine/layers/layers.dart new file mode 100644 index 0000000..bc28d00 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/visual_feast/engine/layers/layers.dart @@ -0,0 +1,123 @@ +/// +/// Created by @Haoyi on 2021/7/16 +/// +/// +part of '../visual_feast_engine.dart'; + +class Layer { + final String name; + final int priority; + + final _SortingLayers _layers; + + static final Layer invalid = Layer._("", -1, _SortingLayers()); + + Layer._(this.name, this.priority, this._layers); + + @override + int get hashCode => hash2(name, priority); + + void _frameUpdate(double t) { + _layers._frameUpdate(t); + } + + void _render(Canvas canvas) { + _layers._render(canvas); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Layer && + runtimeType == other.runtimeType && + name == other.name && + priority == other.priority; + + bool get isEmpty => _layers.isEmpty; + + void attachRenderElement(RenderElement renderElement) { + _layers.attachRender(renderElement); + } + + void detachRenderElement(RenderElement renderElement) { + _layers.detachRender(renderElement); + } + + void detachAll() { + _layers.detachAll(); + } +} + +class LayerManager { + final List layers; + final Map _layerMap = {}; + + Layer findOrCreate(String name, int priority) { + final hashCode = hash2(name, priority); + final layerKey = _layerMap[hashCode]; + if (layerKey != null) { + return layerKey; + } + final newLayer = Layer._(name, priority, _SortingLayers()); + _layerMap[hashCode] = newLayer; + return newLayer; + } + + // Use Binary Search + int _nearest(int low, int high, Layer layer) { + if (low > high) { + return -low; + } + while (low <= high) { + final int mid = (low + high) >> 1; + if (layer.priority == layers[mid].priority) { + return mid; + } else if (layer.priority < layers[mid].priority) { + return _nearest(low, mid - 1, layer); + } else { + return _nearest(mid + 1, high, layer); + } + } + return -low; + } + + LayerManager() : layers = [Layer.invalid]; + + void attachRenderElement(RenderElement renderElement) { + final layer = findOrCreate(renderElement.layerName, renderElement.layerPriority); + final index = _nearest(1, layers.length - 1, layer); + if (index < 0) { + layers.insert(-index, layer); + } + layer.attachRenderElement(renderElement); + } + + void detachRenderElement(RenderElement renderElement) { + final layer = findOrCreate(renderElement.layerName, renderElement.layerPriority); + layer.detachRenderElement(renderElement); + if (layer.isEmpty) { + final index = _nearest(1, layers.length - 1, layer); + if (index > 0) { + layers.removeAt(index); + } + } + } + + void detachAll() { + for (var layer in layers) { + layer.detachAll(); + } + } + + void frameUpdate(double t) { + for (var layer in layers) { + layer._frameUpdate(t); + } + } + + void render(Canvas canvas) { + for (var layer in layers) { + layer._render(canvas); + } + } +} diff --git a/guru_app/packages/guru_utils/lib/visual_feast/engine/layers/render_layer.dart b/guru_app/packages/guru_utils/lib/visual_feast/engine/layers/render_layer.dart new file mode 100644 index 0000000..d426e46 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/visual_feast/engine/layers/render_layer.dart @@ -0,0 +1,76 @@ +/// +/// Created by @Haoyi on 2021/7/16 +/// + +part of '../visual_feast_engine.dart'; + +class RenderLayer { + final int order; + final RenderQueue pendingRenders = RenderQueue(); + final RenderQueue visibleRenders = RenderQueue(); + final RenderQueue invisibleRenders = RenderQueue(); + + final Map rendersIdMapping = {}; + + RenderLayer(this.order); + + static final RenderLayer invalid = RenderLayer(-1); + + void addRender(RenderElement renderObject) { + pendingRenders.addBack(renderObject); + } + + void removeRender(RenderElement renderObject) { + renderObject._unlink(); + renderObject.dispatchDetach(); + } + + void removeAll() { + while (pendingRenders.isNotEmpty) { + final render = pendingRenders.popFront(); + render?.dispatchDetach(); + } + + while (visibleRenders.isNotEmpty) { + final render = visibleRenders.popFront(); + render?.dispatchDetach(); + } + + while (invisibleRenders.isNotEmpty) { + final render = invisibleRenders.popFront(); + render?.dispatchDetach(); + } + } + + bool get isEmpty => pendingRenders.isEmpty && visibleRenders.isEmpty && invisibleRenders.isEmpty; + + void frameUpdate(double t) { + if (pendingRenders.isNotEmpty) { + // Log.d("##frameUpdate!! ${pendingRenders.isNotEmpty} ${visibleRenders.isNotEmpty}"); + pendingRenders.onStart(); + visibleRenders._addAllToBack(pendingRenders); + pendingRenders.reset(); + } + visibleRenders.propagate((render) { + if (render._visibility == RenderVisibility.visible) { + render.dispatchFrameUpdate(t); + } else { + render._unlink(); + invisibleRenders.addBack(render as RenderElement); + } + }); + + invisibleRenders.propagate((render) { + if (render._visibility == RenderVisibility.invisible) { + render.dispatchFrameUpdate(t); + } else { + render._unlink(); + visibleRenders.addBack(render as RenderElement); + } + }); + } + + void render(Canvas canvas) { + visibleRenders.onRender(canvas); + } +} diff --git a/guru_app/packages/guru_utils/lib/visual_feast/engine/layers/sorting_layer.dart b/guru_app/packages/guru_utils/lib/visual_feast/engine/layers/sorting_layer.dart new file mode 100644 index 0000000..779bc1d --- /dev/null +++ b/guru_app/packages/guru_utils/lib/visual_feast/engine/layers/sorting_layer.dart @@ -0,0 +1,77 @@ +/// +/// Created by @Haoyi on 2021/7/16 +/// + +part of '../visual_feast_engine.dart'; + +class _SortingLayers { + final List renderLayers = [RenderLayer.invalid]; + + // Use Binary Search + int _nearest(int low, int high, RenderElement renderObj) { + if (low > high) { + return -low; + } + while (low <= high) { + final int mid = (low + high) >> 1; + if (renderObj.orderInLayer == renderLayers[mid].order) { + return mid; + } else if (renderObj.orderInLayer < renderLayers[mid].order) { + return _nearest(low, mid - 1, renderObj); + } else { + return _nearest(mid + 1, high, renderObj); + } + } + return -low; + } + + _SortingLayers(); + + bool get isEmpty => renderLayers.isEmpty; + + void _frameUpdate(double t) { + for (var renderLayer in renderLayers) { + renderLayer.frameUpdate(t); + } + } + + void _render(Canvas canvas) { + for (var renderLayer in renderLayers) { + renderLayer.render(canvas); + } + } + + void attachRender(RenderElement renderObj) { + final index = _nearest(1, renderLayers.length - 1, renderObj); + if (index >= 0) { + renderLayers[index].addRender(renderObj); + } else { + final layer = RenderLayer(renderObj.orderInLayer); + layer.addRender(renderObj); + if (renderLayers.isEmpty) { + renderLayers.add(layer); + } else { + renderLayers.insert(-index, layer); + } + } + renderObj.dispatchAttach(); + } + + void detachRender(RenderElement renderObj) { + final index = _nearest(1, renderLayers.length - 1, renderObj); + if (index >= 0) { + renderLayers[index].removeRender(renderObj); + if (renderLayers[index].isEmpty) { + renderLayers.removeAt(index); + } + } + } + + void detachAll() { + for (var layer in renderLayers) { + layer.removeAll(); + } + renderLayers.clear(); + renderLayers.add(RenderLayer.invalid); + } +} diff --git a/guru_app/packages/guru_utils/lib/visual_feast/engine/render/render_element.dart b/guru_app/packages/guru_utils/lib/visual_feast/engine/render/render_element.dart new file mode 100644 index 0000000..ca2762c --- /dev/null +++ b/guru_app/packages/guru_utils/lib/visual_feast/engine/render/render_element.dart @@ -0,0 +1,101 @@ +/// Created by @Haoyi on 4/7/21 +/// +/// +/// + +// visible 可见 +// invisible 不可见 + +part of '../visual_feast_engine.dart'; + +enum RenderVisibility { visible, invisible } + +/// onAttach +/// ↓ +/// onStart +/// ↓ +/// onFrameUpdate +/// ↓ +/// onRender +/// ↓ +/// onDetach + +abstract class Render { + late Render _next; + late Render _prev; + + RenderVisibility _visibility = RenderVisibility.visible; + + RenderVisibility get visibility => _visibility; + + void _initRenderNode() { + _next = this; + _prev = this; + } + + void _unlink() { + _prev._next = _next; + _next._prev = _prev; + } + + void setVisible() { + _visibility = RenderVisibility.visible; + } + + void setInvisible() { + _visibility = RenderVisibility.invisible; + } + + void dispatchAttach() { + onAttach(); + } + + void dispatchDetach() { + onDetach(); + } + + void dispatchStart() { + onStart(); + } + + void dispatchFrameUpdate(double dt) { + onFrameUpdate(dt); + } + + void onAttach() {} + + void onDetach() {} + + void onStart() {} //开始渲染第一帧 + + void onRender(Canvas canvas) {} // 渲染 + + void onFrameUpdate(double dt) {} // 更新 +} + +abstract class RenderElement extends Render { + String get renderId; + + String get layerName; + + int get layerPriority; + + int get orderInLayer; + + @mustCallSuper + RenderElement(); + + bool get isVisible => visibility == RenderVisibility.visible; + + @override + void onStart() {} + + @override + void onAttach() {} + + @mustCallSuper + @override + void onDetach() {} + + void onFrameEvent(dynamic event) {} +} diff --git a/guru_app/packages/guru_utils/lib/visual_feast/engine/render/render_queue.dart b/guru_app/packages/guru_utils/lib/visual_feast/engine/render/render_queue.dart new file mode 100644 index 0000000..566042e --- /dev/null +++ b/guru_app/packages/guru_utils/lib/visual_feast/engine/render/render_queue.dart @@ -0,0 +1,126 @@ +/// +/// Created by @Haoyi on 2021/7/16 +/// + +part of '../visual_feast_engine.dart'; + +class RenderQueue extends Render { + bool get isEmpty => _next == this; + + bool get isNotEmpty => !isEmpty; + + RenderQueue() { + _initRenderNode(); + } + + void reset() { + _next = _prev = this; + } + + void _addAllToBack(RenderQueue queue) { + if (queue.isNotEmpty) { + final tail = queue._prev; + final head = queue._next; + tail._next = this; + head._prev = _prev; + _prev._next = head; + _prev = tail; + } + } + + void _addAllToFront(RenderQueue queue) { + if (queue.isNotEmpty) { + final tail = queue._prev; + final head = queue._next; + tail._next = _next; + head._prev = this; + _next._prev = tail; + _next = head; + } + } + + void addBack(RenderElement _new) { + _new._next = this; + _new._prev = _prev; + _prev._next = _new; + _prev = _new; + } + + void addFront(RenderElement _new) { + _new._prev = this; + _new._next = _next; + _next._prev = _new; + _next = _new; + } + + RenderElement? popFront() { + if (isNotEmpty) { + final node = _next; + node._unlink(); + return node as RenderElement; + } + return null; + } + + RenderElement? removeBack() { + if (isNotEmpty) { + final node = _prev; + node._unlink(); + return node as RenderElement; + } + return null; + } + + @override + void onAttach() { + propagate((render) { + render.onAttach(); + }); + } + + @override + void onDetach() { + propagate((render) { + render.onDetach(); + }); + } + + @override + void onStart() { + propagate((render) { + Log.d("onStart ${(render as RenderElement).renderId}"); + render.onStart(); + }); + } //开始渲染第一帧 + + @override + void onRender(Canvas canvas) { + propagate((render) { + render.onRender(canvas); + }); + } // 渲染 + + @override + void onFrameUpdate(double t) { + propagate((render) { + render.onFrameUpdate(t); + }); + } // 更新 + + void propagate(void Function(Render) callback) { + for (var render = _next, n = render._next; render != this; render = n, n = n._next) { + callback.call(render); + } + } + + T propagateWithResult(T Function(Render) callback, + {required bool Function(T) interrupt, required T defaultValue}) { + for (var renderObj = _next; renderObj != this; renderObj = renderObj._next) { + final result = callback.call(renderObj); + if (interrupt.call(result)) { + return result; + } + } + return defaultValue; + } +} diff --git a/guru_app/packages/guru_utils/lib/visual_feast/engine/render/visual_feast_render.dart b/guru_app/packages/guru_utils/lib/visual_feast/engine/render/visual_feast_render.dart new file mode 100644 index 0000000..b02f727 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/visual_feast/engine/render/visual_feast_render.dart @@ -0,0 +1,126 @@ +/// Created by Haoyi on 2022/7/5 +/// + +part of '../visual_feast_engine.dart'; + +abstract class VisualFeastRender extends RenderElement { + @override + String get layerName => "Default"; + + @override + int get layerPriority => 1; + + @override + int get orderInLayer => 1; + + VisualFeastEngine? _engine; + + VisualFeastEngine get engine => _engine!; + + VisualFeastRender(); + + BaseVisualFeastAnimation? _runningAttachedAnimation; + final DoubleLinkedQueue attachedAnimations = DoubleLinkedQueue(); + + bool shouldUpdateOpacity = true; + double opacity = 1.0; + + void updateOpacity() { + if (shouldUpdateOpacity) { + onOpacityChanged(opacity); + shouldUpdateOpacity = false; + } + } + + void setOpacity(double opacity, {bool update = false}) { + // Log.d("setOpacity: $opacity ${StackTrace.current}"); + this.opacity = opacity; + shouldUpdateOpacity = true; + if (update) { + updateOpacity(); + } + } + + void addSprite(String key, VisualFeastSprite sprite) { + engine.addSprite(key, sprite); + } + + VisualFeastSprite getSprite(String key) { + return engine.getSprite(key); + } + + void attachAnimation(BaseVisualFeastAnimation animation, {bool start = true, bool end = false}) { + if (!animation.isCompleted) { + if (end) { + engine.completer.join(animation.id); + animation.setExtras(VisualFeastExtras([engine.completer])); + } + attachedAnimations.add(animation..start()); + } + } + + void resetAllAnimations() { + _runningAttachedAnimation?.directComplete(); + attachedAnimations.clear(); + } + + void removeAnimation() { + final extras = _runningAttachedAnimation?.extras; + if (extras is VisualFeastExtras) { + extras.complete(_runningAttachedAnimation!.id); + _runningAttachedAnimation?.extras = null; + } + for (var animation in attachedAnimations) { + final extras = animation.extras; + if (extras is VisualFeastExtras) { + extras.complete(animation.id); + animation.extras = null; + } + } + attachedAnimations.clear(); + } + + void updateAnimation(double delta) { + final animation = _runningAttachedAnimation; + if (animation != null && !animation.isCompleted) { + animation.update(delta); + } else { + final extras = animation?.extras; + if (extras is VisualFeastExtras) { + extras.complete(animation!.id); + animation.extras = null; + } + if (attachedAnimations.isNotEmpty) { + final animation = attachedAnimations.removeFirst(); + if (!animation.isCompleted) { + animation.update(delta); + _runningAttachedAnimation = animation; + } + } else { + _runningAttachedAnimation = null; + } + } + } + + @override + void dispatchFrameUpdate(double dt) { + updateAnimation(dt); + updateOpacity(); + onFrameUpdate(dt); + } + + void onOpacityChanged(double opacity) {} + + @override + void onFrameUpdate(double dt) {} + + @override + void onRender(Canvas canvas) {} + + @mustCallSuper + @override + void onDetach() { + resetAllAnimations(); + super.onDetach(); + } +} diff --git a/guru_app/packages/guru_utils/lib/visual_feast/engine/sprite/visual_feast_sprite.dart b/guru_app/packages/guru_utils/lib/visual_feast/engine/sprite/visual_feast_sprite.dart new file mode 100644 index 0000000..809b956 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/visual_feast/engine/sprite/visual_feast_sprite.dart @@ -0,0 +1,34 @@ +/// Created by Haoyi on 2022/7/6 + +part of '../visual_feast_engine.dart'; + +abstract class VisualFeastSprite { + factory VisualFeastSprite.fromImage(ui.Image image) = _ImageVisualFeastSprite._; + + factory VisualFeastSprite.invalid() = _InvalidVisualFeastSprite._; + + void renderRect(Canvas canvas, Rect dst, {Paint? overridePaint}); +} + +class _ImageVisualFeastSprite implements VisualFeastSprite { + final Rect src; + final ui.Image image; + static final Paint defaultCommonPaint = Paint()..filterQuality = FilterQuality.high; + + _ImageVisualFeastSprite._(this.image) + : src = Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()); + + @override + void renderRect(Canvas canvas, Rect dst, {Paint? overridePaint}) { + canvas.drawImageRect(image, src, dst, overridePaint ?? defaultCommonPaint); + } +} + +class _InvalidVisualFeastSprite implements VisualFeastSprite { + _InvalidVisualFeastSprite._(); + + @override + void renderRect(Canvas canvas, Rect dst, {Paint? overridePaint}) { + Log.e("invalid sprite!"); + } +} diff --git a/guru_app/packages/guru_utils/lib/visual_feast/engine/visual_feast_engine.dart b/guru_app/packages/guru_utils/lib/visual_feast/engine/visual_feast_engine.dart new file mode 100644 index 0000000..0711048 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/visual_feast/engine/visual_feast_engine.dart @@ -0,0 +1,241 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/hash/hash.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/visual_feast/engine/action/frame_action.dart'; +import 'package:guru_utils/visual_feast/visual_feast_animation.dart'; +import 'package:guru_utils/visual_feast/visual_feast_completer.dart'; +import 'package:guru_utils/visual_feast/visual_feast_widget.dart'; +import 'package:lottie/lottie.dart'; + +import 'dart:ui' as ui show Image; + +/// Created by Haoyi on 2022/7/5 + +part 'layers/layers.dart'; + +part 'layers/render_layer.dart'; + +part 'layers/sorting_layer.dart'; + +part 'render/render_element.dart'; + +part 'render/render_queue.dart'; + +part 'render/visual_feast_render.dart'; + +part 'sprite/visual_feast_sprite.dart'; + +class VisualFeastDirector extends Director { + final FrameActionQueue frameActionQueue = FrameActionQueue(); + VisualFeastEngine? _runningEngine; + final DoubleLinkedQueue waitingEngine = DoubleLinkedQueue(); + + final DoubleLinkedQueue immediateEngine = DoubleLinkedQueue(); + final DoubleLinkedQueue _tempImmediateEngine = DoubleLinkedQueue(); + + final Map spriteMap = {}; + + final Map lottieMap = {}; + + void addSprite(String key, VisualFeastSprite sprite) { + spriteMap[key] = sprite; + } + + VisualFeastSprite getSprite(String key) { + return spriteMap[key] ?? VisualFeastSprite.invalid(); + } + + void addLottie(String key, LottieDrawable drawable) { + lottieMap[key] = drawable; + } + + LottieDrawable getLottie(String key) { + return lottieMap[key]!; + } + + void deliverFrameAction(VoidCallback callback) { + Log.d("deliverFrameAction isStart:$isStarted"); + if (isStarted) { + frameActionQueue.deliverFrameAction(FrameAction(callback)); + } else { + callback.call(); + } + } + + void dispatchFrameAction(double t) { + frameActionQueue.dispatchFrameAction(t); + } + + void enqueueEngine(VisualFeastEngine engine) { + engine.state = VisualFeastEngineState.waiting; + waitingEngine.add(engine); + Log.d("enqueueEngine: isActive:$isActive isPaused:$isPaused state:${engine.state}"); + if (!isActive) { + start(); + } + if (isPaused) { + resume(); + } + } + + void runEngine(VisualFeastEngine engine) { + engine.state = VisualFeastEngineState.waiting; + immediateEngine.add(engine); + if (!isActive) { + start(); + } + if (isPaused) { + resume(); + } + } + + void updateEngine(double delta) { + _tempImmediateEngine.clear(); + bool immediateRunning = false; + for (var engine in immediateEngine) { + if (!engine.isCompleted) { + engine.state = VisualFeastEngineState.started; + engine.frameUpdate(delta); + _tempImmediateEngine.add(engine); + immediateRunning |= true; + } else { + engine.dispose(); + } + } + immediateEngine.clear(); + immediateEngine.addAll(_tempImmediateEngine); + + final engine = _runningEngine; + if (engine != null) { + if (!engine.isCompleted) { + engine.frameUpdate(delta); + return; + } else { + engine.dispose(); + } + } + + if (waitingEngine.isNotEmpty) { + final newEngine = waitingEngine.removeFirst(); + if (!newEngine.isCompleted) { + newEngine.state = VisualFeastEngineState.started; + newEngine.frameUpdate(delta); + _runningEngine = newEngine; + } + } else { + _runningEngine = null; + if (!immediateRunning) { + pause(); + } + } + } + + @override + void frameUpdate(double delta) { + updateEngine(delta); + dispatchFrameAction(delta); + } + + @override + void render(Canvas canvas) { + for (var engine in immediateEngine) { + engine.render(canvas); + } + _runningEngine?.render(canvas); + } + + @override + void dispose() { + spriteMap.clear(); + super.dispose(); + } + + VisualFeastEngine createEngine({VoidCallback? onCompleted}) { + return VisualFeastEngine._(this, onCompleted: onCompleted); + } +} + +enum VisualFeastEngineState { idle, waiting, started, completed } + +class VisualFeastEngine { + final LayerManager layerMgr = LayerManager(); + dynamic params; + final CompositeSubscription subscriptions = CompositeSubscription(); + late VisualFeastCompleter completer; + final VisualFeastDirector director; + VoidCallback? _onCompleted; + + bool get isCompleted => state == VisualFeastEngineState.completed; + VisualFeastEngineState state = VisualFeastEngineState.idle; + + void addSprite(String key, VisualFeastSprite sprite) { + director.addSprite(key, sprite); + } + + VisualFeastSprite getSprite(String key) { + return director.getSprite(key); + } + + void addLottie(String key, LottieDrawable lottie) { + director.addLottie(key, lottie); + } + + LottieDrawable getLottie(String key) { + return director.getLottie(key); + } + + VisualFeastEngine._(this.director, {VoidCallback? onCompleted}) + : _onCompleted = onCompleted, + super() { + completer = VisualFeastCompleter(onComplete: () { + Log.d("visual feast engine complete! mark!!"); + state = VisualFeastEngineState.completed; + }); + } + + void attachRenders(List renders) { + for (var render in renders) { + render._engine = this; + layerMgr.attachRenderElement(render); + } + } + + void detachRenders(List renders) { + for (var render in renders) { + layerMgr.detachRenderElement(render); + render._engine = null; + } + } + + void addSubscription(StreamSubscription? subscription) { + if (subscription != null) { + subscriptions.add(subscription); + } + } + + void disposeSubscriptions() { + subscriptions.dispose(); + } + + void frameUpdate(double t) { + layerMgr.frameUpdate(t); + } + + void render(Canvas canvas) { + layerMgr.render(canvas); + } + + void onPrepare() {} + + void dispose() { + disposeSubscriptions(); + layerMgr.detachAll(); + _onCompleted?.call(); + _onCompleted = null; + } +} diff --git a/guru_app/packages/guru_utils/lib/visual_feast/keyframe/key_frame.dart b/guru_app/packages/guru_utils/lib/visual_feast/keyframe/key_frame.dart new file mode 100644 index 0000000..bab9daa --- /dev/null +++ b/guru_app/packages/guru_utils/lib/visual_feast/keyframe/key_frame.dart @@ -0,0 +1,60 @@ +import 'dart:math'; + +import 'package:flutter/animation.dart'; +import 'package:guru_utils/number/range.dart'; + +/// Created by Haoyi on 2021/7/15 + +class BezierCurve { + final List anchors; + + static const double _errorBound = 0.001; + + BezierCurve(this.anchors); + + double _evaluate(double fraction) { + double result = 0; + for (int i = 0; i < anchors.length; ++i) { + final coefficient = (i == 0 || i == anchors.length - 1) ? 1 : (anchors.length - 1); + result += + anchors[i] * coefficient * pow(1 - fraction, (anchors.length - i) - 1) * pow(fraction, i); + } + return result; + } + + double transform(double t) { + return _evaluate(t); + } +} + +class KeyFrame { + final Range stop; + final Duration duration; + + int _elapseInMillis = 0; + double _percent = 0.0; + + int get elapseInMillis => _elapseInMillis; + + double get percent => _percent; + + double get relativePercent => _percent * stop.interval.toDouble(); + + KeyFrame(this.stop, this.duration); + + double tick(int timeInTick, bool reversed) { + _elapseInMillis = min(_elapseInMillis + timeInTick, duration.inMilliseconds); + if (duration.inMicroseconds == 0) { + _percent = 1.0; + } else { + if (reversed) { + _percent = 1 - _elapseInMillis.toDouble() / duration.inMilliseconds; + } else { + _percent = _elapseInMillis.toDouble() / duration.inMilliseconds; + } + } + return _percent; + } + + bool isCompleted() => _elapseInMillis >= duration.inMilliseconds; +} diff --git a/guru_app/packages/guru_utils/lib/visual_feast/visual_feast.dart b/guru_app/packages/guru_utils/lib/visual_feast/visual_feast.dart new file mode 100644 index 0000000..0d912c5 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/visual_feast/visual_feast.dart @@ -0,0 +1,6 @@ +/// Created by Haoyi on 2020/6/18 +/// + +double convertRadiusToSigma(double radius) { + return radius * 0.57735 + 0.5; +} diff --git a/guru_app/packages/guru_utils/lib/visual_feast/visual_feast_animation.dart b/guru_app/packages/guru_utils/lib/visual_feast/visual_feast_animation.dart new file mode 100644 index 0000000..4094818 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/visual_feast/visual_feast_animation.dart @@ -0,0 +1,577 @@ +import 'dart:collection'; +import 'dart:math'; + +import 'package:flutter/animation.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/number/range.dart'; + +import 'keyframe/key_frame.dart'; + +/// Created by Haoyi on 2020/6/17 +/// +/// + +const repeat_infinity = -1; + +enum VisualFeastAnimationState { init, paused, waiting, playing, finallyRender, completed } + +typedef AnimationStartedCallback = void Function(); +typedef AnimationUpdateCallback = void Function(double value, double percent, int elapseInMillis); +typedef AnimationAnchorCallback = void Function(double percent, int elapseInMillis); +typedef AnimationCompletedCallback = void Function(); +typedef AnimationWillCompleteCallback = void Function(); +typedef AnimationRepeatCallback = void Function(int repeat); +typedef AnimationNextCallback = void Function(BaseVisualFeastAnimation animation); +typedef AnimationSetUpdateCallback = void Function(double percent, int elapseInMillis); + +abstract class BaseVisualFeastAnimation { + static int _idPools = 1; + VisualFeastAnimationState saveState = VisualFeastAnimationState.init; + VisualFeastAnimationState state = VisualFeastAnimationState.init; + + bool get isCompleted => state == VisualFeastAnimationState.completed; + + bool get isFinallyRender => state == VisualFeastAnimationState.finallyRender; + + bool get isStarted => + state == VisualFeastAnimationState.playing || + state == VisualFeastAnimationState.finallyRender; + + bool get isPaused => state == VisualFeastAnimationState.paused; + + final int id = _idPools++; + + final Map properties = {}; + + dynamic extras; + + final String name; + + BaseVisualFeastAnimation({this.name = ""}); + + void start() { + if (state == VisualFeastAnimationState.init) { + state = VisualFeastAnimationState.waiting; + } + } + + void pause() { + saveState = state; + state = VisualFeastAnimationState.paused; + } + + void resume() { + state = saveState; + } + + void stop() { + state = VisualFeastAnimationState.completed; + } + + void update(double t); + + void setProperty(String key, dynamic value) { + properties[key] = value; + } + + T getProperty(String key) { + return properties[key]; + } + + T getAndRemoveProperty(String key) { + return properties.remove(key); + } + + void setExtras(dynamic value) { + extras = value; + } + + T? getExtras() { + return extras; + } + + T getAndRemoveExtras() { + final T result = extras; + extras = null; + return result; + } + + void directComplete(); +} + +class VisualFeastFramesAnimation extends BaseVisualFeastAnimation { + final int fps; + final int frames; + final int repeat; + + double _elapsed = 0; + double _frame = 0; + int _repeatCount = 0; + AnimationStartedCallback? onStarted; + AnimationUpdateCallback? onUpdate; + AnimationCompletedCallback? onCompleted; + AnimationRepeatCallback? onRepeat; + + VisualFeastFramesAnimation({ + required this.fps, + required this.frames, + required this.repeat, + this.onStarted, + this.onUpdate, + this.onCompleted, + this.onRepeat, + }); + + VisualFeastFramesAnimation copyWith({ + int? fps, + int? frames, + int? repeat, + AnimationStartedCallback? onStarted, + AnimationUpdateCallback? onUpdate, + AnimationCompletedCallback? onCompleted, + AnimationRepeatCallback? onRepeat, + }) { + return VisualFeastFramesAnimation( + fps: fps ?? this.fps, + frames: frames ?? this.frames, + repeat: repeat ?? this.repeat, + onStarted: onStarted ?? this.onStarted, + onUpdate: onUpdate ?? this.onUpdate, + onRepeat: onRepeat ?? this.onRepeat, + onCompleted: onCompleted ?? this.onCompleted); + } + + void reset( + {int repeatCount = -1, + AnimationStartedCallback? onStarted, + AnimationUpdateCallback? onUpdate, + AnimationRepeatCallback? onRepeat, + AnimationCompletedCallback? onCompleted}) { + _frame = 0; + _repeatCount = repeatCount; + _elapsed = 0; + saveState = VisualFeastAnimationState.init; + state = VisualFeastAnimationState.init; + if (onStarted != null) { + this.onStarted = onStarted; + } + if (onUpdate != null) { + this.onUpdate = onUpdate; + } + if (onRepeat != null) { + this.onRepeat = onRepeat; + } + if (onCompleted != null) { + this.onCompleted = onCompleted; + } + } + + @override + void update(double t) { + switch (state) { + case VisualFeastAnimationState.completed: + case VisualFeastAnimationState.paused: + case VisualFeastAnimationState.init: + return; + case VisualFeastAnimationState.waiting: + state = VisualFeastAnimationState.playing; + onStarted?.call(); + break; + default: + break; + } + + _elapsed += fps * t; + + final lastFrame = _frame; + _frame = _elapsed % frames; + if (lastFrame >= _frame) { + // Log.d("repeat: $repeat _repeatCount:$_repeatCount lastFrame:$lastFrame _frame:$_frame"); + + if (repeat < 0 || _repeatCount < repeat) { + _repeatCount++; + state = VisualFeastAnimationState.playing; + onRepeat?.call(_repeatCount); + onUpdate?.call(_frame, _frame / frames, _elapsed.toInt() * 1000); + } else { + _complete(); + } + } else { + onUpdate?.call(_frame, _frame / frames, _elapsed.toInt() * 1000); + } + } + + @override + void directComplete() { + if (state != VisualFeastAnimationState.completed) { + if (_elapsed == 0) { + onStarted?.call(); + } + _complete(); + } + } + + void _complete() { + onUpdate?.call(frames - 1, 1.0, _elapsed.toInt() * 1000); + state = VisualFeastAnimationState.completed; + onCompleted?.call(); + } +} + +class VisualFeastAnimation extends BaseVisualFeastAnimation { + final Duration delayed; + final Duration duration; + Curve curve; + int _elapseInMillis = 0; + int repeat = 0; + int _repeatCount = 0; + bool reverse = false; + double _latestAnchor = 0.0; + final double from; + final double to; + final double anchor; + + AnimationStartedCallback? onStarted; + AnimationUpdateCallback? onUpdate; + AnimationCompletedCallback? onCompleted; + AnimationWillCompleteCallback? onWillComplete; + AnimationRepeatCallback? onRepeat; + AnimationAnchorCallback? onAnchor; + + VisualFeastAnimation( + {this.from = 0.0, + this.to = 1.0, + this.anchor = -1, + required this.duration, + this.repeat = 0, + this.curve = Curves.linear, + this.reverse = false, + this.delayed = const Duration(seconds: 0), + this.onStarted, + this.onUpdate, + this.onAnchor, + this.onWillComplete, + this.onCompleted, + this.onRepeat, + String name = ""}) + : super(name: name); + + VisualFeastAnimation.wait({required this.duration, this.onCompleted}) + : from = 0.0, + to = 1.0, + anchor = -1, + repeat = 0, + curve = Curves.linear, + reverse = false, + delayed = const Duration(seconds: 0), + super(name: ""); + + @override + void directComplete() { + if (state != VisualFeastAnimationState.completed) { + if (state == VisualFeastAnimationState.waiting || _elapseInMillis == 0) { + onStarted?.call(); + } + _complete(); + } + } + + void _complete() { + onUpdate?.call(to, 1.0, duration.inMilliseconds); + state = VisualFeastAnimationState.completed; + onCompleted?.call(); + // Log.d("VisualFeast[$id-$name] onComplete", tag: "VisualFeast"); + } + + @override + void update(double t) { + if (state == VisualFeastAnimationState.completed || + state == VisualFeastAnimationState.paused || + state == VisualFeastAnimationState.init) { + return; + } else if (state == VisualFeastAnimationState.finallyRender) { + if (repeat < 0 || _repeatCount < repeat) { + _repeatCount++; + _elapseInMillis = 0; + state = VisualFeastAnimationState.playing; + onRepeat?.call(_repeatCount); + } else { + _complete(); + } + return; + } + + int timeInTick = (t * 1000).toInt(); + if (state == VisualFeastAnimationState.waiting) { + if (delayed.inMilliseconds > 0) { + _elapseInMillis += timeInTick; + } + timeInTick = _elapseInMillis - delayed.inMilliseconds; + if (timeInTick < 0) { + return; + } + _elapseInMillis = 0; + state = VisualFeastAnimationState.playing; + // Log.d("VisualFeast[$id-$name] onStart", tag: "VisualFeast"); + onStarted?.call(); + } + + _elapseInMillis = min(_elapseInMillis + timeInTick, duration.inMilliseconds); + double percent; + if (duration.inMilliseconds == 0) { + percent = 1.0; + } else { + if (reverse && (_repeatCount & 0x01 == 1)) { + percent = 1 - _elapseInMillis.toDouble() / duration.inMilliseconds; + } else { + percent = _elapseInMillis.toDouble() / duration.inMilliseconds; + } + } + final value = curve.transform(percent); + onUpdate?.call(from + (value * (to - from)), percent, _elapseInMillis); + if (_latestAnchor < anchor && percent >= anchor) { + onAnchor?.call(percent, _elapseInMillis); + _latestAnchor = percent; + } + if (_elapseInMillis >= duration.inMilliseconds) { + state = VisualFeastAnimationState.finallyRender; + onWillComplete?.call(); + } + } +} + +class VisualFeastKeyFrameAnimation extends BaseVisualFeastAnimation { + final Duration delayed; + + final List keyFrames; + int _latestCompletedElapsedInMillis = 0; + double _latestCompletedPercent = 0; + final Curve curve; + int repeat = 0; + int _repeatCount = 0; + bool reverse = false; + final double from; + final double to; + final Object? extra; + AnimationStartedCallback? onStarted; + AnimationUpdateCallback? onUpdate; + AnimationCompletedCallback? onCompleted; + AnimationRepeatCallback? onRepeat; + + int _keyFrameIndex = 0; + + final Duration duration; + + KeyFrame? getCurrentKeyFrame() { + if (_keyFrameIndex < keyFrames.length) { + return keyFrames[_keyFrameIndex]; + } + return null; + } + + void moveNextKeyFrame() { + // TODO: 这里还没有使用reversed时的状态 + _keyFrameIndex = _keyFrameIndex + 1; + } + + VisualFeastKeyFrameAnimation( + {this.from = 0.0, + this.to = 1.0, + required List stops, + required List durations, + this.curve = Curves.linear, + this.repeat = 0, + this.reverse = false, + this.delayed = const Duration(seconds: 0), + this.extra, + this.onStarted, + this.onUpdate, + this.onCompleted, + this.onRepeat, + String name = ""}) + : assert(durations.length == stops.length), + keyFrames = List.generate( + stops.length, + (index) => + KeyFrame(Range(index > 0 ? stops[index - 1] : 0, stops[index]), durations[index]), + growable: false), + duration = durations.reduce((value, element) => value + element), + super(name: name); + + @override + void directComplete() { + if (state != VisualFeastAnimationState.completed) { + if (state != VisualFeastAnimationState.playing) { + onStarted?.call(); + } + _complete(); + } + } + + void _complete() { + onUpdate?.call(to, 1.0, duration.inMilliseconds); + state = VisualFeastAnimationState.completed; + onCompleted?.call(); + Log.d("_complete id:$id"); + } + + @override + void update(double t) { + if (state == VisualFeastAnimationState.completed || + state == VisualFeastAnimationState.paused || + state == VisualFeastAnimationState.init) { + return; + } else if (state == VisualFeastAnimationState.finallyRender) { + if (repeat < 0 || _repeatCount < repeat) { + _repeatCount++; + _latestCompletedElapsedInMillis = 0; + state = VisualFeastAnimationState.playing; + onRepeat?.call(_repeatCount); + } else { + _complete(); + } + return; + } + + int timeInTick = (t * 1000).toInt(); + if (state == VisualFeastAnimationState.waiting) { + if (delayed.inMilliseconds > 0) { + _latestCompletedElapsedInMillis += timeInTick; + } + timeInTick = _latestCompletedElapsedInMillis - delayed.inMilliseconds; + if (timeInTick < 0) { + return; + } + _latestCompletedElapsedInMillis = 0; + state = VisualFeastAnimationState.playing; + onStarted?.call(); + } + final reversed = reverse && (_repeatCount & 0x01 == 1); + final keyFrame = getCurrentKeyFrame(); + if (keyFrame != null) { + keyFrame.tick(timeInTick, reversed); + + final value = curve.transform(_latestCompletedPercent + keyFrame.relativePercent); + double percent = _latestCompletedPercent + keyFrame.relativePercent; + int elapsedInMillis = _latestCompletedElapsedInMillis + keyFrame.elapseInMillis; + if (keyFrame.isCompleted()) { + _latestCompletedElapsedInMillis += keyFrame.duration.inMilliseconds; + _latestCompletedPercent += keyFrame.stop.interval.toDouble(); + percent = _latestCompletedPercent; + elapsedInMillis = _latestCompletedElapsedInMillis; + moveNextKeyFrame(); + } + onUpdate?.call(from + (value * (to - from)), percent, elapsedInMillis); + } else { + state = VisualFeastAnimationState.finallyRender; + } + } +} + +class VisualFeastAnimationSet extends BaseVisualFeastAnimation { + final List animations; + final bool sequentially; + + int current = 0; + + final AnimationStartedCallback? onStarted; + final AnimationCompletedCallback? onCompleted; + final AnimationNextCallback? onNext; + + VisualFeastAnimationSet(this.animations, + {this.sequentially = false, this.onStarted, this.onCompleted, this.onNext, String name = ""}) + : super(name: name); + + VisualFeastAnimationSet.sequentially(this.animations, + {this.onStarted, this.onCompleted, this.onNext, String name = ""}) + : sequentially = true, + super(name: name); + + VisualFeastAnimationSet.together(this.animations, + {this.onStarted, this.onCompleted, this.onNext, String name = ""}) + : sequentially = false, + super(name: name); + + @override + void start() { + if (sequentially) { + animations[current].start(); + } else { + for (var animation in animations) { + animation.start(); + } + } + super.start(); + } + + void _complete() { + state = VisualFeastAnimationState.completed; + onCompleted?.call(); + Log.d("[$id]animation set complete!"); + } + + @override + void directComplete() { + if (state != VisualFeastAnimationState.completed) { + if (state != VisualFeastAnimationState.playing) { + onStarted?.call(); + } + for (var animation in animations) { + animation.directComplete(); + } + onCompleted?.call(); + } + } + + @override + void update(double t) { + if (state == VisualFeastAnimationState.completed || + state == VisualFeastAnimationState.paused || + state == VisualFeastAnimationState.init) { + return; + } + if (sequentially) { + if (current >= animations.length) { + return; + } + final animation = animations[current]; + animation.update(t); + switch (animation.state) { + case VisualFeastAnimationState.completed: + current++; + if (current < animations.length) { + animations[current].start(); + } else { + _complete(); + } + break; + case VisualFeastAnimationState.playing: + if (current == 0) { + onStarted?.call(); + state = VisualFeastAnimationState.playing; + } else { + onNext?.call(animation); + } + break; + default: + break; + } + } else { + bool completed = true; + for (var animation in animations) { + animation.update(t); + if (current == 0 && animation.isStarted) { + onStarted?.call(); + state = VisualFeastAnimationState.playing; + current++; + } + completed &= animation.isCompleted; + } + + if (completed) { + _complete(); + Log.d("[$id]together complete!"); + } + } + } +} diff --git a/guru_app/packages/guru_utils/lib/visual_feast/visual_feast_aware.dart b/guru_app/packages/guru_utils/lib/visual_feast/visual_feast_aware.dart new file mode 100644 index 0000000..df3e6a4 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/visual_feast/visual_feast_aware.dart @@ -0,0 +1,34 @@ +import 'dart:ui'; + +import 'package:guru_utils/controller/base_controller.dart'; +import 'package:guru_utils/visual_feast/engine/visual_feast_engine.dart'; + +/// Created by Haoyi on 2022/7/12 + +mixin VisualFeastAware on BaseController { + final VisualFeastDirector director = VisualFeastDirector(); + + void addSprite(String key, VisualFeastSprite sprite) { + director.addSprite(key, sprite); + } + + VisualFeastSprite getSprite(String key) { + return director.getSprite(key); + } + + void dispatch(VisualFeastEngine engine) { + director.enqueueEngine(engine); + } + + void run(VisualFeastEngine engine) { + director.runEngine(engine); + } + + VisualFeastEngine createEngine({VoidCallback? onCompleted}) { + return director.createEngine(onCompleted: onCompleted); + } + + void disposeDirector() { + director.dispose(); + } +} diff --git a/guru_app/packages/guru_utils/lib/visual_feast/visual_feast_completer.dart b/guru_app/packages/guru_utils/lib/visual_feast/visual_feast_completer.dart new file mode 100644 index 0000000..b3dcae0 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/visual_feast/visual_feast_completer.dart @@ -0,0 +1,60 @@ +import 'dart:ui'; + +import 'package:guru_utils/log/log.dart'; + +/// Created by Haoyi on 2022/3/18 + +class VisualFeastExtras { + final List bindCompleter; + + VisualFeastExtras(this.bindCompleter); + + void complete(int id) { + for (var completer in bindCompleter) { + completer.complete(id); + } + } +} + +class VisualFeastCompleter { + int flag = 0; + + bool get isRunning => flag != 0; + + double idleTick = 0; + + final VoidCallback? onComplete; + + VisualFeastCompleter({this.onComplete}); + + void join(int id) { + flag ^= id; + Log.d("join:$id $flag"); + idleTick = 0; + } + + void complete(int id) { + flag ^= id; + Log.d("complete:$id $flag"); + if (flag == 0) { + idleTick = 0; + onComplete?.call(); + } + } + + void tick(double dt) { + if (flag != 0) { + idleTick += dt; + if (idleTick > 8) { + idleTick = 0; + reset(); + onComplete?.call(); + } + } + } + + void reset() { + flag = 0; + Log.e("reset: $flag", tag: "VF"); + } +} diff --git a/guru_app/packages/guru_utils/lib/visual_feast/visual_feast_looper.dart b/guru_app/packages/guru_utils/lib/visual_feast/visual_feast_looper.dart new file mode 100644 index 0000000..016e80a --- /dev/null +++ b/guru_app/packages/guru_utils/lib/visual_feast/visual_feast_looper.dart @@ -0,0 +1,59 @@ +import 'package:flutter/scheduler.dart'; + +/// Created by Haoyi on 2020/6/17 +/// +class VisualFeastLooper { + final Function callback; + Duration previous = Duration.zero; + late Ticker _ticker; + + VisualFeastLooper(this.callback) { + _ticker = Ticker(_tick); + } + + void _tick(Duration timestamp) { + final double dt = _computeDeltaT(timestamp); + callback(dt); + } + + double _computeDeltaT(Duration now) { + Duration delta = now - previous; + if (previous == Duration.zero) { + delta = Duration.zero; + } + previous = now; + + return delta.inMicroseconds / Duration.microsecondsPerSecond; + } + + bool get isActive => _ticker.isActive; + + bool get isResumed => !_ticker.muted; + + bool get isPaused => _ticker.muted; + + bool get isStarted => isActive && isResumed; + + void start() { + if (!isActive) { + _ticker.start(); + } + print("VisualFeastLooper started!"); + } + + void stop() { + _ticker.stop(); + print("VisualFeastLooper stopped!"); + } + + void pause() { + _ticker.muted = true; + previous = Duration.zero; + print("VisualFeastLooper paused!"); + } + + void resume() { + _ticker.muted = false; + print("VisualFeastLooper resumed!"); + } +} diff --git a/guru_app/packages/guru_utils/lib/visual_feast/visual_feast_observer.dart b/guru_app/packages/guru_utils/lib/visual_feast/visual_feast_observer.dart new file mode 100644 index 0000000..bcd023e --- /dev/null +++ b/guru_app/packages/guru_utils/lib/visual_feast/visual_feast_observer.dart @@ -0,0 +1,53 @@ +import 'dart:collection'; +import 'dart:ui'; + +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/visual_feast/visual_feast_animation.dart'; + + +/// Created by Haoyi on 2022/2/5 + +class VisualFeastObserver { + final bool companion; + final DoubleLinkedQueue animations = DoubleLinkedQueue(); + final List checkAnimations = []; + final List tempAnimations = []; + final VoidCallback? onIdle; + + bool lastIsRunning = false; + bool isRunning = false; + + VisualFeastObserver({this.onIdle}) : companion = false; + + VisualFeastObserver.companion({this.onIdle}) : companion = true; + + void add(BaseVisualFeastAnimation animation) { + animations.add(animation); + } + + bool update() { + tempAnimations.clear(); + for (var animation in checkAnimations) { + if (!animation.isCompleted) { + tempAnimations.add(animation); + } + } + while (animations.isNotEmpty) { + final animation = animations.removeFirst(); + if (!animation.isCompleted) { + tempAnimations.add(animation); + } + } + checkAnimations.clear(); + checkAnimations.addAll(tempAnimations); + lastIsRunning = isRunning; + isRunning = checkAnimations.isNotEmpty; + if (lastIsRunning != isRunning) { + Log.e("hasRunningAnimation $isRunning"); + if (!isRunning) { + onIdle?.call(); + } + } + return isRunning; + } +} diff --git a/guru_app/packages/guru_utils/lib/visual_feast/visual_feast_sink.dart b/guru_app/packages/guru_utils/lib/visual_feast/visual_feast_sink.dart new file mode 100644 index 0000000..69bbd5e --- /dev/null +++ b/guru_app/packages/guru_utils/lib/visual_feast/visual_feast_sink.dart @@ -0,0 +1,40 @@ +import 'package:flutter/cupertino.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/visual_feast/visual_feast_animation.dart'; + +/// Created by Haoyi on 2022/3/17 + +class VisualFeastSink { + bool lastIsRunning = false; + bool isRunning = false; + + VoidCallback? onIdle; + + final List animations = []; + + VisualFeastSink({this.onIdle}); + + void join(BaseVisualFeastAnimation animation) { + animations.add(animation); + } + + void complete() { + final List _temp = []; + for (var animation in animations) { + if (!animation.isCompleted) { + _temp.add(animation); + } + } + animations.clear(); + animations.addAll(_temp); + + lastIsRunning = isRunning; + isRunning = animations.isNotEmpty; + if (lastIsRunning != isRunning) { + Log.e("hasRunningAnimation $isRunning"); + if (!isRunning) { + onIdle?.call(); + } + } + } +} diff --git a/guru_app/packages/guru_utils/lib/visual_feast/visual_feast_widget.dart b/guru_app/packages/guru_utils/lib/visual_feast/visual_feast_widget.dart new file mode 100644 index 0000000..87fcd96 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/visual_feast/visual_feast_widget.dart @@ -0,0 +1,195 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/visual_feast/visual_feast_looper.dart'; + +/** + * Created by Haoyi on 2022/7/5. + */ + +abstract class Director { + VisualFeastLooper? _looper; + + bool get isActive => _looper?.isActive == true; + + bool get isResumed => _looper?.isResumed == true; + + bool get isPaused => _looper?.isPaused == true; + + bool get isStarted => _looper?.isStarted == true; + + void _bind(VisualFeastLooper looper) { + _looper = looper; + } + + void _unbind() { + _looper = null; + } + + @mustCallSuper + void start() { + Log.d("looper start!"); + _looper?.start(); + } + + @mustCallSuper + void pause() { + _looper?.pause(); + } + + @mustCallSuper + void resume() { + _looper?.resume(); + } + + @mustCallSuper + void stop() { + _looper?.stop(); + } + + void resize(Size size) {} + + Future prepare() async {} + + void frameUpdate(double delta) {} + + void render(Canvas canvas) {} + + void dispose() { + stop(); + _unbind(); + } +} + +class _VisualFeastRenderBox extends RenderBox with WidgetsBindingObserver { + final BuildContext context; + + final Director director; + final bool autoStart; + + _VisualFeastRenderBox(this.context, this.director, {this.autoStart = false}) { + director._bind(VisualFeastLooper(_looperCallback)); + } + + @override + bool get sizedByParent => true; + + @override + Size computeDryLayout(BoxConstraints constraints) => constraints.biggest; + + @override + void performResize() { + super.performResize(); + director.resize(constraints.biggest); + } + + void start() { + Log.d("looper start!"); + director.start(); + } + + void pause() { + director.pause(); + } + + void resume() { + director.resume(); + } + + void stop() { + director.stop(); + } + + @override + void attach(PipelineOwner owner) { + super.attach(owner); + Log.d("_VisualFeastRenderBox attach $autoStart"); + director.prepare(); + + // if (autoStart) { + // start(); + // } + + Log.d("####### attach"); + _bindLifecycleListener(); + } + + @override + void detach() { + _unbindLifecycleListener(); + director.dispose(); + Log.d("####### detach"); + super.detach(); + } + + void _looperCallback(double dt) { + if (!attached) { + return; + } + // Log.d("####### frameUpdate"); + director.frameUpdate(dt); + markNeedsPaint(); + } + + @override + void paint(PaintingContext context, Offset offset) { + context.canvas.save(); + context.canvas.translate(offset.dx, offset.dy); + director.render(context.canvas); + context.canvas.restore(); + } + + void _bindLifecycleListener() { + WidgetsBinding.instance?.addObserver(this); + } + + void _unbindLifecycleListener() { + WidgetsBinding.instance?.removeObserver(this); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) {} +} + +class _VisualFeast extends LeafRenderObjectWidget { + final Size size; + final Director director; + + const _VisualFeast({required this.size, required this.director}); + + void dispose() { + director.dispose(); + } + + @override + RenderBox createRenderObject(BuildContext context) { + return RenderConstrainedBox( + child: _VisualFeastRenderBox(context, director), + additionalConstraints: BoxConstraints.expand(width: size.width, height: size.height)); + } +// +// @override +// void updateRenderObject(BuildContext context, RenderConstrainedBox renderBox) { +// renderBox +// ..child = _VisualFeastRenderBox(context, director) +// ..additionalConstraints = BoxConstraints.expand(width: size.width, height: size.height); +// } +} + +class VisualFeastWidget extends StatelessWidget { + final Director director; + + const VisualFeastWidget({Key? key, required this.director}) : super(key: key); + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (_, BoxConstraints constraints) { + director.resize(constraints.biggest); + return Stack( + children: [ + _VisualFeast(size: constraints.biggest, director: director), + ], + ); + }); + } +} diff --git a/guru_app/packages/guru_utils/lib/widget/widget_utils.dart b/guru_app/packages/guru_utils/lib/widget/widget_utils.dart new file mode 100644 index 0000000..b399de7 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/widget/widget_utils.dart @@ -0,0 +1,286 @@ +/// Created by Haoyi on 2022/8/25 +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +/// Created by Haoyi on 4/15/21 + +class TextMetrics { + final double width; + final double height; + final int? maxLines; + final bool didExceedMaxLines; + final double preferredLineHeight; + + TextMetrics( + {required this.width, + required this.height, + required this.maxLines, + required this.didExceedMaxLines, + required this.preferredLineHeight}); + + @override + String toString() { + return 'TextMetrics{width: $width, height: $height, maxLines: $maxLines, didExceedMaxLines: $didExceedMaxLines, preferredLineHeight: $preferredLineHeight}'; + } +} + +class WidgetUtils { + static Size calculateTextSize(String text, TextStyle style, + {double? maxWidth, StrutStyle? strutStyle, int maxLine = 1}) { + final constraints = BoxConstraints( + maxWidth: maxWidth ?? double.infinity, + minHeight: 0.0, + minWidth: maxWidth ?? 0.0, + ); + + final renderParagraph = RenderParagraph( + TextSpan( + text: text, + style: style, + ), + maxLines: maxLine, + strutStyle: strutStyle ?? StrutStyle.fromTextStyle(style), + textDirection: TextDirection.ltr) + ..layout(constraints); + return Size(renderParagraph.getMinIntrinsicWidth(style.fontSize ?? 14.0).ceilToDouble(), + renderParagraph.getMinIntrinsicHeight(style.fontSize ?? 14.0).ceilToDouble()); + } + + static TextMetrics measureText(String text, TextStyle style, + {double? maxWidth, StrutStyle? strutStyle, int maxLines = 1}) { + TextPainter painter = TextPainter( + maxLines: maxLines, + textDirection: TextDirection.ltr, + strutStyle: strutStyle ?? StrutStyle.fromTextStyle(style), + text: TextSpan(text: text, style: style)); + painter.layout(maxWidth: maxWidth ?? double.infinity); + return TextMetrics( + width: painter.width, + height: painter.height, + maxLines: maxLines, + didExceedMaxLines: painter.didExceedMaxLines, + preferredLineHeight: painter.preferredLineHeight); + } + + static Size boundingTextSize(BuildContext context, String text, TextStyle style, + {int maxLines = 2 ^ 31, double maxWidth = double.infinity, StrutStyle? strutStyle}) { + if (text.isEmpty) { + return Size.zero; + } + final TextPainter textPainter = TextPainter( + textDirection: TextDirection.ltr, + locale: Localizations.localeOf( + context, + ), + text: TextSpan(text: text, style: style), + strutStyle: strutStyle, + maxLines: maxLines) + ..layout(maxWidth: maxWidth); + return textPainter.size; + } + + static int calculateMaxLines(BuildContext context, String text, TextStyle style, double maxWidth, + {TextAlign? textAlign, TextDirection? textDirection, StrutStyle? strutStyle}) { + int lines = 1; + late TextPainter textPainter; + do { + textPainter = TextPainter( + textAlign: textAlign ?? TextAlign.left, + textDirection: textDirection ?? TextDirection.ltr, + locale: Localizations.localeOf( + context, + ), + text: TextSpan(text: text, style: style), + strutStyle: strutStyle, + maxLines: lines); + textPainter.layout(maxWidth: maxWidth); + if ((textPainter.didExceedMaxLines || textPainter.width > maxWidth)) { + lines++; + } + } while ((textPainter.didExceedMaxLines || textPainter.width > maxWidth)); + return lines; + } + + static String breakWord(String word) { + if (word.isEmpty) { + return word; + } + String breakWord = ' '; + for (var element in word.runes) { + breakWord += String.fromCharCode(element); + breakWord += '\u200B'; + } + return breakWord; + } + + static Rect getWidgetBoundary(GlobalKey key, {Offset offset = Offset.zero}) { + RenderObject? widgetRenderBox = key.currentContext?.findRenderObject(); + if (widgetRenderBox != null) { + Offset widgetOffset = (widgetRenderBox as RenderBox).localToGlobal(Offset.zero); + Size size = widgetRenderBox.size; + final widgetCenter = + Rect.fromLTWH(widgetOffset.dx, widgetOffset.dy, size.width, size.height).center; + return Rect.fromCenter( + center: Offset(widgetCenter.dx + offset.dx, widgetCenter.dy + offset.dy), + width: size.width, + height: size.height); + } + return Rect.zero; + } + + static List calculateFontSize(BuildContext context, String text, BoxConstraints size, + {TextSpan? textSpan, + TextStyle? textStyle, + int? maxLines = 1, + double? textScaleFactor, + List? presetFontSizeList, + double minFontSize = 12, + double maxFontSize = double.infinity, + double stepGranularity = 1, + bool wrapWords = true, + TextAlign? textAlign, + TextDirection? textDirection, + Locale? locale, + StrutStyle? strutStyle}) { + final span = TextSpan( + style: textSpan?.style ?? textStyle, + text: textSpan?.text ?? text, + children: textSpan?.children, + recognizer: textSpan?.recognizer, + ); + + final userScale = textScaleFactor ?? MediaQuery.textScaleFactorOf(context); + + int left; + int right; + final defaultTextStyle = DefaultTextStyle.of(context); + + var style = textStyle; + if (textStyle == null || textStyle.inherit) { + style = defaultTextStyle.style.merge(textStyle); + } + if (style!.fontSize == null) { + style = style.copyWith(fontSize: 14); + } + + final presetFontSizes = presetFontSizeList?.reversed.toList(); + if (presetFontSizes == null) { + final num defaultFontSize = style.fontSize!.clamp(minFontSize, maxFontSize); + final defaultScale = defaultFontSize * userScale / style.fontSize!; + if (checkTextFits(span, defaultScale, maxLines, size, + wrapWords: wrapWords, + textAlign: textAlign, + textDirection: textDirection, + locale: locale, + strutStyle: strutStyle)) { + return [defaultFontSize * userScale, true]; + } + + left = (minFontSize / stepGranularity).floor(); + right = (defaultFontSize / stepGranularity).ceil(); + } else { + left = 0; + right = presetFontSizes.length - 1; + } + + var lastValueFits = false; + while (left <= right) { + final mid = (left + (right - left) / 2).floor(); + double scale; + if (presetFontSizes == null) { + scale = mid * userScale * stepGranularity / style.fontSize!; + } else { + scale = presetFontSizes[mid] * userScale / style.fontSize!; + } + if (checkTextFits(span, scale, maxLines, size, + wrapWords: wrapWords, + textAlign: textAlign, + textDirection: textDirection, + locale: locale, + strutStyle: strutStyle)) { + left = mid + 1; + lastValueFits = true; + } else { + right = mid - 1; + } + } + + if (!lastValueFits) { + right += 1; + } + + double fontSize; + if (presetFontSizes == null) { + fontSize = right * userScale * stepGranularity; + } else { + fontSize = presetFontSizes[right] * userScale; + } + + return [fontSize, lastValueFits]; + } + + static bool checkTextFits(TextSpan text, double scale, int? maxLines, BoxConstraints constraints, + {bool wrapWords = true, + TextAlign? textAlign, + TextDirection? textDirection, + Locale? locale, + StrutStyle? strutStyle}) { + if (!wrapWords) { + final words = text.toPlainText().split(RegExp('\\s+')); + + final wordWrapTextPainter = TextPainter( + text: TextSpan( + style: text.style, + text: words.join('\n'), + ), + textAlign: textAlign ?? TextAlign.left, + textDirection: textDirection ?? TextDirection.ltr, + textScaleFactor: scale, + maxLines: words.length, + locale: locale, + strutStyle: strutStyle, + ); + + wordWrapTextPainter.layout(maxWidth: constraints.maxWidth); + + if (wordWrapTextPainter.didExceedMaxLines || + wordWrapTextPainter.width > constraints.maxWidth) { + return false; + } + } + + final textPainter = TextPainter( + text: text, + textAlign: textAlign ?? TextAlign.left, + textDirection: textDirection ?? TextDirection.ltr, + textScaleFactor: scale, + maxLines: maxLines, + locale: locale, + strutStyle: strutStyle, + ); + + textPainter.layout(maxWidth: constraints.maxWidth); + + return !(textPainter.didExceedMaxLines || + textPainter.height > constraints.maxHeight || + textPainter.width > constraints.maxWidth); + } + + static List separate(List widgets, Widget? separator) { + if (separator == null) { + return widgets; + } + final List separatedList = []; + + for (int i = 0; i < widgets.length; i++) { + separatedList.add(widgets[i]); + + if (i != widgets.length - 1) { + separatedList.add(separator); + } + } + + return separatedList; + } +} diff --git a/guru_app/packages/guru_utils/pubspec.yaml b/guru_app/packages/guru_utils/pubspec.yaml new file mode 100644 index 0000000..a0b2897 --- /dev/null +++ b/guru_app/packages/guru_utils/pubspec.yaml @@ -0,0 +1,118 @@ +name: guru_utils +description: A new Flutter project. +version: 3.0.0 +homepage: + +environment: + sdk: ">=2.19.0 <4.0.0" + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + + get: 4.6.5 + logger: 2.0.2+1 + uuid: 4.1.0 + intl: 0.18.1 + rxdart: 0.27.7 + lottie: 2.7.0 + path_parsing: ^1.0.1 + dartx: 1.2.0 + + device_info_plus: 9.1.1 + android_id: 0.3.6 + device_apps: ^2.2.0 + package_info_plus: 5.0.1 + system_clock: 2.0.0 + + sqflite: 2.3.0 + path_provider: 2.1.1 + archive: 3.4.9 + connectivity_plus: 5.0.2 + http: 1.1.2 + + url_launcher: 6.2.2 + + permission_handler: 11.1.0 + + image: 4.1.3 + + persistent: + git: + url: git@git.chengdu.pundit.company:castbox/guru_sdk.git + path: guru_app/plugins/persistent + ref: v3.0.0 + + soundpool: + git: + url: git@git.chengdu.pundit.company:castbox/guru_sdk.git + path: guru_app/plugins/soundpool + ref: v3.0.0 + + vibration: + git: + url: git@git.chengdu.pundit.company:castbox/guru_sdk.git + path: guru_app/plugins/vibration + ref: v3.0.0 + + guru_applifecycle_flutter: + git: + url: git@git.chengdu.pundit.company:castbox/guru_sdk.git + path: guru_app/plugins/guru_applifecycle_flutter + ref: v3.0.0 + + + + guru_platform_data: + git: + url: git@git.chengdu.pundit.company:castbox/guru_sdk.git + path: guru_app/plugins/guru_platform_data + ref: v3.0.0 +#dependency_overrides: +# guru_popup: +# path: ../../../guru_ui/packages/guru_popup +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + + build_runner: 2.4.7 + json_serializable: 6.7.1 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/guru_app/packages/guru_utils/test/guru_utils_test.dart b/guru_app/packages/guru_utils/test/guru_utils_test.dart new file mode 100644 index 0000000..eea2199 --- /dev/null +++ b/guru_app/packages/guru_utils/test/guru_utils_test.dart @@ -0,0 +1,5 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:guru_utils/guru_utils.dart'; + +void main() {} diff --git a/guru_app/plugins/guru_analytics_flutter/.gitignore b/guru_app/plugins/guru_analytics_flutter/.gitignore new file mode 100644 index 0000000..4d4ec2c --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/.gitignore @@ -0,0 +1,121 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +.vscode +.gradle +.idea +/local.properties +.DS_Store +/build +.metadata +ios/Flutter/flutter_export_environment.sh + +**/.settings +**/.project +**/.classpath + + +# build id +build_id.properties + +# IntelliJ related + +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +/build/ +flutter_* +.flutter-plugins-dependencies + +# goCli related +go-cli/.packages +go-cli/pubspec.lock + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/gradlew +**/android/gradlew.bat +**/android/fastlane/report.xml +**/android/fastlane/README.md + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* +**/ios/build/* +**/ios/fastlane/README.md +**/ios/fastlane/report.xml + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages + +# Crashlytics +debugSymbols/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# i18n +**/android/res/values/strings_en.arb +lib/generated/i18n.dart + +# output +output/ios/adhok/* +output/ios/appstore/* +ios/fastlane/report.xml +android/fastlane/report.xml + +**/android/fastlane/metadata/android/**/images/** +**/ios/fastlane/screenshots/** +**/android/.safedk \ No newline at end of file diff --git a/guru_app/plugins/guru_analytics_flutter/CHANGELOG.md b/guru_app/plugins/guru_analytics_flutter/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/guru_app/plugins/guru_analytics_flutter/LICENSE b/guru_app/plugins/guru_analytics_flutter/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/guru_app/plugins/guru_analytics_flutter/README.md b/guru_app/plugins/guru_analytics_flutter/README.md new file mode 100644 index 0000000..9ab6d82 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/README.md @@ -0,0 +1,15 @@ +# guru_analytics_flutter + +Guru Analytics Library + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/developing-packages/), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/guru_app/plugins/guru_analytics_flutter/analysis_options.yaml b/guru_app/plugins/guru_analytics_flutter/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/guru_app/plugins/guru_analytics_flutter/android/.gitignore b/guru_app/plugins/guru_analytics_flutter/android/.gitignore new file mode 100644 index 0000000..c6cbe56 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/android/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/guru_app/plugins/guru_analytics_flutter/android/build.gradle b/guru_app/plugins/guru_analytics_flutter/android/build.gradle new file mode 100644 index 0000000..5467bf3 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/android/build.gradle @@ -0,0 +1,73 @@ +group 'guru.core.analytics.flutter.guru_analytics_flutter' +version '2.0' + +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + mavenCentral() + mavenLocal() + maven { url 'https://jitpack.io' } + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.2.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + mavenLocal() + maven { url 'https://jitpack.io' } + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + + +def firebaseCoreProject = findProject(':firebase_core') +if (firebaseCoreProject == null) { + throw new GradleException('Could not find the firebase_core FlutterFire plugin, have you added it as a dependency in your pubspec?') +} else if (!firebaseCoreProject.properties['FirebaseSDKVersion']) { + throw new GradleException('A newer version of the firebase_core FlutterFire plugin is required, please update your firebase_core pubspec dependency.') +} + +def getRootProjectExtOrCoreProperty(name, firebaseCoreProject) { + if (!rootProject.ext.has('FlutterFire')) return firebaseCoreProject.properties[name] + if (!rootProject.ext.get('FlutterFire')[name]) return firebaseCoreProject.properties[name] + return rootProject.ext.get('FlutterFire').get(name) +} + +android { + compileSdkVersion 33 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + minSdkVersion 21 + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + api firebaseCoreProject + implementation platform("com.google.firebase:firebase-bom:${getRootProjectExtOrCoreProperty("FirebaseSDKVersion", firebaseCoreProject)}") + implementation 'com.google.firebase:firebase-analytics' + + implementation 'guru.core.analytics:guru_analytics:0.3.1' +} diff --git a/guru_app/plugins/guru_analytics_flutter/android/gradle/wrapper/gradle-wrapper.properties b/guru_app/plugins/guru_analytics_flutter/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..41dfb87 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/guru_app/plugins/guru_analytics_flutter/android/settings.gradle b/guru_app/plugins/guru_analytics_flutter/android/settings.gradle new file mode 100644 index 0000000..195a78d --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'guru_analytics_flutter' diff --git a/guru_app/plugins/guru_analytics_flutter/android/src/main/AndroidManifest.xml b/guru_app/plugins/guru_analytics_flutter/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..11c2c13 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/android/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/guru_app/plugins/guru_analytics_flutter/android/src/main/kotlin/guru/core/flutter/analytics/guru_analytics_flutter/GuruAnalyticsConstants.kt b/guru_app/plugins/guru_analytics_flutter/android/src/main/kotlin/guru/core/flutter/analytics/guru_analytics_flutter/GuruAnalyticsConstants.kt new file mode 100644 index 0000000..74a7fa1 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/android/src/main/kotlin/guru/core/flutter/analytics/guru_analytics_flutter/GuruAnalyticsConstants.kt @@ -0,0 +1,54 @@ +package guru.core.flutter.analytics.guru_analytics_flutter + +/** + * Created by Haoyi on 2020/12/21. + */ +object GuruAnalyticsConstants { + + object Method { + const val Initialize = "initialize" + const val GetAppInstanceId = "getAppInstanceId" + const val LogEvent = "logEvent" + const val SetUserProperty = "setUserProperty" + const val SetDeviceId = "setDeviceId" + const val SetUid = "setUid" + const val SetAdjustId = "setAdjustId" + const val SetAdId = "setAdId" + const val SetFirebaseId = "setFirebaseId" + const val SetScreen = "setScreen" + const val ZipLogs = "zipLogs" + const val GetStatistic = "getStatistic" + } + + object ErrorCode { + const val SUCCESS = "SUCCESS" + const val GET_APP_INSTANCE_ID_FAILED = "GET_INSTANCE_ID_FAILED" + const val PROPERTY_ERROR = "PROPERTY_ERROR" + } + + object FieldName { + const val BATCH_LIMIT = "batchLimit" + const val UPLOAD_PERIOD_IN_SECONDS = "uploadPeriodInSeconds" + const val DELAYED_IN_SECONDS = "delayedInSeconds" + const val EVENT_EXPIRED_IN_DAYS = "eventExpiredInDays" + const val DEVICE_ID = "deviceId" + const val USER_ID = "userId" + const val ADJUST_ID = "adjustId" + const val AD_ID = "adId" + const val FIREBASE_ID = "firebaseId" + const val EVENT_NAME = "eventName" + const val PARAMETERS = "parameters" + const val PRIORITY = "priority" + const val ENABLED = "enabled" + const val MILLISECONDS = "milliseconds" + const val NAME = "name" + const val VALUE = "value" + const val SCREEN = "screen" + const val DEBUG = "debug" + const val LOGGED = "logged"; + const val UPLOADED = "uploaded"; + const val X_APP_ID = "xAppId" + const val X_DEVICE_INFO = "xDeviceInfo" + } + +} \ No newline at end of file diff --git a/guru_app/plugins/guru_analytics_flutter/android/src/main/kotlin/guru/core/flutter/analytics/guru_analytics_flutter/GuruAnalyticsFlutterPlugin.kt b/guru_app/plugins/guru_analytics_flutter/android/src/main/kotlin/guru/core/flutter/analytics/guru_analytics_flutter/GuruAnalyticsFlutterPlugin.kt new file mode 100644 index 0000000..2839db3 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/android/src/main/kotlin/guru/core/flutter/analytics/guru_analytics_flutter/GuruAnalyticsFlutterPlugin.kt @@ -0,0 +1,242 @@ +package guru.core.flutter.analytics.guru_analytics_flutter + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.util.Log +import androidx.annotation.NonNull +import com.google.firebase.analytics.FirebaseAnalytics +import guru.core.analytics.GuruAnalytics +import guru.core.analytics.data.db.model.EventPriority +import guru.core.analytics.data.model.AnalyticsOptions +import guru.core.analytics.handler.AnalyticsCode + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result +import java.util.HashMap +import java.util.concurrent.Executor +import java.util.concurrent.Executors + +/** GuruAnalyticsFlutterPlugin */ +class GuruAnalyticsFlutterPlugin : FlutterPlugin, MethodCallHandler { + + private val executor: Executor = Executors.newFixedThreadPool(1) + + private lateinit var appContext: Context + + private val handler = Handler(Looper.getMainLooper()) + + private var appInstanceId: String = "" + + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel: MethodChannel + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + appContext = flutterPluginBinding.applicationContext + channel = MethodChannel( + flutterPluginBinding.binaryMessenger, + "flutter.guru.guru_analytics_flutter" + ) + channel.setMethodCallHandler(this) + } + + private fun callInitialize(@NonNull call: MethodCall, @NonNull result: Result) { + val batchLimit = call.argument(GuruAnalyticsConstants.FieldName.BATCH_LIMIT) + val uploadPeriodInSeconds = + call.argument(GuruAnalyticsConstants.FieldName.UPLOAD_PERIOD_IN_SECONDS) + val delayedInSeconds = + call.argument(GuruAnalyticsConstants.FieldName.DELAYED_IN_SECONDS) + val eventExpiredInDays = + call.argument(GuruAnalyticsConstants.FieldName.EVENT_EXPIRED_IN_DAYS) + val debug = call.argument(GuruAnalyticsConstants.FieldName.DEBUG) ?: false + val xAppId = call.argument(GuruAnalyticsConstants.FieldName.X_APP_ID) ?: "" + val xDeviceInfo = + call.argument(GuruAnalyticsConstants.FieldName.X_DEVICE_INFO) ?: "" + Log.w( + "GuruAnalyticsPlugin", + "callInitialize: $batchLimit $uploadPeriodInSeconds $delayedInSeconds $eventExpiredInDays $debug" + ) + GuruAnalytics.Builder(appContext) + .setBatchLimit(batchLimit) + .setUploadPeriodInSeconds(uploadPeriodInSeconds?.toLong()) + .setStartUploadDelayInSecond(delayedInSeconds?.toLong()) + .setEventExpiredInDays(eventExpiredInDays) + .setXAppId(xAppId) + .setXDeviceInfo(xDeviceInfo) + .isDebug(debug) + .isPersistableLog(debug) + .isInitPeriodicWork(true) + .setEventHandlerCallback { code, errorInfo -> onAnalyticsCallback(code, errorInfo) } + .build() + + result.success(true) + } + + private fun onAnalyticsCallback(code: Int, errorInfo: String?) { + handler.post { + channel.invokeMethod( + "onAnalyticsCallback", + mapOf( + "code" to code, + "errorInfo" to errorInfo + ) + ) + } + } + + private fun callGetAppInstanceId(@NonNull result: Result) { + val currentAppInstanceId = appInstanceId + if (currentAppInstanceId.isNotBlank()) { + handler.post { + result.success(currentAppInstanceId) + } + } else { + FirebaseAnalytics.getInstance(appContext).appInstanceId.addOnCompleteListener { + if (it.isSuccessful) { + appInstanceId = it.result ?: "" + handler.post { + result.success(appInstanceId) + } + } else { + handler.post { + result.error( + GuruAnalyticsConstants.ErrorCode.GET_APP_INSTANCE_ID_FAILED, + "getAppInstanceId failed![${it.exception?.toString()}]", + "getAppInstanceId failed![${it.exception?.toString()}]" + ) + } + } + } + } + } + + private fun callLogEvent(@NonNull call: MethodCall, @NonNull result: Result) { + val eventName = call.argument(GuruAnalyticsConstants.FieldName.EVENT_NAME) + if (eventName == null) { + result.error( + GuruAnalyticsConstants.ErrorCode.PROPERTY_ERROR, + "eventName is Empty", + "eventName is Empty" + ) + return + } + val parametersMap = + call.argument>(GuruAnalyticsConstants.FieldName.PARAMETERS) + ?: emptyMap() + val parameters: HashMap = HashMap() + for (parameter in parametersMap) { + val key = parameter.key + val value = parameter.value + if (((value is String) + || (value is Long) + || (value is Int) + || (value is Double) + || (value is Float)) + ) { + parameters[key] = value + } + } + val priority = + call.argument(GuruAnalyticsConstants.FieldName.PRIORITY) ?: EventPriority.DEFAULT + val options = AnalyticsOptions(priority) + GuruAnalytics.INSTANCE.logEvent(eventName, parameters = parameters, options = options) + result.success(true) + } + + private fun callSetUserProperty(@NonNull call: MethodCall, @NonNull result: Result) { + val propertyName = call.argument(GuruAnalyticsConstants.FieldName.NAME) + val propertyValue = call.argument(GuruAnalyticsConstants.FieldName.VALUE) + if (propertyName.isNullOrBlank() || propertyValue.isNullOrBlank()) { + result.error( + GuruAnalyticsConstants.ErrorCode.PROPERTY_ERROR, + "Property Name or Value is NullOrBlank", + "Property Name or Value is NullOrBlank" + ) + return + } + GuruAnalytics.INSTANCE.setUserProperty(propertyName, propertyValue) + result.success(true) + } + + private fun callSetDeviceId(@NonNull call: MethodCall, @NonNull result: Result) { + val deviceId = call.argument(GuruAnalyticsConstants.FieldName.DEVICE_ID) ?: "" + GuruAnalytics.INSTANCE.setDeviceId(deviceId) + result.success(true) + } + + private fun callSetUid(@NonNull call: MethodCall, @NonNull result: Result) { + val deviceId = call.argument(GuruAnalyticsConstants.FieldName.USER_ID) ?: "" + GuruAnalytics.INSTANCE.setUid(deviceId) + result.success(true) + } + + private fun callSetAdjustId(@NonNull call: MethodCall, @NonNull result: Result) { + val adjustId = call.argument(GuruAnalyticsConstants.FieldName.ADJUST_ID) ?: "" + GuruAnalytics.INSTANCE.setAdjustId(adjustId) + result.success(true) + } + + private fun callSetAdId(@NonNull call: MethodCall, @NonNull result: Result) { + val adId = call.argument(GuruAnalyticsConstants.FieldName.AD_ID) ?: "" + GuruAnalytics.INSTANCE.setAdId(adId) + result.success(true) + } + + private fun callSetFirebaseId(@NonNull call: MethodCall, @NonNull result: Result) { + val firebaseId = call.argument(GuruAnalyticsConstants.FieldName.FIREBASE_ID) ?: "" + GuruAnalytics.INSTANCE.setFirebaseId(firebaseId) + result.success(true) + } + + private fun callSetScreen(@NonNull call: MethodCall, @NonNull result: Result) { + val screenName = call.argument(GuruAnalyticsConstants.FieldName.SCREEN) ?: "" + GuruAnalytics.INSTANCE.setScreen(screenName) + result.success(true) + } + + private fun callZipLogs(@NonNull call: MethodCall, @NonNull result: Result) { + val file = GuruAnalytics.INSTANCE.zipLogs(appContext) + result.success(file?.absolutePath ?: "") + } + + private fun callGetStatistic(@NonNull call: MethodCall, @NonNull result: Result) { + val statistic = GuruAnalytics.INSTANCE.getEventsStatics() + result.success( + mapOf( + GuruAnalyticsConstants.FieldName.LOGGED to statistic.eventCountAll, + GuruAnalyticsConstants.FieldName.UPLOADED to statistic.eventCountUploaded + ) + ) + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + Log.d("GuruAnalytics", "onMethodCall ${call.method}") + when (call.method) { + GuruAnalyticsConstants.Method.Initialize -> callInitialize(call, result) + GuruAnalyticsConstants.Method.GetAppInstanceId -> callGetAppInstanceId(result) + GuruAnalyticsConstants.Method.LogEvent -> callLogEvent(call, result) + GuruAnalyticsConstants.Method.SetUserProperty -> callSetUserProperty(call, result) + GuruAnalyticsConstants.Method.SetDeviceId -> callSetDeviceId(call, result) + GuruAnalyticsConstants.Method.SetUid -> callSetUid(call, result) + GuruAnalyticsConstants.Method.SetAdjustId -> callSetAdjustId(call, result) + GuruAnalyticsConstants.Method.SetAdId -> callSetAdId(call, result) + GuruAnalyticsConstants.Method.SetFirebaseId -> callSetFirebaseId(call, result) + GuruAnalyticsConstants.Method.SetScreen -> callSetScreen(call, result) + GuruAnalyticsConstants.Method.ZipLogs -> callZipLogs(call, result) + GuruAnalyticsConstants.Method.GetStatistic -> callGetStatistic(call, result) + else -> { + result.notImplemented() + } + } + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } +} diff --git a/guru_app/plugins/guru_analytics_flutter/example/.gitignore b/guru_app/plugins/guru_analytics_flutter/example/.gitignore new file mode 100644 index 0000000..0fa6b67 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/guru_app/plugins/guru_analytics_flutter/example/README.md b/guru_app/plugins/guru_analytics_flutter/example/README.md new file mode 100644 index 0000000..3c71e7d --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/README.md @@ -0,0 +1,16 @@ +# guru_analytics_flutter_example + +Demonstrates how to use the guru_analytics_flutter plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/guru_app/plugins/guru_analytics_flutter/example/analysis_options.yaml b/guru_app/plugins/guru_analytics_flutter/example/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/guru_app/plugins/guru_analytics_flutter/example/android/.gitignore b/guru_app/plugins/guru_analytics_flutter/example/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/guru_app/plugins/guru_analytics_flutter/example/android/app/build.gradle b/guru_app/plugins/guru_analytics_flutter/example/android/app/build.gradle new file mode 100644 index 0000000..3344969 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/android/app/build.gradle @@ -0,0 +1,68 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion flutter.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "guru.core.flutter.analytics.guru_analytics_flutter_example" + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/guru_app/plugins/guru_analytics_flutter/example/android/app/src/debug/AndroidManifest.xml b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..b41ce4f --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/AndroidManifest.xml b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..04fdf70 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/kotlin/guru/core/flutter/analytics/guru_analytics_flutter_example/MainActivity.kt b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/kotlin/guru/core/flutter/analytics/guru_analytics_flutter_example/MainActivity.kt new file mode 100644 index 0000000..95f7f75 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/kotlin/guru/core/flutter/analytics/guru_analytics_flutter_example/MainActivity.kt @@ -0,0 +1,6 @@ +package guru.core.flutter.analytics.guru_analytics_flutter_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/drawable-v21/launch_background.xml b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/drawable/launch_background.xml b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/values-night/styles.xml b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..3db14bb --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/values/styles.xml b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..d460d1e --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/guru_app/plugins/guru_analytics_flutter/example/android/app/src/profile/AndroidManifest.xml b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..b41ce4f --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/guru_app/plugins/guru_analytics_flutter/example/android/build.gradle b/guru_app/plugins/guru_analytics_flutter/example/android/build.gradle new file mode 100644 index 0000000..4256f91 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/guru_app/plugins/guru_analytics_flutter/example/android/gradle.properties b/guru_app/plugins/guru_analytics_flutter/example/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/guru_app/plugins/guru_analytics_flutter/example/android/gradle/wrapper/gradle-wrapper.properties b/guru_app/plugins/guru_analytics_flutter/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ffed3a2 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/guru_app/plugins/guru_analytics_flutter/example/android/settings.gradle b/guru_app/plugins/guru_analytics_flutter/example/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/.gitignore b/guru_app/plugins/guru_analytics_flutter/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Flutter/AppFrameworkInfo.plist b/guru_app/plugins/guru_analytics_flutter/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..8d4492f --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 9.0 + + diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Flutter/Debug.xcconfig b/guru_app/plugins/guru_analytics_flutter/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Flutter/Release.xcconfig b/guru_app/plugins/guru_analytics_flutter/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Podfile b/guru_app/plugins/guru_analytics_flutter/example/ios/Podfile new file mode 100644 index 0000000..1e8c3c9 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..9ecc6f0 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,484 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 39253T242A; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = guru.core.flutter.analytics.guruAnalyticsFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 39253T242A; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = guru.core.flutter.analytics.guruAnalyticsFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 39253T242A; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = guru.core.flutter.analytics.guruAnalyticsFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c87d15a --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/AppDelegate.swift b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..28c6bf0 Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..f091b6b Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..d0ef06e Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..c8f9ed8 Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..75b2d16 Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c4df70d Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Base.lproj/Main.storyboard b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Info.plist b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Info.plist new file mode 100644 index 0000000..17bb105 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Guru Analytics Flutter + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + guru_analytics_flutter_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Runner-Bridging-Header.h b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/guru_app/plugins/guru_analytics_flutter/example/lib/main.dart b/guru_app/plugins/guru_analytics_flutter/example/lib/main.dart new file mode 100644 index 0000000..59cbab9 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/lib/main.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:guru_analytics_flutter/guru_analytics_flutter.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + String _platformVersion = 'Unknown'; + + @override + void initState() { + super.initState(); + initPlatformState(); + } + + // Platform messages are asynchronous, so we initialize in an async method. + Future initPlatformState() async { + String platformVersion; + // Platform messages may fail, so we use a try/catch PlatformException. + // We also handle the message potentially returning null. + + + // If the widget was removed from the tree while the asynchronous platform + // message was in flight, we want to discard the reply rather than calling + // setState to update our non-existent appearance. + if (!mounted) return; + + setState(() { + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: Center( + child: Text('Running on: $_platformVersion\n'), + ), + ), + ); + } +} diff --git a/guru_app/plugins/guru_analytics_flutter/example/pubspec.lock b/guru_app/plugins/guru_analytics_flutter/example/pubspec.lock new file mode 100644 index 0000000..ddf2980 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/pubspec.lock @@ -0,0 +1,243 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.12" + async: + dependency: transitive + description: + name: async + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.9.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.16.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.5" + facebook_app_events: + dependency: transitive + description: + name: facebook_app_events + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.19.0" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + firebase_analytics: + dependency: transitive + description: + name: firebase_analytics + url: "https://pub.flutter-io.cn" + source: hosted + version: "10.1.0" + firebase_analytics_platform_interface: + dependency: transitive + description: + name: firebase_analytics_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.3.17" + firebase_analytics_web: + dependency: transitive + description: + name: firebase_analytics_web + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.1+8" + firebase_core: + dependency: transitive + description: + name: firebase_core + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.5.2" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + guru_analytics_flutter: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "2.0.0" + js: + dependency: transitive + description: + name: js + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.4" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.12" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.2" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.12" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" +sdks: + dart: ">=2.17.0-0 <3.0.0" + flutter: ">=2.5.0" diff --git a/guru_app/plugins/guru_analytics_flutter/example/pubspec.yaml b/guru_app/plugins/guru_analytics_flutter/example/pubspec.yaml new file mode 100644 index 0000000..5e215e9 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/pubspec.yaml @@ -0,0 +1,84 @@ +name: guru_analytics_flutter_example +description: Demonstrates how to use the guru_analytics_flutter plugin. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: ">=2.16.2 <3.0.0" + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + guru_analytics_flutter: + # When depending on this package from a real application you should use: + # guru_analytics_flutter: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^1.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/guru_app/plugins/guru_analytics_flutter/example/test/widget_test.dart b/guru_app/plugins/guru_analytics_flutter/example/test/widget_test.dart new file mode 100644 index 0000000..df21e77 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/example/test/widget_test.dart @@ -0,0 +1,27 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:guru_analytics_flutter_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && + widget.data!.startsWith('Running on:'), + ), + findsOneWidget, + ); + }); +} diff --git a/guru_app/plugins/guru_analytics_flutter/ios/.gitignore b/guru_app/plugins/guru_analytics_flutter/ios/.gitignore new file mode 100644 index 0000000..0c88507 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/guru_app/plugins/guru_analytics_flutter/ios/Assets/.gitkeep b/guru_app/plugins/guru_analytics_flutter/ios/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/guru_app/plugins/guru_analytics_flutter/ios/Classes/GuruAnalyticsConstants.swift b/guru_app/plugins/guru_analytics_flutter/ios/Classes/GuruAnalyticsConstants.swift new file mode 100644 index 0000000..ffbc8ff --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/ios/Classes/GuruAnalyticsConstants.swift @@ -0,0 +1,52 @@ +// +// GuruAnalyticsConstants.swift +// guru_analytics_flutter +// +// Created by KernelTea on 2022/11/26. +// + +import Foundation + +class AnalyticsErrorCode { + static let SUCCESS = "SUCCESS" + static let GET_APP_INSTANCE_ID_FAILED = "GET_INSTANCE_ID_FAILED" + static let PROPERTY_ERROR = "PROPERTY_ERROR" + static let EVENT_ERROR = "EVENT_ERROR" +} + +class AnalyticsMethod { + static let Initialize = "initialize" + static let GetAppInstanceId = "getAppInstanceId" + static let LogEvent = "logEvent" + static let SetUserProperty = "setUserProperty" + static let SetDeviceId = "setDeviceId" + static let SetUid = "setUid" + static let SetAdjustId = "setAdjustId" + static let SetAdId = "setAdId" + static let SetFirebaseId = "setFirebaseId" + static let SetScreen = "setScreen" + static let ZipLogs = "zipLogs" + static let GetStatistic = "getStatistic" +} + +class AnalyticsFieldName { + static let DEVICE_ID = "deviceId" + static let USER_ID = "userId" + static let ADJUST_ID = "adjustId" + static let AD_ID = "adId" + static let FIREBASE_ID = "firebaseId" + static let EVENT_NAME = "eventName" + static let PARAMETERS = "parameters" + static let ENABLED = "enabled" + static let MILLISECONDS = "milliseconds" + static let NAME = "name" + static let VALUE = "value" + static let SCREEN = "screen" + static let BATCH_LIMIT = "batchLimit" + static let UPLOAD_PERIOD_IN_SECONDS = "uploadPeriodInSeconds" + static let DEBUG = "debug" + static let DELAYED_IN_SECONDS = "delayedInSeconds" + static let EVENT_EXPIRED_IN_DAYS = "eventExpiredInDays" + static let X_APP_ID = "xAppId"; + static let X_DEVICE_INFO = "xDeviceInfo"; +} diff --git a/guru_app/plugins/guru_analytics_flutter/ios/Classes/GuruAnalyticsFlutterPlugin.h b/guru_app/plugins/guru_analytics_flutter/ios/Classes/GuruAnalyticsFlutterPlugin.h new file mode 100644 index 0000000..5e0b5aa --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/ios/Classes/GuruAnalyticsFlutterPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface GuruAnalyticsFlutterPlugin : NSObject +@end diff --git a/guru_app/plugins/guru_analytics_flutter/ios/Classes/GuruAnalyticsFlutterPlugin.m b/guru_app/plugins/guru_analytics_flutter/ios/Classes/GuruAnalyticsFlutterPlugin.m new file mode 100644 index 0000000..b327c3a --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/ios/Classes/GuruAnalyticsFlutterPlugin.m @@ -0,0 +1,15 @@ +#import "GuruAnalyticsFlutterPlugin.h" +#if __has_include() +#import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "guru_analytics_flutter-Swift.h" +#endif + +@implementation GuruAnalyticsFlutterPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftGuruAnalyticsFlutterPlugin registerWithRegistrar:registrar]; +} +@end diff --git a/guru_app/plugins/guru_analytics_flutter/ios/Classes/SwiftGuruAnalyticsFlutterPlugin.swift b/guru_app/plugins/guru_analytics_flutter/ios/Classes/SwiftGuruAnalyticsFlutterPlugin.swift new file mode 100644 index 0000000..90c9873 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/ios/Classes/SwiftGuruAnalyticsFlutterPlugin.swift @@ -0,0 +1,268 @@ +import Flutter +import UIKit +import FirebaseAnalytics +import GuruAnalyticsLib + +public class SwiftGuruAnalyticsFlutterPlugin: NSObject, FlutterPlugin { + public static var debugMode: Bool = false + + private var registeredChannel: FlutterMethodChannel? + public static func register(with registrar: FlutterPluginRegistrar) { + let instance = SwiftGuruAnalyticsFlutterPlugin() + instance.setup(registrar: registrar) + } + + private func setup(registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "flutter.guru.guru_analytics_flutter", binaryMessenger: registrar.messenger()) + registrar.addMethodCallDelegate(self, channel: channel) + registeredChannel = channel + } + + private func callInitialize(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? [String : Any] ?? [String : Any]() + let batchLimit = arguments[AnalyticsFieldName.BATCH_LIMIT] as? Int ?? 32 + let uploadPeriodInSeconds = arguments[AnalyticsFieldName.UPLOAD_PERIOD_IN_SECONDS] as? Int ?? 60 + let debug = arguments[AnalyticsFieldName.DEBUG] as? Bool ?? false + let delayedInSeconds = arguments[AnalyticsFieldName.DELAYED_IN_SECONDS] as? Int ?? 5 + let eventExpiredInDays = arguments[AnalyticsFieldName.EVENT_EXPIRED_IN_DAYS] as? Int ?? 7 + let saasXAPPID = arguments[AnalyticsFieldName.X_APP_ID] as? String ?? "" + let saasXDEVICEINFO = arguments[AnalyticsFieldName.X_DEVICE_INFO] as? String ?? "" + + NSLog("[GuruAnalytics] initialize batchLimit:\(batchLimit) uploadPeriodInSeconds:\(uploadPeriodInSeconds) eventExpiredInDays:\(eventExpiredInDays) delayedInSeconds:\(delayedInSeconds) saasXAPPID:\(saasXAPPID) saasXDEVICEINFO:\(saasXDEVICEINFO) debug:\(debug)") + GuruAnalytics.registerInternalEventObserver(reportCallback: processAnalyticsCallback) + GuruAnalytics.initializeLib(uploadPeriodInSecond: Double(uploadPeriodInSeconds), batchLimit: batchLimit, eventExpiredSeconds: Double(eventExpiredInDays * 86400), initializeTimeout: Double(delayedInSeconds), saasXAPPID: saasXAPPID, saasXDEVICEINFO: saasXDEVICEINFO, loggerDebug: debug) + SwiftGuruAnalyticsFlutterPlugin.debugMode = debug + result(true) + } + + private func callGetAppInstanceId(result: @escaping FlutterResult) { + let appInstanceId = Analytics.appInstanceID() + NSLog("[GuruAnalytics] ==> appInstanceID \(String(describing: appInstanceId))") + result(appInstanceId) + // result(FlutterError.init(code: error.errorCode.description, message: error.errorMessage, details: "\(sessionId) \(error.errorMessage) [\(msg)]) + } + + private func processAnalyticsCallback(_ errorCode: Int, _ info: String) { + registeredChannel?.invokeMethod("onAnalyticsCallback", arguments: [ + "code": errorCode, + "errorInfo": info + ]) + } + + private func callLogEvent(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String : Any], + let eventName = arguments[AnalyticsFieldName.EVENT_NAME] as? String + else { + let error = FlutterError.init( + code: AnalyticsErrorCode.EVENT_ERROR, + message: "eventName is Empty", + details: "eventName is Empty") + result(error) + return + } + + let parametersMap = arguments[AnalyticsFieldName.PARAMETERS] as? [String : Any] ?? [String : Any]() + + GuruAnalytics.logEvent(eventName, parameters: parametersMap) + if (SwiftGuruAnalyticsFlutterPlugin.debugMode) { + NSLog("[GuruAnalytics] ==> logEvent(\(eventName)") + } + + result(true) + } + + private func callSetUserProperty(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String : Any], + let name = arguments[AnalyticsFieldName.NAME] as? String, + let value = arguments[AnalyticsFieldName.VALUE] as? String + else { + let error = FlutterError.init( + code: AnalyticsErrorCode.PROPERTY_ERROR, + message: "Property Name or Value is NullOrBlank!", + details: "Property Name or Value is NullOrBlank!") + result(error) + return + } + GuruAnalytics.setUserProperty(value, forName: name) + if (SwiftGuruAnalyticsFlutterPlugin.debugMode) { + NSLog("[GuruAnalytics] ==> setUserProperty \(name) : \(value)") + } + result(true) + + } + + private func callSetDeviceId(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String : Any], + let deviceId = arguments[AnalyticsFieldName.DEVICE_ID] as? String + else { + let error = FlutterError.init( + code: AnalyticsErrorCode.PROPERTY_ERROR, + message: "Device ID is Null", + details: "Device ID is Null") + result(error) + return + } + GuruAnalytics.setDeviceId(deviceId) + if (SwiftGuruAnalyticsFlutterPlugin.debugMode) { + NSLog("[GuruAnalytics] ==> setDeviceId \(deviceId)") + } + result(true) + + } + + private func callSetUid(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String : Any], + let userId = arguments[AnalyticsFieldName.USER_ID] as? String + else { + let error = FlutterError.init( + code: AnalyticsErrorCode.PROPERTY_ERROR, + message: "User ID is Null", + details: "User ID is Null") + result(error) + return + } + GuruAnalytics.setUserID(userId) + if (SwiftGuruAnalyticsFlutterPlugin.debugMode) { + NSLog("[GuruAnalytics] ==> setUserID \(userId)") + } + result(true) + } + + private func callSetAdjustId(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String : Any], + let adjustId = arguments[AnalyticsFieldName.ADJUST_ID] as? String + else { + let error = FlutterError.init( + code: AnalyticsErrorCode.PROPERTY_ERROR, + message: "Adjust ID is Null", + details: "Adjust ID is Null") + result(error) + return + } + GuruAnalytics.setAdjustId(adjustId) + if (SwiftGuruAnalyticsFlutterPlugin.debugMode) { + NSLog("[GuruAnalytics] ==> setAdjustId \(adjustId)") + } + result(true) + + } + + private func callSetAdId(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String : Any], + let adId = arguments[AnalyticsFieldName.AD_ID] as? String + else { + let error = FlutterError.init( + code: AnalyticsErrorCode.PROPERTY_ERROR, + message: "AD ID is Null", + details: "AD ID is Null") + result(error) + return + } + GuruAnalytics.setAdId(adId) + if (SwiftGuruAnalyticsFlutterPlugin.debugMode) { + NSLog("[GuruAnalytics] ==> setAdId \(adId)") + } + result(true) + } + + private func callSetFirebaseId(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String : Any], + let firebaseId = arguments[AnalyticsFieldName.FIREBASE_ID] as? String + else { + let error = FlutterError.init( + code: AnalyticsErrorCode.PROPERTY_ERROR, + message: "Firebase ID is Null", + details: "Firebase ID is Null") + result(error) + return + } + GuruAnalytics.setFirebaseId(firebaseId) + if (SwiftGuruAnalyticsFlutterPlugin.debugMode) { + NSLog("[GuruAnalytics] ==> setFirebaseId \(firebaseId)") + } + result(true) + + } + + private func callSetScreen(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String : Any], + let screen = arguments[AnalyticsFieldName.SCREEN] as? String + else { + let error = FlutterError.init( + code: AnalyticsErrorCode.PROPERTY_ERROR, + message: "ScreenName is Null", + details: "ScreenName is Null") + result(error) + return + } + GuruAnalytics.setScreen(screen) + if (SwiftGuruAnalyticsFlutterPlugin.debugMode) { + NSLog("[GuruAnalytics] ==> setScreen \(screen)") + } + result(true) + } + + private func callZipLogs(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + if (SwiftGuruAnalyticsFlutterPlugin.debugMode) { + NSLog("[GuruAnalytics] ==> callZipLogs") + } + GuruAnalytics.eventsLogsArchive { url in + result(url?.absoluteString ?? "") + } + } + + private func callGetStatistic(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + if (SwiftGuruAnalyticsFlutterPlugin.debugMode) { + NSLog("[GuruAnalytics] ==> callGetStatistic") + } + GuruAnalytics.debug_eventsStatistics{ uploadedEventCount, loggedEventCount in + result(["logged" : loggedEventCount, "uploaded" : uploadedEventCount]) + } + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + // result("iOS " + UIDevice.current.systemVersion) + NSLog("[GuruAnalytics] methodHandler \(call.method)") + switch(call.method) { + case AnalyticsMethod.Initialize: + callInitialize(call, result: result) + break; + case AnalyticsMethod.GetAppInstanceId: + callGetAppInstanceId(result: result) + break; + case AnalyticsMethod.LogEvent: + callLogEvent(call, result: result) + break; + case AnalyticsMethod.SetUserProperty: + callSetUserProperty(call, result: result) + break; + case AnalyticsMethod.SetDeviceId: + callSetDeviceId(call, result: result) + break; + case AnalyticsMethod.SetUid: + callSetUid(call, result: result) + break; + case AnalyticsMethod.SetAdjustId: + callSetAdjustId(call, result: result) + break; + case AnalyticsMethod.SetAdId: + callSetAdId(call, result: result) + break; + case AnalyticsMethod.SetFirebaseId: + callSetFirebaseId(call, result: result) + break; + case AnalyticsMethod.SetScreen: + callSetScreen(call, result: result) + break; + case AnalyticsMethod.ZipLogs: + callZipLogs(call, result: result) + break; + case AnalyticsMethod.GetStatistic: + callGetStatistic(call, result: result) + break; + default: + result(FlutterMethodNotImplemented) + } + } + +} diff --git a/guru_app/plugins/guru_analytics_flutter/ios/guru_analytics_flutter.podspec b/guru_app/plugins/guru_analytics_flutter/ios/guru_analytics_flutter.podspec new file mode 100644 index 0000000..ec1648e --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/ios/guru_analytics_flutter.podspec @@ -0,0 +1,44 @@ +require 'yaml' + +pubspec = YAML.load_file(File.join('..', 'pubspec.yaml')) +library_version = pubspec['version'].gsub('+', '-') + +firebase_sdk_version = '6.33.0' +if defined?($FirebaseSDKVersion) + Pod::UI.puts "#{pubspec['name']}: Using user specified Firebase SDK version '#{$FirebaseSDKVersion}'" + firebase_sdk_version = $FirebaseSDKVersion +else + firebase_core_script = File.join(File.expand_path('..', File.expand_path('..', File.dirname(__FILE__))), 'firebase_core/ios/firebase_sdk_version.rb') + if File.exist?(firebase_core_script) + require firebase_core_script + firebase_sdk_version = firebase_sdk_version! + Pod::UI.puts "#{pubspec['name']}: Using Firebase SDK version '#{firebase_sdk_version}' defined in 'firebase_core'" + end +end + +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint guru_analytics_flutter.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'guru_analytics_flutter' + s.version = '2.0.0' + s.summary = 'Guru Analytics Library.' + s.description = <<-DESC +A new Flutter project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Guru' => 'haoyi.zhang@castbox.fm' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.static_framework = true + s.dependency 'Firebase/Analytics', "~> #{firebase_sdk_version}" + s.dependency 'GuruAnalyticsLib', "0.3.1" + s.platform = :ios, '11.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' +end diff --git a/guru_app/plugins/guru_analytics_flutter/lib/event_logger.dart b/guru_app/plugins/guru_analytics_flutter/lib/event_logger.dart new file mode 100644 index 0000000..4683267 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/lib/event_logger.dart @@ -0,0 +1,447 @@ +import 'package:facebook_app_events/facebook_app_events.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:firebase_analytics/observer.dart'; +import 'package:flutter/widgets.dart'; +import 'guru/guru_statistic.dart'; + +import 'event_logger_common.dart'; +import 'events_constants.dart'; +import 'guru/guru_event_logger.dart'; +import 'guru_analytics_flutter.dart'; + +class EventLogger { + static FirebaseAnalytics analytics = FirebaseAnalytics.instance; + + static FacebookAppEvents facebook = FacebookAppEvents(); + + static GuruEventLogger guru = GuruEventLogger.instance; + + static List transmitters = []; + + static NavigatorObserver getLogScreenNavigatorObserver() { + return FirebaseAnalyticsObserver(analytics: analytics); + } + + static bool dumpLog = false; + + static AppEventCapabilities _capabilities = AppEventCapabilities.standardCapabilities; + + static void setCapabilities(AppEventCapabilities capabilities) { + _capabilities = capabilities; + } + + static AppEventCapabilities getCapabilities() { + return _capabilities; + } + + static void initialize( + {required String appId, + required String deviceInfo, + int batchLimit = 32, + int uploadPeriodInSeconds = 60, + int delayedInSeconds = 10, + int eventExpiredInDays = 7, + bool debug = false, + AnalyticsCallback? callback}) { + guru.initialize( + appId: appId, + deviceInfo: deviceInfo, + uploadPeriodInSeconds: uploadPeriodInSeconds, + batchLimit: batchLimit, + delayedInSeconds: delayedInSeconds, + eventExpiredInDays: eventExpiredInDays, + callback: callback, + debug: debug); + facebook.setAdvertiserTracking(enabled: true); + print("init guru"); + } + + static void setGuruPriorityGetter(PriorityGetter priorityGetter) { + guru.setPriorityGetter(priorityGetter); + } + + /// Creates a new map containing all of the key/value pairs from [parameters] + /// except those whose value is `null`. + static Map _filterOutNulls(Map parameters) { + final Map filtered = {}; + parameters.forEach((String key, dynamic value) { + if (value != null) { + filtered[key] = value; + } + }); + return filtered; + } + + static registerTransmitter(EventTransmitter transmitter) { + transmitters.add(transmitter); + } + + static unregisterTransmitter(EventTransmitter transmitter) { + transmitters.remove(transmitter); + } + + static transmit(String name, Map parameters) { + for (var transmitter in transmitters) { + transmitter.transmit(name, parameters); + } + } + + static Future getAppInstanceId() { + return guru.getAppInstanceId(); + } + + static void firebaseLogEvent( + {required String name, Map parameters = const {}}) { + if (dumpLog) { + print("[firebase] logEvent: $name $parameters"); + } + analytics.logEvent(name: name, parameters: _filterOutNulls(parameters)); + } + + static void facebookLogEvent( + {required String name, + Map parameters = const {}, + double? valueToSum}) { + if (dumpLog) { + print("[facebook] logEvent: $name $parameters value:$valueToSum"); + } + + facebook.logEvent(name: name, parameters: _filterOutNulls(parameters), valueToSum: valueToSum); + } + + static void guruLogEvent( + {required String name, + Map parameters = const {}, + int? priority}) { + if (dumpLog) { + print("[guru] logEvent: $name $parameters"); + } + guru.logEvent(name, parameters: parameters, priority: priority); + } + + static Future setUserProperty(String name, String value) async { + await analytics.setUserProperty(name: name, value: value); + if (_capabilities.hasCapability(AppEventCapabilities.guru)) { + guru.setUserProperty(name, value); + } + } + + static setDeviceId(String deviceId) { + if (_capabilities.hasCapability(AppEventCapabilities.guru)) { + guru.setDeviceId(deviceId); + } + } + + static setUserId(String userId) { + analytics.setUserId(id: userId, callOptions: AnalyticsCallOptions(global: true)); + if (_capabilities.hasCapability(AppEventCapabilities.guru)) { + guru.setUserId(userId); + } + } + + static setAdjustId(String adjustId) { + if (_capabilities.hasCapability(AppEventCapabilities.guru)) { + guru.setAdjustId(adjustId); + } + } + + static setFirebaseId(String firebaseId) { + if (_capabilities.hasCapability(AppEventCapabilities.guru)) { + guru.setFirebaseId(firebaseId); + } + } + + static setAdId(String adId) { + if (_capabilities.hasCapability(AppEventCapabilities.guru)) { + guru.setAdId(adId); + } + } + + static Future zipGuruLogs() { + return guru.zipLogs(); + } + + static Future getStatistic() { + return guru.getStatistic(); + } + + static logScreen(String screenName) { + debugPrint("log SCREEN " + screenName); + analytics.setCurrentScreen(screenName: screenName); + if (_capabilities.hasCapability(AppEventCapabilities.guru)) { + guru.setScreen(screenName); + } +// final facebookAppEvents = FacebookAppEvents(); +// facebookAppEvents.logEvent( +// name: 'screen', +// parameters: {"name": screenName, "category": "screen"}, +// ); + } + + static logEvent(String eventName, Map parameters, + {int? priority, AppEventOptions? options}) { + final capabilities = options?.capabilities ?? _capabilities; + if (capabilities.hasCapability(AppEventCapabilities.firebase)) { + firebaseLogEvent( + name: eventName, + parameters: options?.firebaseParamsConvertor?.call(parameters) ?? parameters, + ); + } + if (capabilities.hasCapability(AppEventCapabilities.facebook)) { + facebookLogEvent( + name: eventName, + parameters: options?.facebookParamsConvertor?.call(parameters) ?? parameters, + ); + } + if (capabilities.hasCapability(AppEventCapabilities.guru)) { + guruLogEvent( + name: eventName, + parameters: options?.guruParamsConvertor?.call(parameters) ?? parameters, + priority: priority); + } + transmit(eventName, parameters); + } + + static logEventWithCategoryAndName(String eventName, String itemCategory, String itemName) { + logEvent(eventName, { + "item_category": itemCategory, + "item_name": itemName, + }); + } + + static logFbRate({String? contentId, String? contentType, int? maxRatingValue}) { + facebookLogEvent(name: FacebookAppEventsConstants.EVENT_NAME_RATE, parameters: { + FacebookAppEventsConstants.EVENT_PARAM_CONTENT_TYPE: contentType, + FacebookAppEventsConstants.EVENT_PARAM_CONTENT_ID: contentId, + FacebookAppEventsConstants.EVENT_PARAM_MAX_RATING_VALUE: maxRatingValue + }); + } + + static logAchievementUnlocked({String? description}) { + facebookLogEvent( + name: FacebookAppEventsConstants.EVENT_NAME_ACHIEVEMENT_UNLOCKED, + parameters: {FacebookAppEventsConstants.EVENT_PARAM_DESCRIPTION: description}); + } + + static logFbAdClick(String adType) { + facebookLogEvent( + name: FacebookAppEventsConstants.EVENT_NAME_AD_CLICK, + parameters: {FacebookAppEventsConstants.EVENT_PARAM_AD_TYPE: adType}); + } + + static logFbAdImpression(String adType) { + facebookLogEvent( + name: FacebookAppEventsConstants.EVENT_NAME_AD_IMPRESSION, + parameters: {FacebookAppEventsConstants.EVENT_PARAM_AD_TYPE: adType}); + } + + static logFbAchieveLevel(String level) { + facebookLogEvent( + name: FacebookAppEventsConstants.EVENT_NAME_ACHIEVED_LEVEL, + parameters: {FacebookAppEventsConstants.EVENT_PARAM_LEVEL: level}); + } + + static logFbSearch(String contentType, String search, bool success) { + facebookLogEvent(name: FacebookAppEventsConstants.EVENT_NAME_SEARCH, parameters: { + FacebookAppEventsConstants.EVENT_PARAM_CONTENT_TYPE: contentType, + FacebookAppEventsConstants.EVENT_PARAM_SEARCH_STRING: search, + FacebookAppEventsConstants.EVENT_PARAM_SUCCESS: success ? 1 : 0, + }); + } + + static logFbAddToWishList(String contentType, String contentId, String currency, double price) { + facebookLogEvent( + name: FacebookAppEventsConstants.EVENT_NAME_ADD_TO_WISHLIST, + parameters: { + FacebookAppEventsConstants.EVENT_PARAM_CONTENT_TYPE: contentType, + FacebookAppEventsConstants.EVENT_PARAM_CONTENT_ID: contentId, + FacebookAppEventsConstants.EVENT_PARAM_CURRENCY: currency, + }, + valueToSum: price, + ); + } + + static logFbAddToCart(String contentType, String contentId, String currency, double price) { + facebookLogEvent( + name: FacebookAppEventsConstants.EVENT_NAME_ADD_TO_CART, + parameters: { + FacebookAppEventsConstants.EVENT_PARAM_CONTENT_TYPE: contentType, + FacebookAppEventsConstants.EVENT_PARAM_CONTENT_ID: contentId, + FacebookAppEventsConstants.EVENT_PARAM_CURRENCY: currency, + }, + valueToSum: price, + ); + } + + static logFbContentView(String contentType, String contentId, String currency, double price) { + facebookLogEvent( + name: FacebookAppEventsConstants.EVENT_NAME_CONTENT_VIEW, + parameters: { + FacebookAppEventsConstants.EVENT_PARAM_CONTENT_TYPE: contentType, + FacebookAppEventsConstants.EVENT_PARAM_CONTENT_ID: contentId, + FacebookAppEventsConstants.EVENT_PARAM_CURRENCY: currency, + }, + valueToSum: price, + ); + } + + static logBeginTutorial(String contentId) { + logEvent("tutorial_begin", {"item_name": contentId}); + } + + //contentData: Parameter key used to specify data for the one or more pieces of content being logged about. + // Data should be a JSON encoded string. + // Example: "[{\"id\": \"1234\", \"quantity\": 2, \"item_price\": 5.99}, {\"id\": \"5678\", \"quantity\": 1, \"item_price\": 9.99}]" + static logCompleteTutorial(String contentId, bool success, {String contentData = ""}) { + logEvent("tutorial_complete", {"item_name": contentId}); + + facebookLogEvent( + name: FacebookAppEventsConstants.EVENT_NAME_COMPLETED_TUTORIAL, + parameters: { + FacebookAppEventsConstants.EVENT_PARAM_CONTENT: contentData, + FacebookAppEventsConstants.EVENT_PARAM_CONTENT_ID: contentId, + FacebookAppEventsConstants.EVENT_PARAM_SUCCESS: success ? 1 : 0 + }); + } + + static logSpendCredits(String contentId, String contentType, int totalValue, + {String virtualCurrencyName = "", String contentData = "", String scene = "", int? balance}) { + logEvent( + "spend_virtual_currency", + _filterOutNulls({ + "item_name": contentId, + "item_category": contentType, + "virtual_currency_name": virtualCurrencyName, + "value": totalValue, + "balance": balance, + "scene": scene + })); + + facebookLogEvent( + name: FacebookAppEventsConstants.EVENT_NAME_SPENT_CREDITS, + parameters: { + FacebookAppEventsConstants.EVENT_PARAM_CONTENT: contentData, + FacebookAppEventsConstants.EVENT_PARAM_CONTENT_ID: contentId, + FacebookAppEventsConstants.EVENT_PARAM_CONTENT_TYPE: contentType + }, + valueToSum: totalValue.toDouble()); + } + + static logFbPurchase(double amount, + {String currency = "", + String? contentId, + String? adPlatform, + Map additionParameters = const {}}) { + final parameters = {}..addAll(additionParameters); + + if (contentId?.isNotEmpty == true) { + parameters[FacebookAppEventsConstants.EVENT_PARAM_CONTENT_ID] = contentId; + } + if (adPlatform?.isNotEmpty == true) { + parameters['ad_platform'] = adPlatform; + } + if (dumpLog) { + print("[facebook] logPurchase: $amount $currency parameters: $parameters"); + } + + facebook.logPurchase(amount: amount, currency: currency, parameters: parameters); + } + + /// LogAdRevenue. + /// + /// * [adRevenue] 当前广告产生的revenue(单位 USD!) + /// * [adPlatform] 广告平台,Max OR MoPub + /// * [adFormat] "BANNER"|"MREC"|"LEADER"|"INTER"|"REWARDED"|"REWARDED_INTER"|"NATIVE"|"XPROMO" + /// * [adSource] Ad Network Name + /// * [adUnitName] Ad Unit Name or Id + /// + static logAdRevenue(double adRevenue, String adPlatform, String currency) { + final parameters = { + FirebaseEventsParams.AD_PLATFORM: adPlatform, + FirebaseEventsParams.CURRENCY: currency, + FirebaseEventsParams.VALUE: adRevenue, + }; + firebaseLogEvent(name: "tch_ad_rev_roas_001", parameters: parameters); + if (_capabilities.hasCapability(AppEventCapabilities.guru)) { + guruLogEvent( + name: "tch_ad_rev_roas_001", parameters: parameters, priority: EventPriority.EMERGENCE); + } + transmit("tch_ad_rev_roas_001", parameters); + } + + /// logAdLtv. + /// + /// * [phase] ltv所属的阶段 现有阶段有: tch_ad_rev_top40, tch_ad_rev_top30, + /// tch_ad_rev_top20, tch_ad_rev_top10, tch_ad_rev_top5 + /// * [ltv] 用户在该[phase]下所产生的LTV + /// + static logAdLtv(String phase, double ltv) { + final parameters = {FirebaseEventsParams.CURRENCY: "USD", FirebaseEventsParams.VALUE: ltv}; + firebaseLogEvent(name: phase, parameters: parameters); + if (_capabilities.hasCapability(AppEventCapabilities.guru)) { + guruLogEvent(name: phase, parameters: parameters); + } + transmit(phase, parameters); + } + + static logShare({ + required String contentType, + required String itemId, + required String method, + }) { + final parameters = filterOutNulls({ + FirebaseEventsParams.CONTENT_TYPE: contentType, + FirebaseEventsParams.ITEM_ID: itemId, + FirebaseEventsParams.METHOD: method, + }); + if (dumpLog) { + print("[firebase] logEvent: share $parameters"); + } + analytics.logShare(contentType: contentType, itemId: itemId, method: method); + facebookLogEvent( + name: "share", + parameters: parameters, + ); + if (_capabilities.hasCapability(AppEventCapabilities.guru)) { + guruLogEvent(name: "share", parameters: parameters); + } + transmit("share", parameters); + } + + static logAdImpression({ + String? adPlatform, + String? adSource, + String? adFormat, + String? adUnitName, + double? value, + String? currency, + }) { + final parameters = _filterOutNulls({ + FirebaseEventsParams.AD_SOURCE: adSource, + FirebaseEventsParams.AD_FORMAT: adFormat, + FirebaseEventsParams.AD_UNIT_NAME: adUnitName, + FirebaseEventsParams.VALUE: value, + FirebaseEventsParams.CURRENCY: currency + }); + + if (dumpLog) { + print("[firebase] logAdImpression $parameters"); + } + analytics.logAdImpression( + adPlatform: adPlatform, + adSource: adSource, + adFormat: adFormat, + adUnitName: adUnitName, + value: value, + currency: currency); + + if (_capabilities.hasCapability(AppEventCapabilities.guru)) { + guruLogEvent(name: "ad_impression", parameters: parameters); + } + transmit("ad_impression", parameters); + } + + static Future setGuruUserProperty(String name, String value) async { + await guru.setUserProperty(name, value); + } +} diff --git a/guru_app/plugins/guru_analytics_flutter/lib/event_logger_common.dart b/guru_app/plugins/guru_analytics_flutter/lib/event_logger_common.dart new file mode 100644 index 0000000..26207a4 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/lib/event_logger_common.dart @@ -0,0 +1,23 @@ +// import 'package:facebook_app_events/facebook_app_events.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:firebase_analytics/observer.dart'; +import 'events_constants.dart'; +import 'package:flutter/widgets.dart'; + +typedef EventHook = void Function(String name, Map parameters); + +class EventTransmitter { + final Map hooks; + final EventHook? defaultHook; + + EventTransmitter(this.hooks, {this.defaultHook}); + + void transmit(String name, Map parameters) { + if (hooks.isNotEmpty == true) { + final hook = hooks[name] ?? defaultHook; + hook?.call(name, parameters); + } else { + defaultHook?.call(name, parameters); + } + } +} \ No newline at end of file diff --git a/guru_app/plugins/guru_analytics_flutter/lib/events_constants.dart b/guru_app/plugins/guru_analytics_flutter/lib/events_constants.dart new file mode 100644 index 0000000..d30691d --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/lib/events_constants.dart @@ -0,0 +1,121 @@ +// import 'package:facebook_app_events/facebook_app_events.dart'; + +class Method { + static const initialize = "initialize"; + static const getAppInstanceId = "getAppInstanceId"; + static const logEvent = "logEvent"; + static const setUserProperty = "setUserProperty"; + static const setDeviceId = "setDeviceId"; + static const setUid = "setUid"; + static const setAdjustId = "setAdjustId"; + static const setAdId = "setAdId"; + static const setFirebaseId = "setFirebaseId"; + static const setScreen = "setScreen"; + static const zipLogs = "zipLogs"; + static const getStatistic = "getStatistic"; + static const onAnalyticsCallback = "onAnalyticsCallback"; +} + +class FieldName { + static const batchLimit = "batchLimit"; + static const uploadPeriodInSeconds = "uploadPeriodInSeconds"; + static const delayedInSeconds = "delayedInSeconds"; + static const eventExpiredInDays = "eventExpiredInDays"; + static const DEVICE_ID = "deviceId"; + static const USER_ID = "userId"; + static const ADJUST_ID = "adjustId"; + static const AD_ID = "adId"; + static const FIREBASE_ID = "firebaseId"; + static const EVENT_NAME = "eventName"; + static const PARAMETERS = "parameters"; + static const PRIORITY = "priority"; + static const ENABLED = "enabled"; + static const MILLISECONDS = "milliseconds"; + static const NAME = "name"; + static const VALUE = "value"; + static const SCREEN = "screen"; + static const DEBUG = "debug"; + static const X_APP_ID = "xAppId"; + static const X_DEVICE_INFO = "xDeviceInfo"; + static const EVENT_CODE = "code"; + static const EVENT_ERROR_INFO = "errorInfo"; +} + +class AdTypeName { + static const String AD_TYPE_BANNER = "banner"; + static const String AD_TYPE_INTERSTITIAL = "interstitial"; + static const String AD_TYPE_REWARDED_VIDEO = "rewarded_video"; + static const String AD_TYPE_NATIVE = "native"; +} + +class FacebookAppEventsConstants { + static const String EVENT_NAME_AD_CLICK = "AdClick"; + static const String EVENT_NAME_AD_IMPRESSION = "AdImpression"; + static const String EVENT_NAME_COMPLETED_TUTORIAL = "fb_mobile_tutorial_completion"; + static const String EVENT_NAME_SPENT_CREDITS = "fb_mobile_spent_credits"; + static const String EVENT_NAME_ACHIEVED_LEVEL = "fb_mobile_level_achieved"; + static const String EVENT_NAME_PURCHASED = "fb_mobile_purchase"; + static const String EVENT_NAME_ACHIEVEMENT_UNLOCKED = "fb_mobile_achievement_unlocked"; + static const String EVENT_NAME_RATE = "fb_mobile_rate"; + static const String EVENT_NAME_SEARCH = "fb_mobile_search"; + static const String EVENT_NAME_ADD_TO_WISHLIST = "fb_mobile_add_to_wishlist"; + static const String EVENT_NAME_ADD_TO_CART = "fb_mobile_add_to_cart"; + static const String EVENT_NAME_CONTENT_VIEW = "fb_mobile_content_view"; + + static const String EVENT_PARAM_AD_TYPE = "ad_type"; + static const String EVENT_PARAM_CONTENT = "fb_content"; + static const String EVENT_PARAM_DESCRIPTION = "fb_description"; + static const String EVENT_PARAM_CONTENT_ID = "fb_content_id"; + static const String EVENT_PARAM_CONTENT_TYPE = "fb_content_type"; + static const String EVENT_PARAM_SEARCH_STRING = "fb_search_string"; + static const String EVENT_PARAM_MAX_RATING_VALUE = "fb_max_rating_value"; + static const String EVENT_PARAM_SUCCESS = "fb_success"; + static const String EVENT_PARAM_LEVEL = "fb_level"; + static const String EVENT_PARAM_CURRENCY = "fb_currency"; +} + +class FirebaseEventsParams { + static const String AD_FORMAT = "ad_format"; + static const String AD_PLATFORM = "ad_platform"; + static const String AD_SOURCE = "ad_source"; + static const String AD_UNIT_NAME = "ad_unit_name"; + static const String CURRENCY = "currency"; + static const String VALUE = "value"; + + /// Type of content selected. + static const String CONTENT_TYPE = 'content_type'; + static const String ITEM_ID = 'item_id'; + static const String METHOD = 'method'; +} + +class AppEventCapabilities { + static const int firebase = 0x01; + static const int facebook = 0x02; + static const int guru = 0x04; + + static const fullCapabilities = AppEventCapabilities(firebase | facebook | guru); + static const firebaseCapabilities = AppEventCapabilities(firebase); + static const facebookCapabilities = AppEventCapabilities(facebook); + static const standardCapabilities = AppEventCapabilities(firebase | facebook); + + final int value; + + const AppEventCapabilities(this.value); + + bool hasCapability(int capability) { + return (value & capability) == capability; + } +} + +typedef EventParamsConvertor = Map Function(Map parameters); + +class AppEventOptions { + final AppEventCapabilities? capabilities; + final EventParamsConvertor? firebaseParamsConvertor; + final EventParamsConvertor? facebookParamsConvertor; + final EventParamsConvertor? guruParamsConvertor; + final EventParamsConvertor? adjustParamsConvertor; + + AppEventOptions( + {this.capabilities, this.firebaseParamsConvertor, this.facebookParamsConvertor, this.guruParamsConvertor, this.adjustParamsConvertor}); +} \ No newline at end of file diff --git a/guru_app/plugins/guru_analytics_flutter/lib/guru/guru_event_logger.dart b/guru_app/plugins/guru_analytics_flutter/lib/guru/guru_event_logger.dart new file mode 100644 index 0000000..9b32711 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/lib/guru/guru_event_logger.dart @@ -0,0 +1,218 @@ +import 'package:flutter/services.dart'; +import 'dart:io'; +import 'package:guru_analytics_flutter/events_constants.dart'; +import 'guru_statistic.dart'; + +/// Created by Haoyi on 2022/11/25 + +class EventPriority { + static const EMERGENCE = 0; + static const HIGH = 5; + static const DEFAULT = 10; + static const LOW = 15; +} + +class AnalyticsCode { + static const STATE_INITIALIZED = 1; // SDK 初始化完成 + static const STATE_START_WORK = 2; // 准备完毕, 等待条件满足开始上报 + static const INIT_DEVICE_INFO = 11; // deviceInfo 设置完成 + static const DELETE_EXPIRED = 12; // 删除过期事件 + static const UPLOAD_SUCCESS = 13; // 上报事件成功 + static const UPLOAD_FAIL = 14; // 上报事件失败 + static const PERIODIC_WORK_ENQUEUE = 15; // 开启PeriodicWork + static const NETWORK_AVAILABLE = 21; // 网络状态可用 + static const NETWORK_LOST = 22; // 网络状态不可用 + static const LIFECYCLE_START = 23; // app可见 + static const LIFECYCLE_PAUSE = 24; // app不可见 + static const ERROR_API = 101; // 调用api出错 + static const ERROR_RESPONSE = 102; // api返回结果错误 + static const ERROR_CACHE_CONTROL = 103; // 设置cacheControl出错 + static const ERROR_DELETE_EXPIRED = 104; // 删除过期事件出错 + static const ERROR_LOAD_MARK = 105; // 从数据库取事件以及更改事件状态为正在上报出错 + static const ERROR_DNS = 106; // dns 错误 + static const EVENT_FIRST_OPEN = 1001; // first_open 事件 + static const EVENT_FG = 1002; // fg 事件 +} + +typedef PriorityGetter = int Function(String name, Map parameters); + +typedef AnalyticsCallback = void Function(int eventCode, String? errorInfo); + +class GuruEventLogger { + static const MethodChannel _channel = MethodChannel( + 'flutter.guru.guru_analytics_flutter'); + + static final RegExp _nonAlphaNumeric = RegExp('[^a-zA-Z0-9_]'); + static final RegExp _alpha = RegExp('[a-zA-Z]'); + + static GuruEventLogger instance = GuruEventLogger._(); + + static bool _initialized = false; + + PriorityGetter _priorityGetter = _defaultPriorityGetter; + + AnalyticsCallback _analyticsCallback = (eventCode, errorInfo) {}; + + GuruEventLogger._(); + + Future getAppInstanceId() async { + return await _channel.invokeMethod(Method.getAppInstanceId); + } + + static int _defaultPriorityGetter(String name, + Map parameters) { + return EventPriority.DEFAULT; + } + + Future initialize( + {required String appId, required String deviceInfo, int batchLimit = 32, + int uploadPeriodInSeconds = 60, int delayedInSeconds = 10, + int eventExpiredInDays = 7, bool debug = false, + AnalyticsCallback? callback}) async { + if (!_initialized) { + if (callback != null) { + _analyticsCallback = callback; + } + _channel.setMethodCallHandler((call) => processMethodCall(call)); + _initialized = await _channel.invokeMethod( + Method.initialize, { + FieldName.batchLimit: batchLimit, + FieldName.uploadPeriodInSeconds: uploadPeriodInSeconds, + FieldName.delayedInSeconds: delayedInSeconds, + FieldName.eventExpiredInDays: eventExpiredInDays, + FieldName.DEBUG: debug, + FieldName.X_APP_ID: appId, + FieldName.X_DEVICE_INFO: deviceInfo + }) ?? false; + } + } + + Future processMethodCall(MethodCall methodCall) async { + switch (methodCall.method) { + case Method.onAnalyticsCallback: + _analyticsCallback.call( + methodCall.arguments[FieldName.EVENT_CODE] as int, + methodCall.arguments[FieldName.EVENT_ERROR_INFO]); + break; + default: + return; + } + } + + void setPriorityGetter(PriorityGetter priorityGetter) { + _priorityGetter = priorityGetter; + } + + Future logEvent(String eventName, + {String? itemCategory, String? itemName, num? value, int? priority, Map< + String, + dynamic>? parameters}) { + if (eventName.isEmpty || + eventName.length > 128 || + eventName.indexOf(_alpha) != 0 || + eventName.contains(_nonAlphaNumeric)) { + throw ArgumentError.value( + eventName, + 'name', + 'must contain 1 to 128 alphanumeric characters.', + ); + } + + final Map mergedParams = { + "itemCategory": itemCategory, + "itemName": itemName, + "value": value, + }; + if (parameters != null) { + mergedParams.addAll(parameters); + } + + return _channel.invokeMethod( + Method.logEvent, { + FieldName.EVENT_NAME: eventName, + FieldName.PARAMETERS: _filterOutNulls(mergedParams), + FieldName.PRIORITY: priority ?? _priorityGetter(eventName, mergedParams) + }); + } + + Future setUserProperty(String name, String value) { + if (name.isEmpty || + name.length > 24 || + name.indexOf(_alpha) != 0 || + name.contains(_nonAlphaNumeric)) { + throw ArgumentError.value( + name, + 'name', + 'must contain 1 to 24 alphanumeric characters.', + ); + } + return _channel.invokeMethod( + Method.setUserProperty, + {FieldName.NAME: name, FieldName.VALUE: value}); + } + + Future setDeviceId(String deviceId) { + return _channel + .invokeMethod( + Method.setDeviceId, {FieldName.DEVICE_ID: deviceId}); + } + + Future setUserId(String uid) { + return _channel.invokeMethod( + Method.setUid, {FieldName.USER_ID: uid}); + } + + Future setAdjustId(String adjustId) { + return _channel + .invokeMethod( + Method.setAdjustId, {FieldName.ADJUST_ID: adjustId}); + } + + Future setAdId(String adId) { + return _channel.invokeMethod( + Method.setAdId, {FieldName.AD_ID: adId}); + } + + Future setFirebaseId(String firebaseId) { + return _channel + .invokeMethod(Method.setFirebaseId, + {FieldName.FIREBASE_ID: firebaseId}); + } + + Future setScreen(String screenName) { + return _channel + .invokeMethod( + Method.setScreen, {FieldName.SCREEN: screenName}); + } + + Future zipLogs() async { + final result = await _channel.invokeMethod( + Method.zipLogs, {}); + if (result is String) { + if (Platform.isAndroid) { + return result; + } else { + final uri = Uri.parse(result); + return uri.path; + } + } + return ""; + } + + Future getStatistic() async { + final json = await _channel.invokeMethod( + Method.getStatistic, {}); + return GuruStatistic.fromJson(json); + } + + static Map _filterOutNulls(Map parameters) { + final Map filtered = {}; + parameters.forEach((String key, dynamic value) { + if (value != null) { + filtered[key] = value; + } + }); + return filtered; + } +} diff --git a/guru_app/plugins/guru_analytics_flutter/lib/guru/guru_statistic.dart b/guru_app/plugins/guru_analytics_flutter/lib/guru/guru_statistic.dart new file mode 100644 index 0000000..57f7766 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/lib/guru/guru_statistic.dart @@ -0,0 +1,40 @@ +/// Created by Haoyi on 2023/2/15 + +class GuruStatistic { + + static const loggedField = "logged"; + static const uploadedField = "uploaded"; + + final int logged; + final int uploaded; + + const GuruStatistic({required this.logged, required this.uploaded}); + + static const GuruStatistic invalid = GuruStatistic(logged: 0, uploaded: 0); + + factory GuruStatistic.fromJson(Map json) => + GuruStatistic( + logged: json[loggedField] as int? ?? 0, + uploaded: json[uploadedField] as int? ?? 0, + ); + + Map toJson(GuruStatistic instance) => + { + loggedField: instance.logged, + uploadedField: instance.uploaded + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is GuruStatistic && runtimeType == other.runtimeType && logged == other.logged && + uploaded == other.uploaded; + + @override + int get hashCode => logged.hashCode ^ uploaded.hashCode; + + @override + String toString() { + return 'GuruStatistic{logged: $logged, uploaded: $uploaded}'; + } +} \ No newline at end of file diff --git a/guru_app/plugins/guru_analytics_flutter/lib/guru_analytics_flutter.dart b/guru_app/plugins/guru_analytics_flutter/lib/guru_analytics_flutter.dart new file mode 100644 index 0000000..8546d50 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/lib/guru_analytics_flutter.dart @@ -0,0 +1,7 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; + +class GuruAnalyticsFlutter { + +} diff --git a/guru_app/plugins/guru_analytics_flutter/pubspec.lock b/guru_app/plugins/guru_analytics_flutter/pubspec.lock new file mode 100644 index 0000000..bb686b7 --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/pubspec.lock @@ -0,0 +1,229 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.12" + async: + dependency: transitive + description: + name: async + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.9.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.16.0" + facebook_app_events: + dependency: "direct main" + description: + name: facebook_app_events + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.19.0" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + firebase_analytics: + dependency: "direct main" + description: + name: firebase_analytics + url: "https://pub.flutter-io.cn" + source: hosted + version: "10.1.0" + firebase_analytics_platform_interface: + dependency: transitive + description: + name: firebase_analytics_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.3.17" + firebase_analytics_web: + dependency: transitive + description: + name: firebase_analytics_web + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.1+8" + firebase_core: + dependency: transitive + description: + name: firebase_core + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.5.2" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + js: + dependency: transitive + description: + name: js + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.4" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.12" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.2" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.12" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" +sdks: + dart: ">=2.17.0-0 <3.0.0" + flutter: ">=2.5.0" diff --git a/guru_app/plugins/guru_analytics_flutter/pubspec.yaml b/guru_app/plugins/guru_analytics_flutter/pubspec.yaml new file mode 100644 index 0000000..a952ccb --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/pubspec.yaml @@ -0,0 +1,70 @@ +name: guru_analytics_flutter +description: Guru Analytics Library +version: 2.0.0 +homepage: + +environment: + sdk: ">=2.16.2 <3.0.0" + flutter: ">=2.5.0" + +dependencies: + flutter: + sdk: flutter + + firebase_analytics: 10.7.4 + # facebook_analytics_plugin: ^0.0.1+8 + facebook_app_events: 0.19.0 + + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^1.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' and Android 'package' identifiers should not ordinarily + # be modified. They are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: guru.core.flutter.analytics.guru_analytics_flutter + pluginClass: GuruAnalyticsFlutterPlugin + ios: + pluginClass: GuruAnalyticsFlutterPlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/guru_app/plugins/guru_analytics_flutter/test/guru_analytics_flutter_test.dart b/guru_app/plugins/guru_analytics_flutter/test/guru_analytics_flutter_test.dart new file mode 100644 index 0000000..d54d85b --- /dev/null +++ b/guru_app/plugins/guru_analytics_flutter/test/guru_analytics_flutter_test.dart @@ -0,0 +1,23 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:guru_analytics_flutter/guru_analytics_flutter.dart'; + +void main() { + const MethodChannel channel = MethodChannel('guru_analytics_flutter'); + + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return '42'; + }); + }); + + tearDown(() { + channel.setMockMethodCallHandler(null); + }); + + test('getPlatformVersion', () async { + expect(await GuruAnalyticsFlutter.platformVersion, '42'); + }); +} diff --git a/guru_app/plugins/guru_applifecycle_flutter/.gitignore b/guru_app/plugins/guru_applifecycle_flutter/.gitignore new file mode 100644 index 0000000..6021f66 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/.gitignore @@ -0,0 +1,290 @@ +# Created by https://www.toptal.com/developers/gitignore/api/fastlane,android,androidstudio,xcode,flutter,gradle +# Edit at https://www.toptal.com/developers/gitignore?templates=fastlane,android,androidstudio,xcode,flutter,gradle + +### Android ### +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Log/OS Files +*.log + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.apk +output.json + +# IntelliJ +*.iml +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Android Profiling +*.hprof + +### Android Patch ### +gen-external-apklibs + +# Replacement of .externalNativeBuild directories introduced +# with Android Studio 3.5. + +### fastlane ### +# fastlane - A streamlined workflow tool for Cocoa deployment +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +# fastlane specific +fastlane/report.xml + +# deliver temporary files +fastlane/Preview.html + +# snapshot generated screenshots +fastlane/screenshots/**/*.png +fastlane/screenshots/screenshots.html + +# scan temporary files +fastlane/test_output + +# Fastlane.swift runner binary +fastlane/FastlaneRunner + +### Flutter ### +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.fvm/ +.packages +.pub-cache/ +.pub/ +coverage/ +lib/generated_plugin_registrant.dart +# For library packages, don’t commit the pubspec.lock file. +# Regenerating the pubspec.lock file lets you test your package against the latest compatible versions of its dependencies. +# See https://dart.dev/guides/libraries/private-files#pubspeclock +#pubspec.lock + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/key.properties +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/.last_build_id +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages + +### Xcode ### +## User settings +xcuserdata/ + +## Xcode 8 and earlier +*.xcscmblueprint +*.xccheckout + +### Xcode Patch ### +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcworkspace/contents.xcworkspacedata +/*.gcno +**/xcshareddata/WorkspaceSettings.xcsettings + +### Gradle ### +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### AndroidStudio ### +# Covers files to be ignored for android development using Android Studio. + +# Built application files +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files + +# Signing files +.signing/ + +# Local configuration file (sdk path, etc) + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files + +# Android Studio +/*/build/ +/*/local.properties +/*/out +/*/*/build +/*/*/production +.navigation/ +*.ipr +*~ +*.swp + +# Keystore files + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Android Patch + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# NDK +obj/ + +# IntelliJ IDEA +*.iws +/out/ + +# User-specific configurations +.idea/caches/ +.idea/libraries/ +.idea/shelf/ +.idea/workspace.xml +.idea/tasks.xml +.idea/.name +.idea/compiler.xml +.idea/copyright/profiles_settings.xml +.idea/encodings.xml +.idea/misc.xml +.idea/modules.xml +.idea/scopes/scope_settings.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml +.idea/datasources.xml +.idea/dataSources.ids +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml +.idea/assetWizardSettings.xml +.idea/gradle.xml +.idea/jarRepositories.xml +.idea/navEditor.xml + +# Legacy Eclipse project files +.cproject +.settings/ + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.war +*.ear + +# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) +hs_err_pid* + +## Plugin-specific files: + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Mongo Explorer plugin +.idea/mongoSettings.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### AndroidStudio Patch ### + +!/gradle/wrapper/gradle-wrapper.jar + +# End of https://www.toptal.com/developers/gitignore/api/fastlane,android,androidstudio,xcode,flutter,gradle diff --git a/guru_app/plugins/guru_applifecycle_flutter/CHANGELOG.md b/guru_app/plugins/guru_applifecycle_flutter/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/guru_app/plugins/guru_applifecycle_flutter/LICENSE b/guru_app/plugins/guru_applifecycle_flutter/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/guru_app/plugins/guru_applifecycle_flutter/README.md b/guru_app/plugins/guru_applifecycle_flutter/README.md new file mode 100644 index 0000000..c39b685 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/README.md @@ -0,0 +1,15 @@ +# guru_applifecycle_flutter + +A new flutter plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/developing-packages/), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/guru_app/plugins/guru_applifecycle_flutter/analysis_options.yaml b/guru_app/plugins/guru_applifecycle_flutter/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/guru_app/plugins/guru_applifecycle_flutter/android/.gitignore b/guru_app/plugins/guru_applifecycle_flutter/android/.gitignore new file mode 100644 index 0000000..c6cbe56 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/android/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/guru_app/plugins/guru_applifecycle_flutter/android/build.gradle b/guru_app/plugins/guru_applifecycle_flutter/android/build.gradle new file mode 100644 index 0000000..9b075f8 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/android/build.gradle @@ -0,0 +1,76 @@ +group 'flutter.guru.guru_applifecycle_flutter' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 31 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + minSdkVersion 16 + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.lifecycle:lifecycle-process:2.2.0' +} + +afterEvaluate { + def containsEmbeddingDependencies = false + for (def configuration : configurations.all) { + for (def dependency : configuration.dependencies) { + if (dependency.group == 'io.flutter' && + dependency.name.startsWith('flutter_embedding') && + dependency.isTransitive()) + { + containsEmbeddingDependencies = true + break + } + } + } + if (!containsEmbeddingDependencies) { + android { + dependencies { + def lifecycle_version = "1.1.1" + compileOnly "android.arch.lifecycle:runtime:$lifecycle_version" + compileOnly "android.arch.lifecycle:common:$lifecycle_version" + compileOnly "android.arch.lifecycle:common-java8:$lifecycle_version" + } + } + } +} diff --git a/guru_app/plugins/guru_applifecycle_flutter/android/settings.gradle b/guru_app/plugins/guru_applifecycle_flutter/android/settings.gradle new file mode 100644 index 0000000..e47ebaf --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'guru_applifecycle_flutter' diff --git a/guru_app/plugins/guru_applifecycle_flutter/android/src/main/AndroidManifest.xml b/guru_app/plugins/guru_applifecycle_flutter/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..27a0a12 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/guru_app/plugins/guru_applifecycle_flutter/android/src/main/kotlin/flutter/guru/guru_applifecycle_flutter/AppStateNotifier.java b/guru_app/plugins/guru_applifecycle_flutter/android/src/main/kotlin/flutter/guru/guru_applifecycle_flutter/AppStateNotifier.java new file mode 100644 index 0000000..6c1cfca --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/android/src/main/kotlin/flutter/guru/guru_applifecycle_flutter/AppStateNotifier.java @@ -0,0 +1,95 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package flutter.guru.guru_applifecycle_flutter; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle.Event; +import androidx.lifecycle.LifecycleEventObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.ProcessLifecycleOwner; + +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.EventChannel.EventSink; +import io.flutter.plugin.common.EventChannel.StreamHandler; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; + +/** Listens to changes in app foreground/background and forwards events to Flutter. */ +final class AppStateNotifier implements LifecycleEventObserver, MethodCallHandler, StreamHandler { + + private static final String METHOD_CHANNEL_NAME = + "guru_applifecycle_flutter/app_state_method"; + private static final String EVENT_CHANNEL_NAME = + "guru_applifecycle_flutter/app_state_event"; + + @NonNull private final MethodChannel methodChannel; + @NonNull private final EventChannel eventChannel; + + @Nullable private EventSink events; + + AppStateNotifier(BinaryMessenger binaryMessenger) { + methodChannel = new MethodChannel(binaryMessenger, METHOD_CHANNEL_NAME); + methodChannel.setMethodCallHandler(this); + eventChannel = new EventChannel(binaryMessenger, EVENT_CHANNEL_NAME); + eventChannel.setStreamHandler(this); + } + + /** Starts listening for app lifecycle changes. */ + void start() { + ProcessLifecycleOwner.get().getLifecycle().addObserver(this); + } + + /** Stops listening for app lifecycle changes. */ + void stop() { + ProcessLifecycleOwner.get().getLifecycle().removeObserver(this); + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { + switch (call.method) { + case "start": + start(); + break; + case "stop": + stop(); + break; + default: + result.notImplemented(); + } + } + + @Override + public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Event event) { + if (event == Event.ON_START && events != null) { + events.success("foreground"); + } else if (event == Event.ON_STOP && events != null) { + events.success("background"); + } + } + + @Override + public void onListen(Object arguments, EventSink events) { + this.events = events; + } + + @Override + public void onCancel(Object arguments) { + this.events = null; + } +} diff --git a/guru_app/plugins/guru_applifecycle_flutter/android/src/main/kotlin/flutter/guru/guru_applifecycle_flutter/GuruApplifecycleFlutterPlugin.kt b/guru_app/plugins/guru_applifecycle_flutter/android/src/main/kotlin/flutter/guru/guru_applifecycle_flutter/GuruApplifecycleFlutterPlugin.kt new file mode 100644 index 0000000..c3d1a28 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/android/src/main/kotlin/flutter/guru/guru_applifecycle_flutter/GuruApplifecycleFlutterPlugin.kt @@ -0,0 +1,42 @@ +package flutter.guru.guru_applifecycle_flutter + +import androidx.annotation.NonNull + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +/** GuruApplifecycleFlutterPlugin */ +class GuruApplifecycleFlutterPlugin: FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel : MethodChannel + + private var appStateNotifier: AppStateNotifier? = null + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + appStateNotifier = AppStateNotifier(flutterPluginBinding.binaryMessenger) + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "guru_applifecycle_flutter") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + if (call.method == "getPlatformVersion") { + result.success("Android ${android.os.Build.VERSION.RELEASE}") + } else { + result.notImplemented() + } + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + if (appStateNotifier != null) { + appStateNotifier!!.stop() + appStateNotifier = null + } + channel.setMethodCallHandler(null) + } +} diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/.gitignore b/guru_app/plugins/guru_applifecycle_flutter/example/.gitignore new file mode 100644 index 0000000..0fa6b67 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/README.md b/guru_app/plugins/guru_applifecycle_flutter/example/README.md new file mode 100644 index 0000000..64c08cb --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/README.md @@ -0,0 +1,16 @@ +# guru_applifecycle_flutter_example + +Demonstrates how to use the guru_applifecycle_flutter plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/analysis_options.yaml b/guru_app/plugins/guru_applifecycle_flutter/example/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/android/.gitignore b/guru_app/plugins/guru_applifecycle_flutter/example/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/android/app/build.gradle b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/build.gradle new file mode 100644 index 0000000..82951f0 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/build.gradle @@ -0,0 +1,68 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion flutter.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "flutter.guru.guru_applifecycle_flutter_example" + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/debug/AndroidManifest.xml b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..b825184 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/AndroidManifest.xml b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..345cb99 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/kotlin/flutter/guru/guru_applifecycle_flutter_example/MainActivity.kt b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/kotlin/flutter/guru/guru_applifecycle_flutter_example/MainActivity.kt new file mode 100644 index 0000000..8174eb3 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/kotlin/flutter/guru/guru_applifecycle_flutter_example/MainActivity.kt @@ -0,0 +1,6 @@ +package flutter.guru.guru_applifecycle_flutter_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/drawable-v21/launch_background.xml b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/drawable/launch_background.xml b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/values-night/styles.xml b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..3db14bb --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/values/styles.xml b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..d460d1e --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/profile/AndroidManifest.xml b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..b825184 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/android/build.gradle b/guru_app/plugins/guru_applifecycle_flutter/example/android/build.gradle new file mode 100644 index 0000000..4256f91 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/android/gradle.properties b/guru_app/plugins/guru_applifecycle_flutter/example/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/android/gradle/wrapper/gradle-wrapper.properties b/guru_app/plugins/guru_applifecycle_flutter/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..bc6a58a --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/android/settings.gradle b/guru_app/plugins/guru_applifecycle_flutter/example/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/.gitignore b/guru_app/plugins/guru_applifecycle_flutter/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Flutter/AppFrameworkInfo.plist b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..8d4492f --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 9.0 + + diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Flutter/Debug.xcconfig b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Flutter/Release.xcconfig b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Podfile b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Podfile new file mode 100644 index 0000000..1e8c3c9 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Podfile.lock b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Podfile.lock new file mode 100644 index 0000000..78292b1 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Podfile.lock @@ -0,0 +1,35 @@ +PODS: + - Flutter (1.0.0) + - fluttertoast (0.0.2): + - Flutter + - Toast + - guru_applifecycle_flutter (0.0.1): + - Flutter + - Toast (4.0.0) + +DEPENDENCIES: + - Flutter (from `Flutter`) + - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) + - guru_applifecycle_flutter (from `.symlinks/plugins/guru_applifecycle_flutter/ios`) + +SPEC REPOS: + trunk: + - Toast + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + fluttertoast: + :path: ".symlinks/plugins/fluttertoast/ios" + guru_applifecycle_flutter: + :path: ".symlinks/plugins/guru_applifecycle_flutter/ios" + +SPEC CHECKSUMS: + Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a + fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037 + guru_applifecycle_flutter: 8ba78cca8be03f71bb0d9e41a7f20c688a822eb5 + Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 + +PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c + +COCOAPODS: 1.11.3 diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..a7f7102 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,552 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + E89497BD1F9A77C94DD3F858 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B865FAF70000C7461804DF78 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2E7E3FFB768FA6047D789A8A /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 79CA6A0AD8D248C2F71DD886 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B865FAF70000C7461804DF78 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E47080C94127BA495653963F /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E89497BD1F9A77C94DD3F858 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 34B3EDCB0D6D3AD99650973F /* Pods */ = { + isa = PBXGroup; + children = ( + 2E7E3FFB768FA6047D789A8A /* Pods-Runner.debug.xcconfig */, + 79CA6A0AD8D248C2F71DD886 /* Pods-Runner.release.xcconfig */, + E47080C94127BA495653963F /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 34B3EDCB0D6D3AD99650973F /* Pods */, + EFBD1033266F4FF0E6823575 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + EFBD1033266F4FF0E6823575 /* Frameworks */ = { + isa = PBXGroup; + children = ( + B865FAF70000C7461804DF78 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 1C275EDCF10F6BD804BA4780 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 08F44223ECA8011332A13F00 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 08F44223ECA8011332A13F00 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 1C275EDCF10F6BD804BA4780 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 39253T242A; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = flutter.guru.guruApplifecycleFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 39253T242A; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = flutter.guru.guruApplifecycleFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 39253T242A; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = flutter.guru.guruApplifecycleFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c87d15a --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/AppDelegate.swift b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..28c6bf0 Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..f091b6b Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..d0ef06e Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..c8f9ed8 Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..75b2d16 Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c4df70d Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Base.lproj/Main.storyboard b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Info.plist b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Info.plist new file mode 100644 index 0000000..6934e0a --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Guru Applifecycle Flutter + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + guru_applifecycle_flutter_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Runner-Bridging-Header.h b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/lib/main.dart b/guru_app/plugins/guru_applifecycle_flutter/example/lib/main.dart new file mode 100644 index 0000000..0c267f2 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/lib/main.dart @@ -0,0 +1,95 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:guru_applifecycle_flutter/guru_applifecycle_flutter.dart'; +import 'package:guru_applifecycle_flutter/lifecycle/app_lifecycle_callback.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State with AppLifecycleCallback { + String _platformVersion = 'Unknown'; + + @override + void initState() { + super.initState(); + listenApplifecycle(); + + // AppStateEventNotifier.startListening(); + // AppStateEventNotifier.appStateStream.listen((event) { + // if (kDebugMode) { + // print("@@@@$event"); + // if (Platform.isIOS) { + // Fluttertoast.showToast(msg: "appstate=$event", toastLength: Toast.LENGTH_LONG); + // } else { + // Fluttertoast.showToast(msg: "appstate=$event", toastLength: Toast.LENGTH_LONG); + // } + // } + // }); + initPlatformState(); + } + + // Platform messages are asynchronous, so we initialize in an async method. + Future initPlatformState() async { + String platformVersion; + // Platform messages may fail, so we use a try/catch PlatformException. + // We also handle the message potentially returning null. + try { + platformVersion = + await GuruApplifecycleFlutter.platformVersion ?? 'Unknown platform version'; + } on PlatformException { + platformVersion = 'Failed to get platform version.'; + } + + // If the widget was removed from the tree while the asynchronous platform + // message was in flight, we want to discard the reply rather than calling + // setState to update our non-existent appearance. + if (!mounted) return; + + setState(() { + _platformVersion = platformVersion; + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: Center( + child: Text('Running on: $_platformVersion\n'), + ), + ), + ); + } + + @override + void dispose() { + cancelListenApplifecycle(); + super.dispose(); + } + + @override + void onAppBackground() { + print("@@@@ background"); + Fluttertoast.showToast(msg: "appstate=background", toastLength: Toast.LENGTH_LONG); + } + + @override + void onAppForeground() { + print("@@@@ foreground"); + Fluttertoast.showToast(msg: "appstate=foreground", toastLength: Toast.LENGTH_LONG); + } +} diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/pubspec.lock b/guru_app/plugins/guru_applifecycle_flutter/example/pubspec.lock new file mode 100644 index 0000000..9ffb514 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/pubspec.lock @@ -0,0 +1,201 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.8.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.15.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.5" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.0.9" + guru_applifecycle_flutter: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" + js: + dependency: transitive + description: + name: js + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.3" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.3" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.7.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.8" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" +sdks: + dart: ">=2.16.2 <3.0.0" + flutter: ">=2.5.0" diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/pubspec.yaml b/guru_app/plugins/guru_applifecycle_flutter/example/pubspec.yaml new file mode 100644 index 0000000..9abff6c --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/pubspec.yaml @@ -0,0 +1,85 @@ +name: guru_applifecycle_flutter_example +description: Demonstrates how to use the guru_applifecycle_flutter plugin. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: ">=2.16.2 <3.0.0" + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + guru_applifecycle_flutter: + # When depending on this package from a real application you should use: + # guru_applifecycle_flutter: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + fluttertoast: ^8.0.9 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^1.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/guru_app/plugins/guru_applifecycle_flutter/example/test/widget_test.dart b/guru_app/plugins/guru_applifecycle_flutter/example/test/widget_test.dart new file mode 100644 index 0000000..eb50f88 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/example/test/widget_test.dart @@ -0,0 +1,27 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../lib/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && + widget.data!.startsWith('Running on:'), + ), + findsOneWidget, + ); + }); +} diff --git a/guru_app/plugins/guru_applifecycle_flutter/ios/.gitignore b/guru_app/plugins/guru_applifecycle_flutter/ios/.gitignore new file mode 100644 index 0000000..0c88507 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/guru_app/plugins/guru_applifecycle_flutter/ios/Assets/.gitkeep b/guru_app/plugins/guru_applifecycle_flutter/ios/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/guru_app/plugins/guru_applifecycle_flutter/ios/Classes/AppStateNotifier.h b/guru_app/plugins/guru_applifecycle_flutter/ios/Classes/AppStateNotifier.h new file mode 100644 index 0000000..eeedf0a --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/ios/Classes/AppStateNotifier.h @@ -0,0 +1,22 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +@interface AppStateNotifier : NSObject + +- (instancetype _Nonnull)initWithBinaryMessenger: + (NSObject *_Nonnull)messenger; + +@end diff --git a/guru_app/plugins/guru_applifecycle_flutter/ios/Classes/AppStateNotifier.m b/guru_app/plugins/guru_applifecycle_flutter/ios/Classes/AppStateNotifier.m new file mode 100644 index 0000000..feeb6b2 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/ios/Classes/AppStateNotifier.m @@ -0,0 +1,142 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "AppStateNotifier.h" + +@implementation AppStateNotifier { + FlutterEventChannel *_eventChannel; + FlutterMethodChannel *_methodChannel; + FlutterEventSink _events; + NSMutableArray> *_observers; + BOOL _applicationInBackground; +} + +- (instancetype _Nonnull)initWithBinaryMessenger: + (NSObject *_Nonnull)messenger { + self = [self init]; + if (self) { + AppStateNotifier *__weak weakSelf = self; + _observers = [[NSMutableArray alloc] init]; + _eventChannel = [FlutterEventChannel + eventChannelWithName: + @"guru_applifecycle_flutter/app_state_event" + binaryMessenger:messenger]; + _methodChannel = [FlutterMethodChannel + methodChannelWithName: + @"guru_applifecycle_flutter/app_state_method" + binaryMessenger:messenger]; + [_eventChannel setStreamHandler:self]; + [_methodChannel setMethodCallHandler:^(FlutterMethodCall *_Nonnull call, + FlutterResult _Nonnull result) { + [weakSelf handleMethodCall:call result:result]; + }]; + } + return self; +} + +- (void)handleMethodCall:(FlutterMethodCall *_Nonnull)call + result:(FlutterResult _Nonnull)result { + if ([call.method isEqualToString:@"start"]) { + [self addAppStateObservers]; + result(nil); + } else if ([call.method isEqualToString:@"stop"]) { + [self removeAppStateObservers]; + result(nil); + } else { + result(FlutterMethodNotImplemented); + } +} + +- (void)addAppStateObservers { + if (_observers.count > 0) { + NSLog(@"AppStateNotifier: Already listening for foreground/background " + @"changes."); + return; + } + + id foregroundObserver = [NSNotificationCenter.defaultCenter + addObserverForName:UIApplicationWillEnterForegroundNotification + object:nil + queue:nil + usingBlock:^(NSNotification *_Nonnull note) { + [self handleWillEnterForeground]; + }]; + [_observers addObject:foregroundObserver]; + + id backgroundObserver = [NSNotificationCenter.defaultCenter + addObserverForName:UIApplicationDidEnterBackgroundNotification + object:nil + queue:nil + usingBlock:^(NSNotification *_Nonnull note) { + [self handleDidEnterBackground]; + }]; + [_observers addObject:backgroundObserver]; + + if (@available(iOS 13.0, *)) { + id foregroundSceneObserver = [NSNotificationCenter.defaultCenter + addObserverForName:UISceneWillEnterForegroundNotification + object:nil + queue:nil + usingBlock:^(NSNotification *_Nonnull note) { + [self handleWillEnterForeground]; + }]; + [_observers addObject:foregroundSceneObserver]; + + id backgroundSceneObserver = [NSNotificationCenter.defaultCenter + addObserverForName:UISceneDidEnterBackgroundNotification + object:nil + queue:nil + usingBlock:^(NSNotification *_Nonnull note) { + [self handleDidEnterBackground]; + }]; + [_observers addObject:backgroundSceneObserver]; + } +} + +- (void)removeAppStateObservers { + while (_observers.count > 0) { + [NSNotificationCenter.defaultCenter removeObserver:_observers.lastObject]; + [_observers removeLastObject]; + } +} + +- (void)handleWillEnterForeground { + if (!_applicationInBackground) { + return; + } + _applicationInBackground = NO; + _events(@"foreground"); +} + +- (void)handleDidEnterBackground { + if (_applicationInBackground) { + return; + } + _applicationInBackground = YES; + _events(@"background"); +} + +- (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments { + _events = nil; + return nil; +} + +- (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments + eventSink: + (nonnull FlutterEventSink)events { + _events = events; + return nil; +} + +@end diff --git a/guru_app/plugins/guru_applifecycle_flutter/ios/Classes/GuruApplifecycleFlutterPlugin.h b/guru_app/plugins/guru_applifecycle_flutter/ios/Classes/GuruApplifecycleFlutterPlugin.h new file mode 100644 index 0000000..89a5546 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/ios/Classes/GuruApplifecycleFlutterPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface GuruApplifecycleFlutterPlugin : NSObject +@end diff --git a/guru_app/plugins/guru_applifecycle_flutter/ios/Classes/GuruApplifecycleFlutterPlugin.m b/guru_app/plugins/guru_applifecycle_flutter/ios/Classes/GuruApplifecycleFlutterPlugin.m new file mode 100644 index 0000000..c05f166 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/ios/Classes/GuruApplifecycleFlutterPlugin.m @@ -0,0 +1,39 @@ +#import "GuruApplifecycleFlutterPlugin.h" +#import "AppStateNotifier.h" + +@implementation GuruApplifecycleFlutterPlugin { + AppStateNotifier *_appStateNotifier; +} ++ (void)registerWithRegistrar:(NSObject*)registrar { + FlutterMethodChannel* channel = [FlutterMethodChannel + methodChannelWithName:@"guru_applifecycle_flutter" + binaryMessenger:[registrar messenger]]; + GuruApplifecycleFlutterPlugin* instance = [[GuruApplifecycleFlutterPlugin alloc] initWithBinaryMessenger:registrar.messenger]; + [registrar addMethodCallDelegate:instance channel:channel]; +} + +- (instancetype)init { + self = [super init]; + return self; +} + +- (instancetype)initWithBinaryMessenger: + (id)binaryMessenger { + self = [self init]; + if (self) { + _appStateNotifier = + [[AppStateNotifier alloc] initWithBinaryMessenger:binaryMessenger]; + } + + return self; +} + +- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { + if ([@"getPlatformVersion" isEqualToString:call.method]) { + result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]); + } else { + result(FlutterMethodNotImplemented); + } +} + +@end diff --git a/guru_app/plugins/guru_applifecycle_flutter/ios/guru_applifecycle_flutter.podspec b/guru_app/plugins/guru_applifecycle_flutter/ios/guru_applifecycle_flutter.podspec new file mode 100644 index 0000000..c2ea236 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/ios/guru_applifecycle_flutter.podspec @@ -0,0 +1,25 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint guru_applifecycle_flutter.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'guru_applifecycle_flutter' + s.version = '0.0.1' + s.summary = 'A new flutter plugin project.' + s.description = <<-DESC +A new flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + s.platform = :ios, '9.0' + + # Flutter.framework does not contain a i386 slice. +# s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS' => 'armv7 arm64 x86_64' } + s.static_framework = true +end diff --git a/guru_app/plugins/guru_applifecycle_flutter/lib/guru_applifecycle.dart b/guru_app/plugins/guru_applifecycle_flutter/lib/guru_applifecycle.dart new file mode 100644 index 0000000..9388b55 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/lib/guru_applifecycle.dart @@ -0,0 +1,2 @@ +export 'lifecycle/app_background_event_notifier.dart'; +export 'lifecycle/app_lifecycle_callback.dart'; \ No newline at end of file diff --git a/guru_app/plugins/guru_applifecycle_flutter/lib/guru_applifecycle_flutter.dart b/guru_app/plugins/guru_applifecycle_flutter/lib/guru_applifecycle_flutter.dart new file mode 100644 index 0000000..26d082c --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/lib/guru_applifecycle_flutter.dart @@ -0,0 +1,13 @@ + +import 'dart:async'; + +import 'package:flutter/services.dart'; + +class GuruApplifecycleFlutter { + static const MethodChannel _channel = MethodChannel('guru_applifecycle_flutter'); + + static Future get platformVersion async { + final String? version = await _channel.invokeMethod('getPlatformVersion'); + return version; + } +} diff --git a/guru_app/plugins/guru_applifecycle_flutter/lib/lifecycle/app_background_event_notifier.dart b/guru_app/plugins/guru_applifecycle_flutter/lib/lifecycle/app_background_event_notifier.dart new file mode 100644 index 0000000..e633a3c --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/lib/lifecycle/app_background_event_notifier.dart @@ -0,0 +1,63 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; + +/// The app foreground/background state. +enum AppState { + /// App is backgrounded. + background, + + /// App is foregrounded. + foreground +} + +/// Notifies changes in app foreground/background. +/// +/// Subscribe to [appStateStream] to get notified of changes in [AppState]. +/// Use this when implementing app open ads instead of [WidgetsBindingObserver], +/// because the latter also notifies when the state of the Flutter +/// activity/view controller changes. This makes an interstitial taking control +/// of the screen indistinguishable from the app background/foreground. +class AppStateEventNotifier { + static const _eventChannelName = + 'guru_applifecycle_flutter/app_state_event'; + static const _methodChannelName = + 'guru_applifecycle_flutter/app_state_method'; + static const EventChannel _eventChannel = EventChannel(_eventChannelName); + static const MethodChannel _methodChannel = MethodChannel(_methodChannelName); + + static late final Stream _stateStream = + _eventChannel.receiveBroadcastStream().map((event) => + event == 'foreground' ? AppState.foreground : AppState.background); + + /// Subscribe to this to get notified of changes in [AppState]. + /// + /// Call [startListening] before subscribing to this stream to + /// start listening to background/foreground events on the platform side. + static Stream get appStateStream => _stateStream; + + /// Start listening to background/foreground events. + static Future startListening() async { + return _methodChannel.invokeMethod('start'); + } + + /// Stop listening to background/foreground events. + static Future stopListening() async { + return _methodChannel.invokeMethod('stop'); + } +} diff --git a/guru_app/plugins/guru_applifecycle_flutter/lib/lifecycle/app_lifecycle_callback.dart b/guru_app/plugins/guru_applifecycle_flutter/lib/lifecycle/app_lifecycle_callback.dart new file mode 100644 index 0000000..ef45b1a --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/lib/lifecycle/app_lifecycle_callback.dart @@ -0,0 +1,26 @@ +import 'dart:async'; + +import 'package:guru_applifecycle_flutter/lifecycle/app_background_event_notifier.dart'; + +mixin AppLifecycleCallback { + + StreamSubscription? _appStateSubscription; + + void listenApplifecycle() { + AppStateEventNotifier.startListening(); + _appStateSubscription = AppStateEventNotifier.appStateStream.listen((event) { + if (event == AppState.background) { + onAppBackground(); + } else { + onAppForeground(); + } + }); + } + + Future cancelListenApplifecycle() async { + await _appStateSubscription?.cancel(); + } + + void onAppBackground(); + void onAppForeground(); +} \ No newline at end of file diff --git a/guru_app/plugins/guru_applifecycle_flutter/pubspec.lock b/guru_app/plugins/guru_applifecycle_flutter/pubspec.lock new file mode 100644 index 0000000..bb44a0a --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/pubspec.lock @@ -0,0 +1,168 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.8.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.15.0" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.3" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.7.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.8" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" +sdks: + dart: ">=2.16.2 <3.0.0" + flutter: ">=2.5.0" diff --git a/guru_app/plugins/guru_applifecycle_flutter/pubspec.yaml b/guru_app/plugins/guru_applifecycle_flutter/pubspec.yaml new file mode 100644 index 0000000..5e53264 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/pubspec.yaml @@ -0,0 +1,65 @@ +name: guru_applifecycle_flutter +description: A new flutter plugin project. +version: 0.0.1 +homepage: + +environment: + sdk: ">=2.16.2 <3.0.0" + flutter: ">=2.5.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^1.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' and Android 'package' identifiers should not ordinarily + # be modified. They are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: flutter.guru.guru_applifecycle_flutter + pluginClass: GuruApplifecycleFlutterPlugin + ios: + pluginClass: GuruApplifecycleFlutterPlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/guru_app/plugins/guru_applifecycle_flutter/test/guru_applifecycle_flutter_test.dart b/guru_app/plugins/guru_applifecycle_flutter/test/guru_applifecycle_flutter_test.dart new file mode 100644 index 0000000..2892400 --- /dev/null +++ b/guru_app/plugins/guru_applifecycle_flutter/test/guru_applifecycle_flutter_test.dart @@ -0,0 +1,23 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:guru_applifecycle_flutter/guru_applifecycle_flutter.dart'; + +void main() { + const MethodChannel channel = MethodChannel('guru_applifecycle_flutter'); + + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return '42'; + }); + }); + + tearDown(() { + channel.setMockMethodCallHandler(null); + }); + + test('getPlatformVersion', () async { + expect(await GuruApplifecycleFlutter.platformVersion, '42'); + }); +} diff --git a/guru_app/plugins/guru_applovin_flutter/.gitignore b/guru_app/plugins/guru_applovin_flutter/.gitignore new file mode 100644 index 0000000..02c4c38 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ +.idea/ \ No newline at end of file diff --git a/guru_app/plugins/guru_applovin_flutter/CHANGELOG.md b/guru_app/plugins/guru_applovin_flutter/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/guru_app/plugins/guru_applovin_flutter/LICENSE b/guru_app/plugins/guru_applovin_flutter/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/guru_app/plugins/guru_applovin_flutter/README.md b/guru_app/plugins/guru_applovin_flutter/README.md new file mode 100644 index 0000000..6c87831 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/README.md @@ -0,0 +1,15 @@ +# guru_applovin_flutter + +A new flutter plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/developing-packages/), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/guru_app/plugins/guru_applovin_flutter/analysis_options.yaml b/guru_app/plugins/guru_applovin_flutter/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/guru_app/plugins/guru_applovin_flutter/android/.gitignore b/guru_app/plugins/guru_applovin_flutter/android/.gitignore new file mode 100644 index 0000000..c6cbe56 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/android/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/guru_app/plugins/guru_applovin_flutter/android/build.gradle b/guru_app/plugins/guru_applovin_flutter/android/build.gradle new file mode 100644 index 0000000..bb73062 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/android/build.gradle @@ -0,0 +1,133 @@ +group 'flutter.guru.guru_applovin_flutter' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.2.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + mavenLocal() + maven { url "https://android-sdk.is.com" } + maven { url "https://artifact.bytedance.com/repository/pangle" } + maven { url "https://s3.amazonaws.com/smaato-sdk-releases/" } + maven { url "https://artifactory.verizonmedia.com/artifactory/maven" } + maven { url "https://verve.jfrog.io/artifactory/verve-gradle-release" } + maven { url "https://dl-maven-android.mintegral.com/repository/mbridge_android_sdk_oversea" } + maven { url 'https://repo.pubmatic.com/artifactory/public-repos' } + maven { url "https://artifactory.bidmachine.io/bidmachine" } + maven { url "https://cboost.jfrog.io/artifactory/chartboost-ads/" } + maven { url "https://maven.ogury.co" } +// maven { url "https://artifactory.bidmachine.io/bidmachine" } + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 31 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + minSdkVersion 21 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + lintOptions { + disable 'InvalidPackage' + } +} + +dependencies { +// compileOnly files('libs/GuruConsent-1.0.0.aar') +// implementation 'guru.core.consent:GuruConsent:1.0.0' +// implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +// implementation 'androidx.appcompat:appcompat:1.3.0' +// implementation "androidx.core:core-ktx:1.8.0" +// +// // https://github.com/AppLovin/AppLovin-MAX-SDK-Android/issues/33 +// implementation 'com.google.android.exoplayer:exoplayer:2.15.1' +// api 'com.applovin:applovin-sdk:11.6.1' +// api 'com.applovin.mediation:facebook-adapter:6.12.0.2' +// // DT Exchange +// api 'com.applovin.mediation:fyber-adapter:8.2.1.2' +// api 'com.applovin.mediation:google-adapter:21.4.0.1' +// api 'com.applovin.mediation:google-ad-manager-adapter:21.4.0.1' +// api 'com.applovin.mediation:ironsource-adapter:7.2.7.0.0' +// api 'com.applovin.mediation:smaato-adapter:21.8.6.0' +// api 'com.applovin.mediation:unityads-adapter:4.5.0.2' +// // Yahoo +// api 'com.applovin.mediation:verizonads-adapter:2.2.0.8' +// +// // Pangle +// api 'com.applovin.mediation:bytedance-adapter:4.9.0.8.0' +// +// // pubnative +// api 'com.applovin.mediation:verve-adapter:2.16.0.0' +// api 'com.applovin.mediation:vungle-adapter:6.12.0.5' +// api 'com.applovin.mediation:chartboost-adapter:9.1.1.2' +// api 'com.applovin.mediation:mintegral-adapter:16.3.51.1' +// +// api 'com.amazon.android:aps-sdk:9.6.2' +// api 'com.applovin.mediation:amazon-tam-adapter:9.6.2.2' +// +// +// // InMobi +// implementation 'com.squareup.picasso:picasso:2.71828' +// implementation 'androidx.recyclerview:recyclerview:1.2.1' +// api 'com.applovin.mediation:inmobi-adapter:10.1.2.4' +// +// implementation 'com.pubmatic.sdk:openwrap:2.6.1' +// implementation 'com.applovin.mediation:alopenwrapadapter:1.0.2' + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.appcompat:appcompat:1.5.1' + implementation "androidx.core:core-ktx:1.8.0" + implementation 'androidx.recyclerview:recyclerview:1.2.1' + implementation 'guru.core.consent:GuruConsent:1.1.0' + // https://github.com/AppLovin/AppLovin-MAX-SDK-Android/issues/33 + implementation 'com.google.android.exoplayer:exoplayer:2.15.1' + + // implementation 'guru.ads.max:max-adapter:0.2.0' + + api 'com.applovin:applovin-sdk:11.11.3' + api 'com.applovin.mediation:facebook-adapter:6.16.0.2' + // DT Exchange + api 'com.applovin.mediation:fyber-adapter:8.2.3.3' + api 'com.applovin.mediation:google-adapter:22.3.0.0' + api 'com.applovin.mediation:google-ad-manager-adapter:22.3.0.0' + api 'com.applovin.mediation:ironsource-adapter:7.5.0.0.1' + api 'com.applovin.mediation:smaato-adapter:22.3.2.0' + api 'com.applovin.mediation:unityads-adapter:4.8.0.0' + api 'com.applovin.mediation:bytedance-adapter:5.6.0.1.0' + api 'com.applovin.mediation:verve-adapter:2.19.0.0' + api 'com.applovin.mediation:vungle-adapter:6.11.0.1' + api 'com.applovin.mediation:chartboost-adapter:9.4.1.0' + api 'com.applovin.mediation:inmobi-adapter:10.1.4.3' + api 'com.amazon.android:aps-sdk:9.8.4' + api 'com.applovin.mediation:amazon-tam-adapter:9.8.4.0' + api 'com.applovin.mediation:mintegral-adapter:16.5.11.0' + // PubMatic + api 'com.pubmatic.sdk:openwrap:3.1.0' + api 'com.applovin.mediation:alopenwrapadapter:1.1.0' + + api 'com.applovin.mediation:bidmachine-adapter:2.3.2.0' + + api 'com.moloco.sdk.adapters:applovin:1.6.0.0' + api 'com.applovin.mediation:ogury-presage-adapter:5.6.2.0' + +} diff --git a/guru_app/plugins/guru_applovin_flutter/android/gradle.properties b/guru_app/plugins/guru_applovin_flutter/android/gradle.properties new file mode 100644 index 0000000..250fd7d --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/android/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true +android.useAndroidX=true +android.enableJetifier=true +android.enableDexingArtifactTransform=false diff --git a/guru_app/plugins/guru_applovin_flutter/android/gradle/wrapper/gradle-wrapper.properties b/guru_app/plugins/guru_applovin_flutter/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..939efa2 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip diff --git a/guru_app/plugins/guru_applovin_flutter/android/settings.gradle b/guru_app/plugins/guru_applovin_flutter/android/settings.gradle new file mode 100644 index 0000000..82bf7e3 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'guru_applovin_flutter' diff --git a/guru_app/plugins/guru_applovin_flutter/android/src/main/AndroidManifest.xml b/guru_app/plugins/guru_applovin_flutter/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..564e12b --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/android/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/AdHelp.kt b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/AdHelp.kt new file mode 100644 index 0000000..d4add46 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/AdHelp.kt @@ -0,0 +1,94 @@ +package flutter.guru.guru_applovin_flutter + +import android.app.Activity +import android.content.pm.PackageManager +import com.applovin.mediation.MaxAd +import com.applovin.sdk.AppLovinSdk +import org.json.JSONObject +import java.lang.ref.SoftReference +import java.lang.ref.WeakReference +import java.util.* +import java.util.concurrent.atomic.AtomicReference + +/** + * Created by yaqiliu on 2020-03-06. + */ +object AdHelp { + + + private val activityRef: AtomicReference = AtomicReference(null) + + val activity: Activity? + get() = activityRef.get() + + val appVersion: String by lazy { + activity?.let { + it.packageManager.getPackageInfo(it.packageName, 0).versionName + } ?: "" + } + + fun attach(activity: Activity) { + this.activityRef.set(activity) + } + + fun detach() { + this.activityRef.set(null) + } + + fun argumentsMap(vararg args: Any): Map? { + val arguments: MutableMap = HashMap() + var i = 0 + while (i < args.size) { + arguments[args[i].toString()] = args[i + 1] + i += 2 + } + return arguments + } + + fun argumentsMap(id: Int, vararg args: Any): Map? { + val arguments: MutableMap = HashMap() + var i = 0 + arguments["id"] = id + while (i < args.size) { + arguments[args[i].toString()] = args[i + 1] + i += 2 + } + return arguments + } + + fun argumentsMapEx(id: Int, parameters: Map): Map? { + val arguments: MutableMap = HashMap() + arguments["id"] = id + arguments.putAll(parameters) + return arguments + } + + fun toAdPayload(ad: MaxAd?): String { + if (ad == null) { + return "" + } + return try { + val config = AppLovinSdk.getInstance(activity).configuration + JSONObject( + mapOf( + "ad_platform" to "MAX", + "id" to (ad.creativeId ?: ""), + "adunit_id" to ad.adUnitId, + "adunit_name" to (ad.placement ?: ad.adUnitId), + "adunit_format" to ad.format.label, + "adgroup_id" to "", + "adgroup_name" to "", + "adgroup_type" to "", + "currency" to "USD", + "country" to config.countryCode, + "app_version" to appVersion, + "publisher_revenue" to ad.revenue, + "network_name" to (ad.networkName ?: ""), + "network_placement_id" to ad.networkPlacement, + "precision" to "publisher_defined" + )).toString() + } catch (throwable: Throwable) { + "" + } + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/AdStatus.kt b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/AdStatus.kt new file mode 100644 index 0000000..8959793 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/AdStatus.kt @@ -0,0 +1,93 @@ +package flutter.guru.guru_applovin_flutter + +import com.google.android.ump.FormError +import androidx.annotation.IntDef + +/** + * Created by yaqiliu on 2020-03-06. + */ + +class AdStatus { + companion object { + @JvmStatic + val CREATED = 0 + + @JvmStatic + val LOADING = 1 + + @JvmStatic + val FAILED = 2 + + @JvmStatic + val LOADED = 3 + + @JvmStatic + val HIDDEN = 4 + } +} + +class AdFormats { + + companion object { + @JvmStatic + val BANNER = "BANNER" + + @JvmStatic + val MREC = "MREC" + + @JvmStatic + val LEADER = "LEADER" + + @JvmStatic + val INTERSTITIAL = "INTER" + + @JvmStatic + val REWARDED = "REWARDED" + + @JvmStatic + val REWARDED_INTERSTITIAL = "REWARDED_INTER" + + @JvmStatic + val NATIVE = "NATIVE" + + @JvmStatic + val CROSS_PROMO = "XPROMO" + } + +} + +class ConsentResult { + companion object { + val ShouldShow = 0 + val NotShow = -1 + val GdprNotApplies = -2 + val Unknown = -3 + } +} + +@IntDef( + ConsentErrorCode.SUCCESS, ConsentErrorCode.INTERNAL_ERROR, + ConsentErrorCode.INTERNET_ERROR, ConsentErrorCode.INVALID_OPERATION, ConsentErrorCode.TIME_OUT +) +@Retention(AnnotationRetention.SOURCE) +annotation class ConsentErrorCode { + companion object { + // Consent Form Not Available + const val SUCCESS = 0 + + const val INTERNAL_ERROR = 1 + const val INTERNET_ERROR = 2 + const val INVALID_OPERATION = 3 + const val TIME_OUT = 4 + + fun toConsentErrorCode(error: FormError?): Int { + return when (error?.errorCode) { + FormError.ErrorCode.INTERNAL_ERROR -> INTERNAL_ERROR + FormError.ErrorCode.TIME_OUT -> TIME_OUT + FormError.ErrorCode.INTERNET_ERROR -> INTERNET_ERROR + FormError.ErrorCode.INVALID_OPERATION -> INVALID_OPERATION + else -> SUCCESS + } + } + } +} \ No newline at end of file diff --git a/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/BannerAd.kt b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/BannerAd.kt new file mode 100644 index 0000000..7d21803 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/BannerAd.kt @@ -0,0 +1,304 @@ +package flutter.guru.guru_applovin_flutter + +import android.app.Activity +import android.util.Log +import android.util.SparseArray +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.core.util.valueIterator +import com.amazon.device.ads.* +import com.applovin.mediation.MaxAd +import com.applovin.mediation.MaxAdRevenueListener +import com.applovin.mediation.MaxAdViewAdListener +import com.applovin.mediation.MaxError +import com.applovin.mediation.MaxNetworkResponseInfo +import com.applovin.mediation.ads.MaxAdView +import com.applovin.mediation.ads.MaxInterstitialAd +import com.applovin.sdk.AppLovinAdSize +import com.applovin.sdk.AppLovinSdkUtils +import flutter.guru.guru_applovin_flutter.log.Logger +import io.flutter.plugin.common.MethodChannel + +/** + * Created by yaqiliu on 2020-03-10. + */ +class BannerAd(id: Int, channel: MethodChannel) : MaxAdViewAdListener, MaxAdRevenueListener { + + private val id: Int = id + private val channel: MethodChannel = channel + var status: Int = AdStatus.CREATED + + var anchorOffset = 0.0 + var horizontalCenterOffset = 0.0 + var anchorType = 0 + + private var bannerAd: MaxAdView? = null + + companion object { + + private val allAds: SparseArray = SparseArray() + + fun createBanner( + id: Int, + channel: MethodChannel, + ): BannerAd { + var bannerAd = getAdForId(id) + if (bannerAd == null) { + bannerAd = BannerAd( + id, + channel + ) + } + return bannerAd + } + + fun getAdForId(id: Int): BannerAd? { + return allAds.get(id) + } + + fun disposeAll(activity: Activity?) { + if (activity == null) return + var iterator = allAds.valueIterator() + if (iterator.hasNext()) { + iterator.next().dispose(activity) + } + } + } + + init { + allAds.put(id, this) + anchorOffset = 0.0 + horizontalCenterOffset = 0.0 + anchorType = 80 + } + + private fun logWarn(message: String) { + Logger.w("BannerAd", message) + } + + private fun logDebug(message: String) { + Logger.d("BannerAd", message) + } + + private fun logError(message: String) { + Logger.e("BannerAd", message) + } + + internal fun load( + activity: Activity, + adUnitId: String, + adAmazonSlotId: String?, + placement: String? = null + ) { + status = AdStatus.LOADING + + bannerAd = MaxAdView(adUnitId, activity).let { + it.placement = placement ?: "Unknown" + it.setListener(this) + it.setRevenueListener(this) + it + } + + if (!adAmazonSlotId.isNullOrBlank()) { + val loader = DTBAdRequest() + loader.setSizes(DTBAdSize(320, 50, adAmazonSlotId)) + loader.loadAd(object : DTBAdCallback { + // No APS bid available + override fun onFailure(adError: AdError) { + logDebug("Amazon:Oops banner ad load has failed: ${adError.message}") + bannerAd?.apply { + setLocalExtraParameter("amazon_ad_error", adError) + loadAd() + } + } + + override fun onSuccess(dtbAdResponse: DTBAdResponse) { + logDebug("Amazon BannerAd load success") + bannerAd?.apply { + setLocalExtraParameter("amazon_ad_response", dtbAdResponse) + loadAd() + } + } + + }) + } else { + bannerAd?.loadAd() + } + } + + internal fun show(activity: Activity) { + if (activity.findViewById(id) == null) { + val isTablet = AppLovinSdkUtils.isTablet(activity) // Available on Android SDK 9.6.2+ + val heightPx = AppLovinSdkUtils.dpToPx(activity, if (isTablet) 90 else 50) + + bannerAd!!.setLayoutParams( + LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + heightPx + ) + ) + + val content = LinearLayout(activity) + content.id = id + content.orientation = LinearLayout.VERTICAL + content.gravity = anchorType + content.addView(bannerAd) + val scale = activity.resources.displayMetrics.density + val left = + if (horizontalCenterOffset > 0) (horizontalCenterOffset * scale).toInt() else 0 + val right = + if (horizontalCenterOffset < 0) (Math.abs(horizontalCenterOffset) * scale).toInt() else 0 + if (anchorType == Gravity.BOTTOM) { + content.setPadding(left, 0, right, (anchorOffset * scale).toInt()) + } else { + content.setPadding(left, (anchorOffset * scale).toInt(), right, 0) + } + activity.addContentView( + content, + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT + ) + ) + + bannerAd!!.visibility = View.VISIBLE + bannerAd!!.startAutoRefresh() + } else { + bannerAd!!.startAutoRefresh() + bannerAd!!.visibility = View.VISIBLE + } + } + + internal fun hide(activity: Activity) { + if (activity.findViewById(id) != null) { + bannerAd!!.stopAutoRefresh() + bannerAd!!.visibility = View.GONE + } + } + + internal fun dispose(activity: Activity) { + allAds.remove(id) + bannerAd!!.destroy() + + val contentView = activity.findViewById(id) + if (contentView == null || contentView.parent !is ViewGroup) return + + val contentParent = contentView.parent as ViewGroup + contentParent.removeView(contentView) + } + + override fun onAdLoadFailed(adUnitId: String?, err: MaxError) { + if (status == AdStatus.LOADED) { + return + } + val waterfallName = try { + err.waterfall?.let { waterfall -> + logWarn("Waterfall Name: " + waterfall.name + " and Test Name: " + waterfall.testName) + logWarn("Waterfall latency was: " + waterfall.latencyMillis + " milliseconds") + + for (networkResponse: MaxNetworkResponseInfo in waterfall.networkResponses) { + logWarn( + "Network -> " + networkResponse.mediatedNetwork + + "...latency: " + networkResponse.latencyMillis + + "...credentials: " + networkResponse.credentials + " milliseconds" + + "...error: " + networkResponse.error + ) + } + return@let waterfall.name + } ?: "unknown" + } catch (throwable: Throwable) { + logError("onAdLoadFailed error: ${throwable.message}") + "unknown" + } + + status = AdStatus.FAILED + channel.invokeMethod( + "onBannerAdLoadFailed", + AdHelp.argumentsMapEx( + id, + parameters = mapOf("errorCode" to err.code, "ad_waterfall_name" to waterfallName) + ) + ) + } + + override fun onAdClicked(ad: MaxAd?) { + channel.invokeMethod("onBannerAdClicked", AdHelp.argumentsMap(id)) + } + + override fun onAdDisplayed(ad: MaxAd?) { + channel.invokeMethod( + "onBannerAdDisplayed", AdHelp.argumentsMapEx( + id, parameters = mapOf( + "ad_revenue" to (ad?.revenue ?: -1.0), + "ad_format" to (ad?.format?.label ?: ""), + "ad_source" to (ad?.networkName ?: ""), + "ad_unit_name" to (ad?.adUnitId ?: "") + ) + ) + ) + } + + override fun onAdExpanded(ad: MaxAd?) { + } + + override fun onAdCollapsed(ad: MaxAd?) { + } + + override fun onAdLoaded(ad: MaxAd?) { + // Banner ad is ready to be shown. interstitialAd!!.isReady() will now return 'true' + val waterfallName = try { + ad?.waterfall?.let { waterfall -> + logWarn("Waterfall Name: " + waterfall.name + " and Test Name: " + waterfall.testName) + logWarn("Waterfall latency was: " + waterfall.latencyMillis + " milliseconds") + + for (networkResponse: MaxNetworkResponseInfo in waterfall.networkResponses) { + logWarn( + "Network -> " + networkResponse.mediatedNetwork + + "...latency: " + networkResponse.latencyMillis + + "...credentials: " + networkResponse.credentials + " milliseconds" + + "...error: " + networkResponse.error + ) + } + return@let waterfall.name + } ?: "unknown" + } catch (throwable: Throwable) { + logError("onAdLoadFailed error: ${throwable.message}") + "unknown" + } + val parameters = mapOf( + "ad_revenue" to (ad?.revenue ?: -1.0), + "ad_format" to (ad?.format?.label ?: ""), + "ad_source" to (ad?.networkName ?: ""), + "ad_unit_name" to (ad?.adUnitId ?: ""), + "ad_waterfall_name" to waterfallName, + ) + status = AdStatus.LOADED + channel.invokeMethod("onBannerAdLoaded", AdHelp.argumentsMapEx(id, parameters = parameters)) + } + + override fun onAdHidden(ad: MaxAd?) { + // Interstitial ad is hidden. Pre-load the next ad + status = AdStatus.HIDDEN + channel.invokeMethod("onBannerAdHidden", AdHelp.argumentsMap(id)) + Logger.w("Ads", "onBannerAdHidden") + } + + override fun onAdDisplayFailed(ad: MaxAd?, err: MaxError) { + // Interstitial ad failed to display. We recommend loading the next ad + status = AdStatus.FAILED + channel.invokeMethod( + "onBannerAdDisplayFailed", + AdHelp.argumentsMap(id, "errorCode", err.code) + ) + } + + override fun onAdRevenuePaid(ad: MaxAd?) { + channel.invokeMethod( + "onAdImpression", mapOf( + "payload" to AdHelp.toAdPayload(ad) + ) + ) + } + +} diff --git a/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/GuruApplovin.kt b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/GuruApplovin.kt new file mode 100644 index 0000000..75ef682 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/GuruApplovin.kt @@ -0,0 +1,23 @@ +package flutter.guru.guru_applovin_flutter + +import android.app.Activity +import android.content.Context +import com.applovin.sdk.AppLovinPrivacySettings +import com.applovin.sdk.AppLovinSdk +import com.applovin.sdk.AppLovinSdkUtils + +class GuruApplovin { + companion object { + fun afterAcceptPrivacy(activity: Activity) { + AppLovinPrivacySettings.setHasUserConsent(true, activity) + } + + fun isTablet(activity: Activity): Boolean { + return AppLovinSdkUtils.isTablet(activity) + } + + fun showMediationDebugger(context: Context) { + AppLovinSdk.getInstance(context).showMediationDebugger() + } + } +} \ No newline at end of file diff --git a/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/GuruApplovinFlutterPlugin.kt b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/GuruApplovinFlutterPlugin.kt new file mode 100644 index 0000000..e8a8425 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/GuruApplovinFlutterPlugin.kt @@ -0,0 +1,734 @@ +package flutter.guru.guru_applovin_flutter + +import android.app.Activity +import android.content.Context +import android.util.Log +import android.view.Gravity +import androidx.annotation.NonNull +import com.amazon.device.ads.AdRegistration +import com.amazon.device.ads.DTBAdNetwork +import com.amazon.device.ads.DTBAdNetworkInfo +import com.amazon.device.ads.MRAIDPolicy +import com.applovin.sdk.AppLovinPrivacySettings +import com.applovin.sdk.AppLovinSdk +import com.applovin.sdk.AppLovinSdkConfiguration +import com.applovin.sdk.AppLovinSdkUtils +import com.google.android.gms.ads.MobileAds +import com.google.android.gms.ads.RequestConfiguration +import com.pubmatic.sdk.common.OpenWrapSDK +import com.pubmatic.sdk.common.models.POBApplicationInfo +import flutter.guru.guru_applovin_flutter.log.Logger +import guru.core.consent.gdpr.ConsentManager +import guru.core.consent.gdpr.ConsentRequest +import guru.core.consent.gdpr.ConsentStatus +import guru.core.consent.gdpr.GdprHelper +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import java.net.URL +import java.util.* +import java.util.concurrent.atomic.AtomicBoolean + +/** GuruApplovinFlutterPlugin */ +class GuruApplovinFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel: MethodChannel + private var applicationContext: Context? = null + private var activity: Activity? = null + private val initialized = AtomicBoolean(false) + + private val consentManager: ConsentManager by lazy { + ConsentManager(activity!!) + } + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + Logger.d("Ads", "GuruApplovinFlutterPlugin onAttachedToEngine") + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "guru_applovin_flutter") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) { + if (activity == null) { + Logger.w("Ads", "GuruApplovinFlutterPlugin onMethodCall activity is null") + result.error( + "no_activity", + "firebase_admob plugin requires a foreground activity", + null + ) + return + } + val id = if (call.arguments is Map<*, *>) { + call.argument("id") + } else { + null + } + Logger.d("Ads", "GuruApplovinFlutterPlugin onMethodCall id: $id method:${call.method}") + when (call.method) { + "initialize" -> callInitialize(call, result) + "gatherConsentAndInitialize" -> callGatherConsentAndInitialize(call, result) + "requestGdpr" -> callRequestGdpr(call, result) + "resetGdpr" -> callResetGdpr(call, result) + "showPrivacyOptionsForm" -> callShowPrivacyOptionsForm(call, result) + "loadRewardedVideoAd" -> callLoadRewardedVideoAd(id, call, result) + "showRewardedVideoAd" -> callShowRewardedVideoAd(id, call, result) + "loadInterstitialAd" -> callLoadInterstitialAd(id, call, result) + "showInterstitialAd" -> callShowInterstitialAd(id, call, result) + "isInterstitialAdLoaded" -> callInterstitialAdLoaded(id, result) + "loadBannerAd" -> callLoadBannerAd(id, call, result) + "showBannerAd" -> callShowBannerAd(id, call, result) + "hideBannerAd" -> callHideBannerAd(id, result) + "disposeBannerAd" -> callDisposeBannerAd(id, result) + "disposeRewardedAd" -> callDisposeRewardedVideoAd(id, result) + "getRewardedAdState" -> callGetRewardedAdsState(id, result) + "disposeInterstitialAd" -> callDisposeInterstitialAd(id, result) + "getInterstitialAdState" -> callGetInterstitialAdsState(id, result) + "afterAcceptPrivacy" -> callAfterAcceptPrivacy(call, result) + "checkConsentDialogStatus" -> callCheckConsentDialogStatus(result) + "hasUserConsent" -> callHasUserConsent(result) + "openDebugger" -> callOpenDebugger(result) + "isTablet" -> callIsTablet(result) + "setDebugMode" -> callSetDebugMode(call, result) + "setKeywords" -> callSetKeywords(call, result) + "clearTargetingData" -> callClearTargetingData(call, result) + "getBannerAdSize" -> callGetBannerAdSize(call, result) + else -> result.notImplemented() + } + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + Logger.d("Ads", "GuruApplovinFlutterPlugin onAttachedToActivity"); + attachActivity(binding.activity) + } + + override fun onDetachedFromActivityForConfigChanges() { + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + } + + override fun onDetachedFromActivity() { + Logger.d("Ads", "GuruApplovinFlutterPlugin onDetachedFromActivity"); + detachActivity() + } + + private fun attachActivity(activity: Activity?) { + activity?.apply { + AdHelp.attach(this) + this@GuruApplovinFlutterPlugin.activity = this + this@GuruApplovinFlutterPlugin.applicationContext = applicationContext + Logger.initialize(this) + } + } + + private fun detachActivity() { + BannerAd.disposeAll(activity) + this.activity = null + AdHelp.detach() + } + + private fun callInitialize(methodCall: MethodCall, result: MethodChannel.Result) { + if (!initialized.compareAndSet(false, true)) { + Logger.d("Ads", "call already initialized! ignore!") + return + } + val debugLog = methodCall.argument("debug_mode") ?: false + val userId = methodCall.argument("user_id") ?: "" + + //init amazon + val amazonAppId = methodCall.argument("amazon_appId") + if (!amazonAppId.isNullOrBlank() && activity != null) { + AdRegistration.getInstance(amazonAppId, activity!!) + AdRegistration.setAdNetworkInfo(DTBAdNetworkInfo(DTBAdNetwork.MAX)) + AdRegistration.setMRAIDSupportedVersions(arrayOf("1.0", "2.0", "3.0")) + AdRegistration.setMRAIDPolicy(MRAIDPolicy.CUSTOM) + AdRegistration.useGeoLocation(true) + AdRegistration.enableLogging(debugLog) +// AdRegistration.enableTesting(true) + } + + val pubmaticStoreUrl = methodCall.argument("pubmatic_store_url") + if (!pubmaticStoreUrl.isNullOrBlank() && activity != null) { + val appInfo = POBApplicationInfo() + try { + appInfo.storeURL = URL(pubmaticStoreUrl) + } catch (error: Throwable) { + } + OpenWrapSDK.setApplicationInfo(appInfo) + } + + AppLovinSdk.getInstance(activity).mediationProvider = "max" + AppLovinSdk.getInstance(activity).settings.isMuted = true + if (userId.isNotEmpty()) { + AppLovinSdk.getInstance(activity).setUserIdentifier(userId) + Logger.d("Ads", "set userId$userId success"); + } else { + Logger.d("Ads", "userId is null"); + } + AppLovinSdk.getInstance(activity).settings.setVerboseLogging(debugLog) + var isInvoked = false + AppLovinSdk.getInstance(activity).initializeSdk { + // AppLovin SDK is initialized, start loading ads + if (!isInvoked) { + result.success(true) + } + Logger.d("Ads", "AppLovinSdk initializeSdk success") + isInvoked = true + } + if (activity != null) { + MobileAds.initialize(activity!!) { + MobileAds.setAppMuted(true) + MobileAds.setAppVolume(0.0F) + } + } + } + + private fun callRequestGdpr(methodCall: MethodCall, result: MethodChannel.Result) { + Logger.d("GuruAds", "callRequestGdpr ${methodCall.argument("debug_geography")}") + + val debugGeography = methodCall.argument("debug_geography") + val testDeviceId = methodCall.argument("test_device_id") + val deviceIds: Set = if (testDeviceId != null) setOf(testDeviceId) else emptySet() + Logger.d("GuruAds", "requestGdpr debugGeography:${debugGeography}, deviceIds:${deviceIds}") + val request: ConsentRequest = ConsentRequest.Builder(activity!!) + .debugGeography(debugGeography) + .addDeviceIds(deviceIds) + .withListener(object : ConsentRequest.Listener { + override fun onConsentResult(status: Int) { + Logger.d("GuruAds", "onConsentResult:$status") + result.success(status) + } + + override fun onConsentImpression() { + Logger.d("GuruAds", "onConsentImpression") + + } + + override fun onConsentLoadFailure() { + Logger.d("GuruAds", "onConsentLoadFailure") + result.success(ConsentStatus.NOT_AVAILABLE) + } + }).build() + GdprHelper.request(request) + } + + private fun callGatherConsentAndInitialize( + methodCall: MethodCall, + result: MethodChannel.Result + ) { + Logger.d( + "GuruAds", + "callGatherConsentAndInitialize ${methodCall.argument("debug_geography")}" + ) + + val debugGeography = methodCall.argument("debug_geography") + val testDeviceId = methodCall.argument("test_device_id") + val deviceIds: Set = if (testDeviceId != null) setOf(testDeviceId) else emptySet() + Logger.d( + "GuruAds", + "callGatherConsentAndInitialize debugGeography:${debugGeography}, deviceIds:${deviceIds}" + ) + val request: ConsentRequest = ConsentRequest.Builder(activity!!) + .debugGeography(debugGeography) + .addDeviceIds(deviceIds) + .withConsentGatheringCompleteListener { error -> + error?.let { + // Consent not obtained in current session. + Logger.d("GuruAds", "gather consent error! ${it.errorCode}: ${it.message}") + } + if (consentManager.canRequestAds) { + callInitialize(methodCall, result) + } + }.build() + consentManager.gather(request) + + if (consentManager.canRequestAds) { + callInitialize(methodCall, result) + } + } + + private fun callShowPrivacyOptionsForm(methodCall: MethodCall, result: MethodChannel.Result) { + consentManager.showPrivacyOptionsForm(activity!!) { formError -> + result.success(ConsentErrorCode.toConsentErrorCode(formError)) + } + } + + private fun callResetGdpr(methodCall: MethodCall, result: MethodChannel.Result) { + GdprHelper.reset(activity!!) + result.success(true) + } + + private fun callLoadRewardedVideoAd( + id: Int?, + methodCall: MethodCall, + result: MethodChannel.Result + ) { + Logger.d("Ads", "RewardedVideo applovin native callLoadRewardedVideoAd") + if (id == null) { + Logger.w("Ads", "RewardedVideo applovin native callLoadRewardedVideoAd lose hash id") + result.error("no_hash_id", "load rewardedVideoAd lose hash id", null) + } + + var rewardedVideoAd = RewardedVideoAd.createRewardedVideo(id!!, channel!!) + if (rewardedVideoAd.status == AdStatus.LOADED) { + Logger.w("Ads", "RewardedVideo applovin native callLoadRewardedVideoAd ad is loaded") + result.success(true) // The ad was already loaded or loading. + return; + } + + var adUnitId = methodCall.argument("adUnitId") + if (adUnitId.isNullOrEmpty()) { + Logger.w( + "Ads", + "RewardedVideo applovin native callLoadRewardedVideoAd adUnitId is null or empty" + ) + result.error( + "no_adunit_id", + "a null or empty adUnitId was provided for rewarded video id=" + rewardedVideoAd.id, + null + ) + return + } + + if (activity == null) { + Logger.w( + "Ads", + "RewardedVideo applovin native callLoadRewardedVideoAd activity is null" + ) + result.error("activity_is_null", "activity_is_null", null) + return + } + Logger.d("Ads", "RewardedVideo applovin native callLoadRewardedVideoAd load") + val adAmazonSlotId = methodCall.argument("adAmazonSlotId") ?: "" + + rewardedVideoAd.load(activity!!, adUnitId, adAmazonSlotId) + result.success(true) + } + + private fun callShowRewardedVideoAd( + id: Int?, + methodCall: MethodCall, + result: MethodChannel.Result + ) { + if (id == null) { + Logger.w("Ads", "RewardedVideo applovin native callShowRewardedVideoAd lose hash id") + result.error("no_hash_id", "show rewardedVideoAd lose hash id", null) + } + + var rewardedVideoAd = RewardedVideoAd.getAdForId(id!!) + if (rewardedVideoAd != null && rewardedVideoAd.status == AdStatus.LOADED) { + val placement = methodCall.argument("placement") + result.success(rewardedVideoAd.show(placement)) + Logger.d("Ads", "RewardedVideo applovin native callShowRewardedVideoAd show") + } else { + Logger.w( + "Ads", + "RewardedVideo applovin native callShowRewardedVideoAd ad is not loaded" + ) + result.error( + "ad_not_loaded", + "show failed for rewardedVideo ad, no ad was loaded", + null + ) + } + } + + private fun callLoadInterstitialAd( + id: Int?, + methodCall: MethodCall, + result: MethodChannel.Result + ) { + Logger.d("Ads", "applovin native callLoadInterstitialAd") + if (id == null) { + result.error("no_hash_id", "load interstitialAd lose hash id", null) + } + + var interstitialAd = InterstitialAd.createInterstitial(id!!, channel!!) + if (interstitialAd.status == AdStatus.LOADED) { + Logger.w("Ads", "applovin native callLoadInterstitialAd ad is loaded") + result.success(true) // The ad was already loaded or loading. + return; + } + + var adUnitId = methodCall.argument("adUnitId") + if (adUnitId.isNullOrEmpty()) { + Logger.w("Ads", "applovin native callLoadInterstitialAd adUnitId is null or empty") + result.error( + "no_adunit_id", + "a null or empty adUnitId was provided for ad id=" + interstitialAd.id, + null + ) + return + } + + var adAmazonSlotId = methodCall.argument("adAmazonSlotId") + + if (activity == null) { + Logger.w("Ads", "applovin native callLoadInterstitialAd activity is null") + result.error("activity_is_null", "activity_is_null", null) + return + } + + Logger.d( + "Ads", + "applovin native callLoadInterstitialAd load adAmazonSlotId:$adAmazonSlotId" + ) + + interstitialAd.load(activity!!, adUnitId, adAmazonSlotId) + result.success(true) + } + + private fun callShowInterstitialAd( + id: Int?, + methodCall: MethodCall, + result: MethodChannel.Result + ) { + if (id == null) { + Logger.w("Ads", "applovin native callShowInterstitialAd lose hash id") + result.error("no_hash_id", "show interstitialAd lose hash id", null) + } + + var interstitialAd = InterstitialAd.getAdForId(id!!) + if (interstitialAd != null && interstitialAd.status == AdStatus.LOADED) { + val placement = methodCall.argument("placement") + result.success(interstitialAd.show(placement)) + Logger.d("Ads", "applovin native callShowInterstitialAd show") + } else { + Logger.w("Ads", "applovin native callShowInterstitialAd ad is not loaded") + result.error("ad_not_loaded", "show failed for interstitial ad, no ad was loaded", null) + } + } + + private fun callInterstitialAdLoaded(id: Int?, result: MethodChannel.Result) { + if (id == null) { + Logger.w("Ads", "applovin native callInterstitialAdLoaded lose hash id") + result.error("no_hash_id", "is interstitialAd loaded lose hash id", null) + } + + var interstitialAd = InterstitialAd.getAdForId(id!!) + if (interstitialAd == null) { + Logger.w("Ads", "applovin native callInterstitialAdLoaded ad is not loaded") + result.error( + "no_ad_for_id", + "isAdLoaded failed, no add exists for interstitialAd id=$id", + null + ) + return + } + if (interstitialAd.status == AdStatus.LOADED) { + result.success(true) + } else { + result.success(false) + } + Logger.d("Ads", "applovin native callInterstitialAdLoaded status:${interstitialAd.status}") + } + + private fun callLoadBannerAd(id: Int?, methodCall: MethodCall, result: MethodChannel.Result) { + Logger.d("Ads", "applovin native callLoadBannerAd") + if (id == null) { + result.error("no_hash_id", "load bannerAd lose hash id", null) + } + + var adUnitId = methodCall.argument("adUnitId") + if (adUnitId.isNullOrEmpty()) { + Logger.w("Ads", "applovin native callLoadBannerAd adUnitId is null or empty") + result.error( + "no_adunit_id", + "a null or empty adUnitId was provided for ad id=$id", + null + ) + return + } + + val adAmazonSlotId = methodCall.argument("adAmazonSlotId") + val placement = methodCall.argument("placement") + + if (activity == null) { + Logger.w("Ads", "applovin native callLoadBannerAd activity is null") + result.error("activity_is_null", "activity_is_null", null) + return + } + + var bannerAd = BannerAd.createBanner(id!!, channel!!) + + Logger.d("Ads", "applovin native callLoadBannerAd load") + bannerAd.load(activity!!, adUnitId, adAmazonSlotId, placement) + + result.success(true) + } + + private fun callShowBannerAd(id: Int?, methodCall: MethodCall, result: MethodChannel.Result) { + if (id == null) { + Logger.w("Ads", "applovin native callShowBannerAd lose hash id") + result.error("no_hash_id", "show bannerAd lose hash id", null) + } + + var bannerAd = BannerAd.getAdForId(id!!) + if (bannerAd == null || (bannerAd.status != AdStatus.LOADED && bannerAd.status != AdStatus.HIDDEN)) { + Logger.w("Ads", "applovin native callShowBannerAd ad is not loaded ${bannerAd?.status}") + result.error("ad_not_loaded", "show failed for banner ad, no ad was loaded ${bannerAd?.status}", null) + return + } + val anchorOffset: String? = methodCall.argument("anchorOffset") + val horizontalCenterOffset: String? = methodCall.argument("horizontalCenterOffset") + val anchorType: String? = methodCall.argument("anchorType") + if (anchorOffset != null) { + bannerAd.anchorOffset = anchorOffset!!.toDouble() + } + if (horizontalCenterOffset != null) { + bannerAd.horizontalCenterOffset = horizontalCenterOffset!!.toDouble() + } + if (anchorType != null) { + bannerAd.anchorType = if ("bottom" == anchorType) Gravity.BOTTOM else Gravity.TOP + } + + if (activity == null) { + result.error("activity_is_null", "activity_is_null", null) + Logger.w("Ads", "applovin native callShowBannerAd activity is null") + return + } + + bannerAd!!.show(activity!!) + result.success(true) + Logger.d("Ads", "applovin native callShowBannerAd show") + } + + private fun callHideBannerAd(id: Int?, result: MethodChannel.Result) { + if (id == null) { + Logger.w("Ads", "applovin native callHideBannerAd no_hash_id") + result.error("no_hash_id", "hide bannerAd lose hash id", null) + } + + var bannerAd = BannerAd.getAdForId(id!!) + if (bannerAd == null) { + Logger.w("Ads", "applovin native callHideBannerAd ad_not_loaded") + result.error("ad_not_loaded", "bannerAd is null", null) + return + } + + if (activity == null) { + Logger.w("Ads", "applovin native callHideBannerAd activity_is_null") + result.error("activity_is_null", "activity_is_null", null) + return + } + + bannerAd!!.hide(activity!!) + result.success(true) + } + + private fun callDisposeBannerAd(id: Int?, result: MethodChannel.Result) { + if (id == null) { + Logger.w("Ads", "applovin native callDisposeBannerAd no_hash_id") + result.error("no_hash_id", "dispose bannerAd lose hash id", null) + } else { + val act = activity + if (act == null) { + Logger.w("Ads", "applovin native callDisposeBannerAd activity_is_null") + result.error("activity_is_null", "activity is detached", null) + return + } + val ad = BannerAd.getAdForId(id) + if (ad != null) { + ad.dispose(act) + result.success(true) + } else { + Logger.w("Ads", "applovin native callDisposeBannerAd ad_not_found") + result.error( + "ad_not_found", + "dispose failed for bannerAd ad, no add exists for id=$id", + null + ) + } + } + } + + private fun callDisposeRewardedVideoAd(id: Int?, result: MethodChannel.Result) { + if (id == null) { + Logger.w("Ads", "applovin native callDisposeRewardedVideoAd no_hash_id") + result.error("no_hash_id", "dispose rewardedVideoAd lose hash id", null) + } else { + val ad = RewardedVideoAd.getAdForId(id) + if (ad != null) { + ad.dispose() + result.success(true) + } else { + Logger.w("Ads", "applovin native callDisposeRewardedVideoAd ad_not_found") + result.error( + "ad_not_found", + "dispose failed for rewardedVideo ad, not found ad", + null + ) + } + } + } + + private fun callGetRewardedAdsState(id: Int?, result: MethodChannel.Result) { + if (id == null) { + Logger.w("Ads", "applovin native callGetRewardedAdsState no_hash_id") + result.error("no_hash_id", "get Rewarded STATE lose hash id", null) + } else { + val ad = RewardedVideoAd.getAdForId(id) + if (ad != null) { + result.success(ad.getState()) + } else { + result.error( + "ad_not_found", + "get ad state for rewardedVideo ad, not found ad", + null + ) + } + } + } + + private fun callDisposeInterstitialAd(id: Int?, result: MethodChannel.Result) { + if (id == null) { + Logger.w("Ads", "applovin native callDisposeInterstitialAd no_hash_id") + result.error("no_hash_id", "dispose interstitialAd lose hash id", null) + } else { + val ad = InterstitialAd.getAdForId(id) + if (ad != null) { + ad.dispose() + result.success(true) + } else { + Logger.w("Ads", "applovin native callDisposeInterstitialAd ad_not_found") + result.error( + "ad_not_found", + "dispose failed for interstitialAd ad, not found ad", + null + ) + } + } + } + + private fun callGetInterstitialAdsState(id: Int?, result: MethodChannel.Result) { + if (id == null) { + Logger.w("Ads", "applovin native callGetInterstitialAdsState no_hash_id") + result.error("no_hash_id", "get Interstitial STATE lose hash id", null) + } else { + val ad = InterstitialAd.getAdForId(id) + if (ad != null) { + val state = ad.getState() + Logger.d( + "Ads", + "applovin native callGetInterstitialAdsState[${ad.adUnitId}] ad.getState() = $state" + ) + result.success(state) + } else { + Logger.w("Ads", "applovin native callGetInterstitialAdsState ad_not_found") + result.error( + "ad_not_found", + "getAdState for Interstitial ad, not found ad", + null + ) + } + } + } + + private fun callAfterAcceptPrivacy(methodCall: MethodCall, result: MethodChannel.Result) { +// when (methodCall.argument("consentResult")) { +// true -> { +// AdRegistration.setConsentStatus(AdRegistration.ConsentStatus.EXPLICIT_YES) +// } +// false -> { +// AdRegistration.setConsentStatus(AdRegistration.ConsentStatus.EXPLICIT_NO) +// } +// else -> { +// AdRegistration.setConsentStatus(AdRegistration.ConsentStatus.UNKNOWN) +// } +// } + Logger.i("Ads", "applovin native callAfterAcceptPrivacy") + AppLovinPrivacySettings.setHasUserConsent(true, activity) + result.success(true) + } + + private fun callCheckConsentDialogStatus(result: MethodChannel.Result) { + val appLovinSdkConfiguration = AppLovinSdk.getInstance(AdHelp.activity).configuration + + when (appLovinSdkConfiguration.consentDialogState) { + AppLovinSdkConfiguration.ConsentDialogState.APPLIES -> { + result.success(ConsentResult.ShouldShow) + } + + AppLovinSdkConfiguration.ConsentDialogState.DOES_NOT_APPLY -> { + result.success(ConsentResult.GdprNotApplies) + } + + else -> { + result.success(ConsentResult.Unknown) + } + } + } + + private fun callHasUserConsent(result: MethodChannel.Result) { + result.success(AppLovinPrivacySettings.hasUserConsent(activity)) + } + + private fun callOpenDebugger(result: MethodChannel.Result) { + AppLovinSdk.getInstance(activity).showMediationDebugger() + result.success(true) + } + + private fun callIsTablet(result: MethodChannel.Result) { + if (activity == null) { + Logger.w("Ads", "applovin native callIsTablet activity_is_null") + result.error("no attach activity", "Activity is null!", null) + } + val isTablet = AppLovinSdkUtils.isTablet(activity) + result.success(isTablet) + } + + private fun callSetDebugMode(methodCall: MethodCall, result: MethodChannel.Result) { + if (activity == null) { + Logger.w("Ads", "applovin native callSetDebugMode activity_is_null") + result.error("no attach activity", "Activity is null!", null) + } + val debugLog = methodCall.argument("debug_mode") ?: false + AppLovinSdk.getInstance(activity).settings.setVerboseLogging(debugLog) + result.success(true) + } + + private fun callSetKeywords(methodCall: MethodCall, result: MethodChannel.Result) { + if (activity == null) { + Logger.w("Ads", "applovin native callSetKeywords activity_is_null") + result.error("no attach activity", "Activity is null!", null) + } + val keywords = methodCall.arguments>() + AppLovinSdk.getInstance(activity).targetingData.keywords = keywords + result.success(true) + Logger.w("Ads", "native setKeywords $keywords") + } + + private fun callClearTargetingData(methodCall: MethodCall, result: MethodChannel.Result) { + if (activity == null) { + Logger.w("Ads", "applovin native callClearTargetingData activity_is_null") + result.error("no attach activity", "Activity is null!", null) + } + AppLovinSdk.getInstance(activity).targetingData.clearAll() + result.success(true) + Logger.w("Ads", "native callClearTargetingData") + } + private fun callGetBannerAdSize(methodCall: MethodCall, result: MethodChannel.Result) { + if (activity == null) { + Logger.w("Ads", "applovin native callGetBannerAdSize activity_is_null") + result.error("no attach activity", "Activity is null!", null) + } + val isTablet = AppLovinSdkUtils.isTablet(activity) // Available on Android SDK 9.6.2+ + val heightPx = AppLovinSdkUtils.dpToPx(activity, if (isTablet) 90 else 50) + + val size = mapOf( + "width" to -1.0, + "height" to heightPx.toDouble() + ) + result.success(size) + Logger.w("Ads", "native callGetBannerAdSize $size") + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/InterstitialAd.kt b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/InterstitialAd.kt new file mode 100644 index 0000000..45665f1 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/InterstitialAd.kt @@ -0,0 +1,273 @@ +package flutter.guru.guru_applovin_flutter + +import android.app.Activity +import android.util.SparseArray +import com.amazon.device.ads.* +import com.applovin.mediation.* +import com.applovin.mediation.ads.MaxInterstitialAd +import flutter.guru.guru_applovin_flutter.log.Logger +import io.flutter.plugin.common.MethodChannel + + +/** + * Created by yaqiliu on 2020-03-06. + */ +class InterstitialAd(id: Int, channel: MethodChannel) : MaxAdListener, MaxAdRevenueListener { + + val id: Int = id + private val channel: MethodChannel = channel + var status: Int = AdStatus.CREATED + + private var interstitialAd: MaxInterstitialAd? = null + + private var isFirstLoad = true + + val adUnitId: String? + get() = interstitialAd?.adUnitId + + companion object { + + private val allAds: SparseArray = SparseArray() + + fun createInterstitial( + id: Int, + channel: MethodChannel + ): InterstitialAd { + var interstitialAd = getAdForId(id) + if (interstitialAd == null) { + interstitialAd = InterstitialAd( + id, + channel + ) + } + return interstitialAd + } + + fun getAdForId(id: Int): InterstitialAd? { + return allAds.get(id) + } + } + + init { + allAds.put(id, this) + } + + private fun logWarn(message: String) { + Logger.w("InterstitialAd", "[${interstitialAd?.adUnitId}] $message") + } + + private fun logDebug(message: String) { + Logger.d("InterstitialAd", "[${interstitialAd?.adUnitId}] $message") + } + + private fun logError(message: String) { + Logger.e("InterstitialAd", "[${interstitialAd?.adUnitId}] $message") + } + + internal fun load(activity: Activity, adUnitId: String, adAmazonSlotId: String?) { + status = AdStatus.LOADING + interstitialAd?.apply { + setListener(null) + setRevenueListener(null) + destroy() + } + interstitialAd = MaxInterstitialAd(adUnitId, activity) + interstitialAd?.apply { + setListener(this@InterstitialAd) + setRevenueListener(this@InterstitialAd) + } + if (!adAmazonSlotId.isNullOrBlank() && isFirstLoad) { + // APS LoadAd only needs to be called once. + isFirstLoad = false + val loader = DTBAdRequest() + loader.setSizes(DTBAdSize(320, 480, adAmazonSlotId)) + loader.loadAd(object : DTBAdCallback { + override fun onFailure(adError: AdError) { + logDebug( + "Amazon:Oops interstitial ad load has failed: ${adError.message}" + ) + interstitialAd?.setLocalExtraParameter("amazon_ad_error", adError) + interstitialAd?.loadAd() + } + + override fun onSuccess(dtbAdResponse: DTBAdResponse) { + logWarn("Amazon InterstitialAd load success") + interstitialAd?.setLocalExtraParameter("amazon_ad_response", dtbAdResponse) + interstitialAd?.loadAd() + } + }) + } else { + Logger.d( + "InterstitialAd", + "load interstitial ad $adUnitId" + ) + interstitialAd?.loadAd() + } + + } + + internal fun show(placement: String? = null): Boolean { + val result = interstitialAd?.let { + if (it.isReady) { + logWarn("show true!") + it.showAd(placement) + true + } else false + } ?: false + logWarn("show2 $result") + return result + } + + internal fun dispose() { + interstitialAd?.destroy() + logDebug("dispose") + allAds.remove(id) + } + + internal fun getState(): Int { + return when (status) { + AdStatus.LOADED -> { + if (interstitialAd?.isReady == true) { + AdStatus.LOADED + } else { + AdStatus.FAILED + } + } + + AdStatus.LOADING -> { + if (interstitialAd?.isReady == true) { + AdStatus.LOADED + } else { + AdStatus.LOADING + } + } + + else -> status + } + } + + override fun onAdLoadFailed(adUnitId: String?, err: MaxError) { + + // Interstitial ad failed to load. We recommend re-trying in 3 seconds. + val waterfallName = try { + err.waterfall?.let { waterfall -> + logWarn("Waterfall Name: " + waterfall.name + " and Test Name: " + waterfall.testName) + logWarn("Waterfall latency was: " + waterfall.latencyMillis + " milliseconds") + + for (networkResponse: MaxNetworkResponseInfo in waterfall.networkResponses) { + logWarn( + "Network -> " + networkResponse.mediatedNetwork + + "...latency: " + networkResponse.latencyMillis + + "...credentials: " + networkResponse.credentials + " milliseconds" + + "...error: " + networkResponse.error + ) + } + return@let waterfall.name + } ?: "unknown" + } catch (throwable: Throwable) { + logError("onAdLoadFailed error: ${throwable.message}") + "unknown" + } + status = AdStatus.FAILED + logWarn("onAdLoadFailed[$adUnitId] ${err.message}") + channel.invokeMethod( + "onInterstitialAdLoadFailed", + AdHelp.argumentsMapEx( + id, parameters = mapOf( + "errorCode" to err.code, + "ad_unit_name" to (adUnitId ?: ""), + "ad_waterfall_name" to waterfallName + ) + ) + ) + } + + override fun onAdClicked(ad: MaxAd?) { + channel.invokeMethod("onInterstitialAdClicked", AdHelp.argumentsMap(id)) + logDebug("onAdClicked") + } + + override fun onAdDisplayed(ad: MaxAd?) { + val parameters = mapOf( + "ad_revenue" to (ad?.revenue ?: -1.0), + "ad_format" to (ad?.format?.label ?: ""), + "ad_source" to (ad?.networkName ?: ""), + "ad_unit_name" to (ad?.adUnitId ?: ""), + "ad_creative_id" to (ad?.creativeId ?: ""), + "ad_network_name" to (ad?.networkName ?: "") + ) + channel.invokeMethod( + "onInterstitialAdDisplayed", AdHelp.argumentsMapEx( + id, parameters = parameters + ) + ) + logDebug("onAdDisplayed $parameters") + } + + override fun onAdLoaded(ad: MaxAd?) { + // Interstitial ad is ready to be shown. interstitialAd!!.isReady() will now return 'true' + val waterfallName = try { + ad?.waterfall?.let { waterfall -> + logWarn("Waterfall Name: " + waterfall.name + " and Test Name: " + waterfall.testName) + logWarn("Waterfall latency was: " + waterfall.latencyMillis + " milliseconds") + + for (networkResponse: MaxNetworkResponseInfo in waterfall.networkResponses) { + logWarn( + "Network -> " + networkResponse.mediatedNetwork + + "...latency: " + networkResponse.latencyMillis + + "...credentials: " + networkResponse.credentials + " milliseconds" + + "...error: " + networkResponse.error + ) + } + return@let waterfall.name + } ?: "unknown" + } catch (throwable: Throwable) { + logError("onAdLoadFailed error: ${throwable.message}") + "unknown" + } + val parameters = mapOf( + "ad_waterfall_name" to waterfallName, + ) + status = AdStatus.LOADED + channel.invokeMethod( + "onInterstitialAdLoaded", + AdHelp.argumentsMapEx(id, parameters = parameters) + ) + logDebug("onAdLoaded") + } + + override fun onAdHidden(ad: MaxAd?) { + // Interstitial ad is hidden. Pre-load the next ad + status = AdStatus.CREATED + channel.invokeMethod("onInterstitialAdHidden", AdHelp.argumentsMap(id)) + logDebug("onAdHidden") + } + + override fun onAdDisplayFailed(ad: MaxAd?, err: MaxError) { + // Interstitial ad failed to display. We recommend loading the next ad + status = AdStatus.FAILED + channel.invokeMethod( + "onInterstitialAdDisplayFailed", AdHelp.argumentsMapEx( + id, parameters = mapOf( + "errorCode" to err.code, + "ad_format" to (ad?.format?.label ?: ""), + "ad_source" to (ad?.networkName ?: ""), + "ad_unit_name" to (ad?.adUnitId ?: ""), + "ad_creative_id" to (ad?.creativeId ?: ""), + "ad_network_name" to (ad?.networkName ?: "") + ) + ) + ) + logDebug("onAdDisplayFailed") + + } + + override fun onAdRevenuePaid(ad: MaxAd?) { + channel.invokeMethod( + "onAdImpression", mapOf( + "payload" to AdHelp.toAdPayload(ad) + ) + ) + } + +} diff --git a/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/RewardedVideoAd.kt b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/RewardedVideoAd.kt new file mode 100644 index 0000000..84d086f --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/RewardedVideoAd.kt @@ -0,0 +1,276 @@ +package flutter.guru.guru_applovin_flutter + +import android.app.Activity +import android.util.Log +import android.util.SparseArray +import com.amazon.device.ads.AdError +import com.amazon.device.ads.DTBAdCallback +import com.amazon.device.ads.DTBAdRequest +import com.amazon.device.ads.DTBAdResponse +import com.amazon.device.ads.DTBAdSize.DTBVideo +import com.applovin.mediation.* +import com.applovin.mediation.ads.MaxRewardedAd +import flutter.guru.guru_applovin_flutter.AdHelp.argumentsMap +import flutter.guru.guru_applovin_flutter.AdHelp.argumentsMapEx +import flutter.guru.guru_applovin_flutter.log.Logger +import io.flutter.plugin.common.MethodChannel +import org.json.JSONObject + +/** + * Created by yaqiliu on 2020-03-05. + */ +class RewardedVideoAd(id: Int, channel: MethodChannel) : MaxRewardedAdListener, + MaxAdRevenueListener { + + val id: Int = id + private val channel: MethodChannel = channel + var status: Int = AdStatus.CREATED + private var isFirstLoad = true + private var rewardedAd: MaxRewardedAd? = null + + companion object { + + private val allAds: SparseArray = SparseArray() + + fun createRewardedVideo( + id: Int, + channel: MethodChannel + ): RewardedVideoAd { + var interstitialAd = getAdForId(id) + if (interstitialAd == null) { + interstitialAd = RewardedVideoAd( + id, + channel + ) + } + return interstitialAd + } + + fun getAdForId(id: Int): RewardedVideoAd? { + return allAds.get(id) + } + + + } + + init { + allAds.put(id, this) + } + + private fun logWarn(message: String) { + Logger.w("RewardedAd", "[${rewardedAd?.adUnitId}] $message") + } + + private fun logDebug(message: String) { + Logger.d("RewardedAd", "[${rewardedAd?.adUnitId}] $message") + } + + private fun logError(message: String) { + Logger.e("RewardedAd", "[${rewardedAd?.adUnitId}] $message") + } + + internal fun load(activity: Activity, adUnitId: String, amazonSlotId: String) { + status = AdStatus.LOADING + rewardedAd = MaxRewardedAd.getInstance(adUnitId, activity)?.apply { + setListener(this@RewardedVideoAd) + setRevenueListener(this@RewardedVideoAd) + } + if (isFirstLoad && amazonSlotId.isNotEmpty()) { + isFirstLoad = false + val loader = DTBAdRequest() + loader.setSizes(DTBVideo(320, 480, amazonSlotId)) + loader.loadAd(object : DTBAdCallback { + // APS bid returned + override fun onSuccess(dtbAdResponse: DTBAdResponse) { + logWarn("amazon reward ad load success") + rewardedAd?.setLocalExtraParameter("amazon_ad_response", dtbAdResponse) + rewardedAd?.loadAd() + } + + // No APS bid available + override fun onFailure(adError: AdError) { + logError("amazon reward ad load has failed: " + adError.message) + rewardedAd?.setLocalExtraParameter("amazon_ad_error", adError) + rewardedAd?.loadAd() + } + }) + } else { + rewardedAd?.loadAd() + } + + } + + internal fun show(placement: String? = null): Boolean { + val result = rewardedAd?.let { + if (it.isReady) { + logWarn("show true!") + it.showAd(placement) + true + } else false + } ?: false + logWarn("show2 $result") + return result + } + + internal fun dispose() { + rewardedAd?.destroy() + allAds.remove(id) + logDebug("dispose") + } + + internal fun getState(): Int { + return when (status) { + AdStatus.LOADED -> { + if (rewardedAd?.isReady == true) { + AdStatus.LOADED + } else { + AdStatus.FAILED + } + } + + AdStatus.LOADING -> { + if (rewardedAd?.isReady == true) { + AdStatus.LOADED + } else { + AdStatus.LOADING + } + } + + else -> status + } + } + + override fun onAdLoadFailed(adUnitId: String?, err: MaxError) { + // Rewarded ad failed to display. We recommend loading the next ad + val waterfallName = try { + err.waterfall?.let { waterfall -> + logWarn("Waterfall Name: " + waterfall.name + " and Test Name: " + waterfall.testName) + logWarn("Waterfall latency was: " + waterfall.latencyMillis + " milliseconds") + + for (networkResponse: MaxNetworkResponseInfo in waterfall.networkResponses) { + logWarn( + "Network -> " + networkResponse.mediatedNetwork + + "...latency: " + networkResponse.latencyMillis + + "...credentials: " + networkResponse.credentials + " milliseconds" + + "...error: " + networkResponse.error + ) + } + return@let waterfall.name + } ?: "unknown" + } catch (throwable: Throwable) { + logError("onAdLoadFailed error: ${throwable.message}") + "unknown" + } + status = AdStatus.FAILED + channel.invokeMethod( + "onRewardedVideoAdLoadFailed", argumentsMapEx( + id, parameters = mapOf( + "errorCode" to err.code, + "ad_unit_name" to (adUnitId ?: ""), + "ad_waterfall_name" to waterfallName + ) + ) + ) + logDebug("onAdLoadFailed $err") + } + + override fun onAdClicked(ad: MaxAd?) { + channel.invokeMethod("onRewardedVideoAdClicked", argumentsMap(id)) + logDebug("onAdClicked") + } + + override fun onAdDisplayed(ad: MaxAd?) { + val parameters = mapOf( + "ad_revenue" to (ad?.revenue ?: -1.0), + "ad_format" to (ad?.format?.label ?: ""), + "ad_source" to (ad?.networkName ?: ""), + "ad_unit_name" to (ad?.adUnitId ?: ""), + "ad_creative_id" to (ad?.creativeId ?: ""), + "ad_network_name" to (ad?.networkName ?: ""), + "payload" to AdHelp.toAdPayload(ad) + ) + channel.invokeMethod( + "onRewardedVideoAdDisplayed", AdHelp.argumentsMapEx( + id, parameters = parameters + ) + ) + logDebug("onAdDisplayed $parameters") + } + + override fun onRewardedVideoCompleted(ad: MaxAd?) { + channel.invokeMethod("onRewardedVideoCompleted", argumentsMap(id)) + logDebug("onRewardedVideoCompleted") + } + + override fun onUserRewarded(ad: MaxAd?, reward: MaxReward?) { + // Rewarded ad was displayed and user should receive the reward + channel.invokeMethod( + "onRewardedVideoUserRewarded", argumentsMap( + id, "rewardLabel", reward?.label + ?: "", "rewardAmount", reward?.amount ?: 0 + ) + ) + logDebug("onUserRewarded") + } + + override fun onRewardedVideoStarted(ad: MaxAd?) { + channel.invokeMethod("onRewardedVideoStarted", argumentsMap(id)) + logDebug("onUserRewarded") + } + + override fun onAdLoaded(ad: MaxAd?) { + // Rewarded ad is ready to be shown. rewardedAd!!.isReady() will now return 'true' + val waterfallName = try { + ad?.waterfall?.let { waterfall -> + logWarn("Waterfall Name: " + waterfall.name + " and Test Name: " + waterfall.testName) + logWarn("Waterfall latency was: " + waterfall.latencyMillis + " milliseconds") + + for (networkResponse: MaxNetworkResponseInfo in waterfall.networkResponses) { + logWarn( + "Network -> " + networkResponse.mediatedNetwork + + "...latency: " + networkResponse.latencyMillis + + "...credentials: " + networkResponse.credentials + " milliseconds" + + "...error: " + networkResponse.error + ) + } + return@let waterfall.name + } ?: "unknown" + } catch (throwable: Throwable) { + logError("onAdLoadFailed error: ${throwable.message}") + "unknown" + } + val parameters = mapOf( + "ad_waterfall_name" to waterfallName, + ) + status = AdStatus.LOADED + channel.invokeMethod("onRewardedVideoAdLoaded", argumentsMapEx(id, parameters = parameters)) + } + + override fun onAdHidden(ad: MaxAd?) { + // rewarded ad is hidden. Pre-load the next ad + status = AdStatus.CREATED + channel.invokeMethod("onRewardedVideoAdHidden", argumentsMap(id)) + logDebug("onAdHidden") + } + + override fun onAdDisplayFailed(ad: MaxAd?, err: MaxError) { + // Rewarded ad failed to display. We recommend loading the next ad + status = AdStatus.FAILED + channel.invokeMethod( + "onRewardedVideoAdDisplayFailed", AdHelp.argumentsMapEx( + id, parameters = mapOf( + "errorCode" to err.code + ) + ) + ) + logDebug("onAdDisplayFailed $err") + } + + override fun onAdRevenuePaid(ad: MaxAd?) { + channel.invokeMethod( + "onAdImpression", mapOf( + "payload" to AdHelp.toAdPayload(ad) + ) + ) + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/log/Formatter.kt b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/log/Formatter.kt new file mode 100644 index 0000000..5f1ca62 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/log/Formatter.kt @@ -0,0 +1,18 @@ +package flutter.guru.guru_applovin_flutter.log + +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.logging.Formatter +import java.util.logging.LogRecord + +class MainFormatter : Formatter() { + + private val dateTimeFormatter = SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.getDefault()) + + override fun format(record: LogRecord?): String { + if (record == null) return "" + val formatTime = dateTimeFormatter.format(record.millis) + val message = formatMessage(record) + return "$formatTime $message\n" + } +} \ No newline at end of file diff --git a/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/log/Logger.kt b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/log/Logger.kt new file mode 100644 index 0000000..9f0c691 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/log/Logger.kt @@ -0,0 +1,85 @@ +package flutter.guru.guru_applovin_flutter.log + +import android.content.Context +import android.os.Environment +import java.io.File +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicBoolean +import java.util.logging.FileHandler +import java.util.logging.Level +import java.util.logging.Logger + +object Logger { + + private const val verbose = 0 + private const val debug = 1 + private const val info = 2 + private const val warning = 3 + private const val error = 4 + private const val wtf = 5 + + private val levelTaps = arrayOf("V", "D", "I", "W", "E", "WTF") + + private const val LOGS_DIRECTORY = "guru/logs" + private val defaultLevel = object : Level("default", 2629, "guru.ads.max.flutter") {} + private val initialized = AtomicBoolean(false) + private var logger: Logger? = null + private val deliverExecutor = Executors.newSingleThreadExecutor() + + fun initialize(context: Context) { + if (!initialized.compareAndSet(false, true)) { + return + } + val resolvedLogsDirectory = File( + if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState() && !Environment.isExternalStorageRemovable()) { + context.getExternalFilesDir(null) + } else { + context.filesDir + }, LOGS_DIRECTORY + ).also { it.mkdirs() } + + val debugApp = SystemProperties.read("debug.guru.ads.max.flutter") + val forceDebug = true //debugApp == context.packageName + val filePattern = resolvedLogsDirectory.path + "/" + "flutter-max" + logger = Logger.getLogger("flutter-max").also { + kotlin.runCatching { + FileHandler(filePattern, 1024 * 1024 * 10, 7, true) + }.getOrNull()?.let { fileHandler -> + fileHandler.formatter = MainFormatter() + it.useParentHandlers = forceDebug + it.addHandler(fileHandler) + } + } + } + + private fun log(tag: String, level: Int, message: String) { + deliverExecutor.execute { + logger?.log(defaultLevel, "[$tag] ${levelTaps[level]} $message") + } + } + + fun v(tag: String, message: String) { + log(tag, verbose, message) + } + + fun d(tag: String, message: String) { + log(tag, debug, message) + } + + fun i(tag: String, message: String) { + log(tag, info, message) + } + + fun w(tag: String, message: String) { + log(tag, warning, message) + } + + fun e(tag: String, message: String) { + log(tag, error, message) + } + + fun wtf(tag: String, message: String) { + log(tag, wtf, message) + } + +} \ No newline at end of file diff --git a/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/log/SystemProperties.kt b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/log/SystemProperties.kt new file mode 100644 index 0000000..46fcf9b --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/log/SystemProperties.kt @@ -0,0 +1,38 @@ +package flutter.guru.guru_applovin_flutter.log + +import android.util.Log +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStreamReader + +object SystemProperties { + private const val GETPROP_EXECUTABLE_PATH = "/system/bin/getprop" + private const val TAG = "SystemProperties" + + fun read(propName: String): String { + var process: Process? = null + var bufferedReader: BufferedReader? = null + return try { + process = ProcessBuilder().command(GETPROP_EXECUTABLE_PATH, propName) + .redirectErrorStream(true).start() + bufferedReader = BufferedReader(InputStreamReader(process.inputStream)) + var line: String? = bufferedReader.readLine() + if (line == null) { + line = "" //prop not set + } + Log.i(TAG, "read System Property: $propName=$line") + line + } catch (e: Throwable) { + Log.e(TAG, "Failed to read System Property $propName", e) + "" + } finally { + if (bufferedReader != null) { + try { + bufferedReader.close() + } catch (e: IOException) { + } + } + process?.destroy() + } + } +} \ No newline at end of file diff --git a/guru_app/plugins/guru_applovin_flutter/example/.gitignore b/guru_app/plugins/guru_applovin_flutter/example/.gitignore new file mode 100644 index 0000000..0fa6b67 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/guru_app/plugins/guru_applovin_flutter/example/README.md b/guru_app/plugins/guru_applovin_flutter/example/README.md new file mode 100644 index 0000000..f1083f5 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/README.md @@ -0,0 +1,16 @@ +# guru_applovin_flutter_example + +Demonstrates how to use the guru_applovin_flutter plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/guru_app/plugins/guru_applovin_flutter/example/analysis_options.yaml b/guru_app/plugins/guru_applovin_flutter/example/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/guru_app/plugins/guru_applovin_flutter/example/android/.gitignore b/guru_app/plugins/guru_applovin_flutter/example/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/guru_app/plugins/guru_applovin_flutter/example/android/app/build.gradle b/guru_app/plugins/guru_applovin_flutter/example/android/app/build.gradle new file mode 100644 index 0000000..58a0ce6 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/android/app/build.gradle @@ -0,0 +1,57 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new FileNotFoundException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 31 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + applicationId "flutter.guru.guru_applovin_flutter_example" + minSdkVersion 21 + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/guru_app/plugins/guru_applovin_flutter/example/android/app/src/debug/AndroidManifest.xml b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..8a1fac6 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/AndroidManifest.xml b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..891647d --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/kotlin/flutter/guru/guru_applovin_flutter_example/MainActivity.kt b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/kotlin/flutter/guru/guru_applovin_flutter_example/MainActivity.kt new file mode 100644 index 0000000..fd5480d --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/kotlin/flutter/guru/guru_applovin_flutter_example/MainActivity.kt @@ -0,0 +1,6 @@ +package flutter.guru.guru_applovin_flutter_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/drawable-v21/launch_background.xml b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/drawable/launch_background.xml b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/values-night/styles.xml b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..3db14bb --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/values/styles.xml b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..d460d1e --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/example/android/app/src/profile/AndroidManifest.xml b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..8a1fac6 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/example/android/build.gradle b/guru_app/plugins/guru_applovin_flutter/example/android/build.gradle new file mode 100644 index 0000000..d140f42 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/android/build.gradle @@ -0,0 +1,33 @@ +buildscript { + ext.kotlin_version = '1.5.10' + repositories { + google() + jcenter() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.2.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + jcenter() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/guru_app/plugins/guru_applovin_flutter/example/android/gradle.properties b/guru_app/plugins/guru_applovin_flutter/example/android/gradle.properties new file mode 100644 index 0000000..250fd7d --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/android/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true +android.useAndroidX=true +android.enableJetifier=true +android.enableDexingArtifactTransform=false diff --git a/guru_app/plugins/guru_applovin_flutter/example/android/gradle/wrapper/gradle-wrapper.properties b/guru_app/plugins/guru_applovin_flutter/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ffed3a2 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/guru_app/plugins/guru_applovin_flutter/example/android/settings.gradle b/guru_app/plugins/guru_applovin_flutter/example/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/.gitignore b/guru_app/plugins/guru_applovin_flutter/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Flutter/AppFrameworkInfo.plist b/guru_app/plugins/guru_applovin_flutter/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..8d4492f --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 9.0 + + diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Flutter/Debug.xcconfig b/guru_app/plugins/guru_applovin_flutter/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Flutter/Release.xcconfig b/guru_app/plugins/guru_applovin_flutter/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Podfile b/guru_app/plugins/guru_applovin_flutter/example/ios/Podfile new file mode 100644 index 0000000..7d0e0f4 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project + platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' +source 'git@github.com:castbox/GuruSpecs.git' +source 'git@github.com:castbox/guru_applovin_flutter.git' +source 'https://github.com/CocoaPods/Specs.git' +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Podfile.lock b/guru_app/plugins/guru_applovin_flutter/example/ios/Podfile.lock new file mode 100644 index 0000000..74f9ed1 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/ios/Podfile.lock @@ -0,0 +1,447 @@ +PODS: + - Ads-Global/BUAdSDK_Compatible (5.4.0.8): + - Ads-Global/BURelyAdSDK + - Ads-Global/Dep_Compatible + - Ads-Global/BURelyAdSDK (5.4.0.8) + - Ads-Global/Dep_Compatible (5.4.0.8): + - BURelyFoundation_Global/Pangle (~> 0.1.2.5) + - AmazonPublisherServicesSDK (4.7.5) + - AppLovinMediationAmazonAdMarketplaceAdapter (4.7.5.0): + - AppLovinSDK + - AppLovinMediationBidMachineAdapter (2.3.0.2.0): + - AppLovinSDK + - BidMachine (= 2.3.0.2) + - AppLovinMediationByteDanceAdapter (5.4.0.8.0): + - Ads-Global/BUAdSDK_Compatible (= 5.4.0.8) + - AppLovinSDK + - AppLovinMediationChartboostAdapter (9.4.0.0): + - AppLovinSDK + - ChartboostSDK (= 9.4.0) + - AppLovinMediationFacebookAdapter (6.12.0.3): + - AppLovinSDK + - FBAudienceNetwork (= 6.12.0) + - AppLovinMediationFyberAdapter (8.2.4.0): + - AppLovinSDK + - Fyber_Marketplace_SDK (= 8.2.4) + - AppLovinMediationGoogleAdapter (10.9.0.0): + - AppLovinSDK + - Google-Mobile-Ads-SDK (= 10.9.0) + - AppLovinMediationGoogleAdManagerAdapter (10.9.0.0): + - AppLovinSDK + - Google-Mobile-Ads-SDK (= 10.9.0) + - AppLovinMediationInMobiAdapter (10.1.4.2): + - AppLovinSDK + - InMobiSDK/Core (= 10.1.4) + - AppLovinMediationIronSourceAdapter (7.4.0.0.1): + - AppLovinSDK + - IronSourceSDK (= 7.4.0.0) + - AppLovinMediationMintegralAdapter (7.4.2.0.0): + - AppLovinSDK + - MintegralAdSDK (= 7.4.2) + - MintegralAdSDK/BidSplashAd (= 7.4.2) + - AppLovinMediationMobileFuseAdapter (1.5.2.0): + - AppLovinSDK + - MobileFuseSDK (= 1.5.2) + - AppLovinMediationSmaatoAdapter (22.3.0.0): + - AppLovinSDK + - smaato-ios-sdk (= 22.3.0) + - smaato-ios-sdk/InApp (= 22.3.0) + - AppLovinMediationUnityAdsAdapter (4.8.0.1): + - AppLovinSDK + - UnityAds (= 4.8.0) + - AppLovinMediationVerveAdapter (2.19.0.0): + - AppLovinSDK + - HyBid (= 2.19.0) + - AppLovinMediationVungleAdapter (7.0.1.0): + - AppLovinSDK + - VungleAds (= 7.0.1) + - AppLovinPubMaticAdapter (1.1.0): + - AppLovinSDK (>= 11.2.1) + - OpenWrapSDK (>= 3.1.0) + - AppLovinSDK (11.11.2) + - BidMachine (2.3.0.2): + - BidMachine/Core (= 2.3.0.2) + - BidMachine/ApiCore (2.3.0.2) + - BidMachine/BiddingCore (2.3.0.2): + - BidMachine/ApiCore + - StackIAB (~> 2.2.1) + - StackModules (~> 1.5.1) + - BidMachine/Core (2.3.0.2): + - BidMachine/ApiCore + - BidMachine/BiddingCore + - BidMachine/ModulesCore + - BidMachine/ModulesCore (2.3.0.2): + - BidMachine/ApiCore + - BURelyFoundation_Global/AFNetworking (0.1.2.5) + - BURelyFoundation_Global/APM (0.1.2.5) + - BURelyFoundation_Global/Foundation (0.1.2.5): + - BURelyFoundation_Global/NETWork + - BURelyFoundation_Global/Gecko (0.1.2.5): + - BURelyFoundation_Global/Foundation + - BURelyFoundation_Global/Header (0.1.2.5) + - BURelyFoundation_Global/NETWork (0.1.2.5): + - BURelyFoundation_Global/AFNetworking + - BURelyFoundation_Global/Pangle (0.1.2.5): + - BURelyFoundation_Global/AFNetworking + - BURelyFoundation_Global/APM + - BURelyFoundation_Global/Foundation + - BURelyFoundation_Global/Gecko + - BURelyFoundation_Global/Header + - BURelyFoundation_Global/NETWork + - BURelyFoundation_Global/SDWebImage + - BURelyFoundation_Global/YYModel + - BURelyFoundation_Global/ZFPlayer + - BURelyFoundation_Global/Zip + - BURelyFoundation_Global/SDWebImage (0.1.2.5): + - BURelyFoundation_Global/Foundation + - BURelyFoundation_Global/YYModel (0.1.2.5) + - BURelyFoundation_Global/ZFPlayer (0.1.2.5): + - BURelyFoundation_Global/Foundation + - BURelyFoundation_Global/Zip + - BURelyFoundation_Global/Zip (0.1.2.5): + - BURelyFoundation_Global/Foundation + - ChartboostSDK (9.4.0) + - FBAudienceNetwork (6.12.0) + - Flutter (1.0.0) + - Fyber_Marketplace_SDK (8.2.4) + - Google-Mobile-Ads-SDK (10.9.0): + - GoogleAppMeasurement (< 11.0, >= 7.0) + - GoogleUserMessagingPlatform (>= 1.1) + - GoogleAppMeasurement (10.15.0): + - GoogleAppMeasurement/AdIdSupport (= 10.15.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30910.0, >= 2.30908.0) + - GoogleAppMeasurement/AdIdSupport (10.15.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 10.15.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30910.0, >= 2.30908.0) + - GoogleAppMeasurement/WithoutAdIdSupport (10.15.0): + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30910.0, >= 2.30908.0) + - GoogleUserMessagingPlatform (2.1.0) + - GoogleUtilities/AppDelegateSwizzler (7.11.5): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Environment (7.11.5): + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.11.5): + - GoogleUtilities/Environment + - GoogleUtilities/MethodSwizzler (7.11.5): + - GoogleUtilities/Logger + - GoogleUtilities/Network (7.11.5): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (7.11.5)" + - GoogleUtilities/Reachability (7.11.5): + - GoogleUtilities/Logger + - guru_applovin_flutter (2.3.0): + - AmazonPublisherServicesSDK (= 4.7.5) + - AppLovinMediationAmazonAdMarketplaceAdapter (= 4.7.5.0) + - AppLovinMediationBidMachineAdapter (= 2.3.0.2.0) + - AppLovinMediationByteDanceAdapter (= 5.4.0.8.0) + - AppLovinMediationChartboostAdapter (= 9.4.0.0) + - AppLovinMediationFacebookAdapter (= 6.12.0.3) + - AppLovinMediationFyberAdapter (= 8.2.4.0) + - AppLovinMediationGoogleAdapter (= 10.9.0.0) + - AppLovinMediationGoogleAdManagerAdapter (= 10.9.0.0) + - AppLovinMediationInMobiAdapter (= 10.1.4.2) + - AppLovinMediationIronSourceAdapter (= 7.4.0.0.1) + - AppLovinMediationMintegralAdapter (= 7.4.2.0.0) + - AppLovinMediationMobileFuseAdapter (= 1.5.2.0) + - AppLovinMediationSmaatoAdapter (= 22.3.0.0) + - AppLovinMediationUnityAdsAdapter (= 4.8.0.1) + - AppLovinMediationVerveAdapter (= 2.19.0.0) + - AppLovinMediationVungleAdapter (= 7.0.1.0) + - AppLovinPubMaticAdapter (= 1.1.0) + - AppLovinSDK (= 11.11.2) + - Flutter + - GuruConsent (= 1.4.1) + - OpenWrapSDK (= 3.1.0) + - GuruConsent (1.4.1): + - GoogleUserMessagingPlatform (= 2.1.0) + - HyBid (2.19.0): + - HyBid/Banner (= 2.19.0) + - HyBid/Core (= 2.19.0) + - HyBid/FullScreen (= 2.19.0) + - HyBid/Native (= 2.19.0) + - HyBid/RewardedVideo (= 2.19.0) + - HyBid/Banner (2.19.0): + - HyBid/Core + - HyBid/Core (2.19.0) + - HyBid/FullScreen (2.19.0): + - HyBid/Core + - HyBid/Native (2.19.0): + - HyBid/Core + - HyBid/RewardedVideo (2.19.0): + - HyBid/Core + - InMobiSDK/Core (10.1.4) + - IronSourceSDK (7.4.0.0) + - MintegralAdSDK (7.4.2): + - MintegralAdSDK/BannerAd (= 7.4.2) + - MintegralAdSDK/BidBannerAd (= 7.4.2) + - MintegralAdSDK/BidInterstitialVideoAd (= 7.4.2) + - MintegralAdSDK/BidNativeAd (= 7.4.2) + - MintegralAdSDK/BidNewInterstitialAd (= 7.4.2) + - MintegralAdSDK/BidRewardVideoAd (= 7.4.2) + - MintegralAdSDK/InterstitialVideoAd (= 7.4.2) + - MintegralAdSDK/NativeAd (= 7.4.2) + - MintegralAdSDK/NewInterstitialAd (= 7.4.2) + - MintegralAdSDK/RewardVideoAd (= 7.4.2) + - MintegralAdSDK/BannerAd (7.4.2): + - MintegralAdSDK/NativeAd + - MintegralAdSDK/BidBannerAd (7.4.2): + - MintegralAdSDK/BannerAd + - MintegralAdSDK/BidNativeAd + - MintegralAdSDK/BidInterstitialVideoAd (7.4.2): + - MintegralAdSDK/BidNativeAd + - MintegralAdSDK/InterstitialVideoAd + - MintegralAdSDK/BidNativeAd (7.4.2): + - MintegralAdSDK/NativeAd + - MintegralAdSDK/BidNewInterstitialAd (7.4.2): + - MintegralAdSDK/BidNativeAd + - MintegralAdSDK/NewInterstitialAd + - MintegralAdSDK/BidRewardVideoAd (7.4.2): + - MintegralAdSDK/BidNativeAd + - MintegralAdSDK/RewardVideoAd + - MintegralAdSDK/BidSplashAd (7.4.2): + - MintegralAdSDK/BidNativeAd + - MintegralAdSDK/SplashAd + - MintegralAdSDK/InterstitialVideoAd (7.4.2): + - MintegralAdSDK/NativeAd + - MintegralAdSDK/NativeAd (7.4.2) + - MintegralAdSDK/NewInterstitialAd (7.4.2): + - MintegralAdSDK/InterstitialVideoAd + - MintegralAdSDK/NativeAd + - MintegralAdSDK/RewardVideoAd (7.4.2): + - MintegralAdSDK/NativeAd + - MintegralAdSDK/SplashAd (7.4.2): + - MintegralAdSDK/NativeAd + - MobileFuseSDK (1.5.2) + - nanopb (2.30909.0): + - nanopb/decode (= 2.30909.0) + - nanopb/encode (= 2.30909.0) + - nanopb/decode (2.30909.0) + - nanopb/encode (2.30909.0) + - OMSDK_Appodeal (1.4.2.0) + - OpenWrapSDK (3.1.0): + - OpenWrapSDK/OpenWrap (= 3.1.0) + - OpenWrapSDK/OpenWrap (3.1.0) + - PromisesObjC (2.3.1) + - smaato-ios-sdk (22.3.0): + - smaato-ios-sdk/Full (= 22.3.0) + - smaato-ios-sdk/Banner (22.3.0): + - smaato-ios-sdk/Modules/Banner + - smaato-ios-sdk/Modules/Core + - smaato-ios-sdk/Modules/RichMedia + - smaato-ios-sdk/Full (22.3.0): + - smaato-ios-sdk/Banner + - smaato-ios-sdk/Interstitial + - smaato-ios-sdk/Native + - smaato-ios-sdk/Outstream + - smaato-ios-sdk/RewardedAds + - smaato-ios-sdk/InApp (22.3.0): + - smaato-ios-sdk/Modules/Core + - smaato-ios-sdk/Interstitial (22.3.0): + - smaato-ios-sdk/Modules/Interstitial + - smaato-ios-sdk/Modules/RichMedia + - smaato-ios-sdk/Modules/Video + - smaato-ios-sdk/Modules/Banner (22.3.0): + - smaato-ios-sdk/Modules/Core + - smaato-ios-sdk/Modules/Core (22.3.0) + - smaato-ios-sdk/Modules/Interstitial (22.3.0): + - smaato-ios-sdk/Modules/Core + - smaato-ios-sdk/Modules/Native (22.3.0): + - smaato-ios-sdk/Modules/Core + - smaato-ios-sdk/Modules/OpenMeasurement (22.3.0): + - smaato-ios-sdk/Modules/Core + - smaato-ios-sdk/Modules/Outstream (22.3.0): + - smaato-ios-sdk/Modules/Banner + - smaato-ios-sdk/Modules/Core + - smaato-ios-sdk/Modules/RichMedia + - smaato-ios-sdk/Modules/RewardedAds (22.3.0): + - smaato-ios-sdk/Modules/Core + - smaato-ios-sdk/Modules/RichMedia (22.3.0): + - smaato-ios-sdk/Modules/Core + - smaato-ios-sdk/Modules/OpenMeasurement + - smaato-ios-sdk/Modules/Video (22.3.0): + - smaato-ios-sdk/Modules/Core + - smaato-ios-sdk/Modules/OpenMeasurement + - smaato-ios-sdk/Native (22.3.0): + - smaato-ios-sdk/Modules/Core + - smaato-ios-sdk/Modules/Native + - smaato-ios-sdk/Outstream (22.3.0): + - smaato-ios-sdk/Modules/Outstream + - smaato-ios-sdk/RewardedAds (22.3.0): + - smaato-ios-sdk/Modules/RewardedAds + - smaato-ios-sdk/Modules/Video + - StackIAB (2.2.2): + - StackIAB/StackIABAssets (= 2.2.2) + - StackIAB/StackMRAIDKit (= 2.2.2) + - StackIAB/StackNASTKit (= 2.2.2) + - StackIAB/StackOpenMeasure (= 2.2.2) + - StackIAB/StackRichMedia (= 2.2.2) + - StackIAB/StackVASTAssets (= 2.2.2) + - StackIAB/StackVASTKit (= 2.2.2) + - StackIAB/StackVideoPlayer (= 2.2.2) + - StackIAB/StackXML (= 2.2.2) + - StackModules (~> 1.5.0) + - StackIAB/StackIABAssets (2.2.2): + - StackModules (~> 1.5.0) + - StackIAB/StackMRAIDKit (2.2.2): + - StackIAB/StackIABAssets + - StackIAB/StackOpenMeasure + - StackModules (~> 1.5.0) + - StackIAB/StackNASTKit (2.2.2): + - StackIAB/StackVASTKit + - StackModules (~> 1.5.0) + - StackIAB/StackOpenMeasure (2.2.2): + - OMSDK_Appodeal (~> 1.4.2.0) + - StackModules (~> 1.5.0) + - StackIAB/StackRichMedia (2.2.2): + - StackIAB/StackIABAssets + - StackIAB/StackVASTAssets + - StackIAB/StackVideoPlayer + - StackModules (~> 1.5.0) + - StackIAB/StackVASTAssets (2.2.2): + - StackIAB/StackIABAssets + - StackIAB/StackXML + - StackModules (~> 1.5.0) + - StackIAB/StackVASTKit (2.2.2): + - StackIAB/StackMRAIDKit + - StackIAB/StackVASTAssets + - StackIAB/StackVideoPlayer + - StackModules (~> 1.5.0) + - StackIAB/StackVideoPlayer (2.2.2): + - StackModules (~> 1.5.0) + - StackIAB/StackXML (2.2.2): + - StackModules (~> 1.5.0) + - StackModules (1.5.2): + - StackModules/StackProductPresentation (= 1.5.2) + - StackModules/StackFoundation (1.5.2) + - StackModules/StackProductPresentation (1.5.2): + - StackModules/StackUIKit + - StackModules/StackUIKit (1.5.2): + - StackModules/StackFoundation + - UnityAds (4.8.0) + - VungleAds (7.0.1) + +DEPENDENCIES: + - Flutter (from `Flutter`) + - guru_applovin_flutter (from `.symlinks/plugins/guru_applovin_flutter/ios`) + +SPEC REPOS: + "git@github.com:castbox/GuruSpecs.git": + - GuruConsent + https://github.com/CocoaPods/Specs.git: + - Ads-Global + - AmazonPublisherServicesSDK + - AppLovinMediationAmazonAdMarketplaceAdapter + - AppLovinMediationBidMachineAdapter + - AppLovinMediationByteDanceAdapter + - AppLovinMediationChartboostAdapter + - AppLovinMediationFacebookAdapter + - AppLovinMediationFyberAdapter + - AppLovinMediationGoogleAdapter + - AppLovinMediationGoogleAdManagerAdapter + - AppLovinMediationInMobiAdapter + - AppLovinMediationIronSourceAdapter + - AppLovinMediationMintegralAdapter + - AppLovinMediationMobileFuseAdapter + - AppLovinMediationSmaatoAdapter + - AppLovinMediationUnityAdsAdapter + - AppLovinMediationVerveAdapter + - AppLovinMediationVungleAdapter + - AppLovinPubMaticAdapter + - AppLovinSDK + - BidMachine + - BURelyFoundation_Global + - ChartboostSDK + - FBAudienceNetwork + - Fyber_Marketplace_SDK + - Google-Mobile-Ads-SDK + - GoogleAppMeasurement + - GoogleUserMessagingPlatform + - GoogleUtilities + - HyBid + - InMobiSDK + - IronSourceSDK + - MintegralAdSDK + - MobileFuseSDK + - nanopb + - OMSDK_Appodeal + - OpenWrapSDK + - PromisesObjC + - smaato-ios-sdk + - StackIAB + - StackModules + - UnityAds + - VungleAds + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + guru_applovin_flutter: + :path: ".symlinks/plugins/guru_applovin_flutter/ios" + +SPEC CHECKSUMS: + Ads-Global: 042c8262e8902f9d13988e116b82e18173a41b9f + AmazonPublisherServicesSDK: 35889640d3edabe3e0102366a47d20e46a82348c + AppLovinMediationAmazonAdMarketplaceAdapter: 2fc41a22e356e1b401509a9590f89d0fe1a57fb0 + AppLovinMediationBidMachineAdapter: 4743259ff12dd57977eb977e8f239f4c7ee98efb + AppLovinMediationByteDanceAdapter: 5d3434aafabcc9097627c51b55ac8fc448f8bd43 + AppLovinMediationChartboostAdapter: f22b520cda48f284efcd2273b033afc5cc910ab8 + AppLovinMediationFacebookAdapter: 296b4b4faeda8b70103ae5c4134b5604082defbd + AppLovinMediationFyberAdapter: 3e8e8b80704236c233e12028373bf58d514ee910 + AppLovinMediationGoogleAdapter: 74543937be18babf0999a3bec23ea7c4f8dfa386 + AppLovinMediationGoogleAdManagerAdapter: be8e256a93c7364998f5c2348169b38d886d294b + AppLovinMediationInMobiAdapter: f6852d200d3c25525d3cb5497f367bb01120e5a9 + AppLovinMediationIronSourceAdapter: c03d6154492507fca4f84af53b4b0ef8f616cc09 + AppLovinMediationMintegralAdapter: 7c2178087e5549d663ac5d06ef9ad2bc5329a630 + AppLovinMediationMobileFuseAdapter: 3fc5f3a610277ebf89375c18835a306db4ea8baf + AppLovinMediationSmaatoAdapter: cca90a93f4f8452bda37866a000015e6bf93cf5b + AppLovinMediationUnityAdsAdapter: b372cde7ee94362aed39cc4bf46408cd978f35d0 + AppLovinMediationVerveAdapter: fc9abbb83e8a1b342cbf2371041e7f65131b8ada + AppLovinMediationVungleAdapter: fa3e9ef18d47419db9cbabdcc7662ada68b8d9f8 + AppLovinPubMaticAdapter: 226f6f913ae140856d60affdc7cbad832a6dd4af + AppLovinSDK: 86ac2d11e3fda1d2cb5b235fb8a4bbd39ab0ebee + BidMachine: cfb3e056683e0dea0bf597dbca9865f420b00c99 + BURelyFoundation_Global: cd9e09f6c9c1fc42944af4fdaef0b60a24a0bc39 + ChartboostSDK: 53079774b4cd6aefd0805ec54a74f1ffd75b1317 + FBAudienceNetwork: e0fcc9091fced34910ed0b6da06f129db46ac9e6 + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + Fyber_Marketplace_SDK: c51474f63c016c9ff7eb32e3542528d08d456bc5 + Google-Mobile-Ads-SDK: e81e8b009a182dc8dd14951782efdbb30a5e4510 + GoogleAppMeasurement: 722db6550d1e6d552b08398b69a975ac61039338 + GoogleUserMessagingPlatform: dce302b8f1b84d6e945812ee7a15c3f65a102cbf + GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084 + guru_applovin_flutter: ef136ebed21efe4d0817c645d2b869be2b178bf5 + GuruConsent: fb173787a907eed8c4140416c93192ac1d8f012e + HyBid: 3863d3b20f480153029994da69b9591bf4693aa2 + InMobiSDK: 285be689e0e92f68d436f0486ddc476f086d60c8 + IronSourceSDK: 31bf34fdf92756dde41c00d10e3d3344461cbe2e + MintegralAdSDK: a7da9c65b3dd69e8194a7b26cec7068b2bade7cc + MobileFuseSDK: 0a23972f93ec5ec60901bb0a13f11018f53f7a36 + nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 + OMSDK_Appodeal: c3e648a353665dc4c0a8235d6e7efa66b8277ab5 + OpenWrapSDK: f4979a24f5d294f36c8ed18080ccac152507ec5c + PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 + smaato-ios-sdk: bfc97340dd58261a0d71708d2e9efb6477b84d1f + StackIAB: ad28f8fa22c029df7648152a3bcbd47f135494bd + StackModules: d737d6bc0453569434977fc6e589fc12cc3a2c27 + UnityAds: 390144b2029ac9ce2ec81c3cfd0007686b59236a + VungleAds: 49e2cbb981c1efa0086029d2dcec8a7322105809 + +PODFILE CHECKSUM: 672476cb8882d4d113bb92653fa5554311bd2e4a + +COCOAPODS: 1.12.1 diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..3da4962 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,572 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + F54332B3DDB21521E4677CB9 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14EA893DC9F9E59181D92CCA /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 14EA893DC9F9E59181D92CCA /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 45EB77F5B00DA09D3E7180E2 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 66FB056186070269A88B3C48 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C27EB300ABE943E2472E2FA7 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F54332B3DDB21521E4677CB9 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 12E6DBE5802EC304F476B830 /* Pods */ = { + isa = PBXGroup; + children = ( + 66FB056186070269A88B3C48 /* Pods-Runner.debug.xcconfig */, + 45EB77F5B00DA09D3E7180E2 /* Pods-Runner.release.xcconfig */, + C27EB300ABE943E2472E2FA7 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 12E6DBE5802EC304F476B830 /* Pods */, + F42804AD035DF00C3925347C /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + F42804AD035DF00C3925347C /* Frameworks */ = { + isa = PBXGroup; + children = ( + 14EA893DC9F9E59181D92CCA /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + BC98033F27C8D086166C8506 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + F8B4E521141FC7E3A1E75849 /* [CP] Embed Pods Frameworks */, + 640CC3F771D49BD049934644 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 640CC3F771D49BD049934644 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + BC98033F27C8D086166C8506 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F8B4E521141FC7E3A1E75849 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 39253T242A; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = flutter.guru.guruApplovinFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 39253T242A; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = flutter.guru.guruApplovinFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 39253T242A; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = flutter.guru.guruApplovinFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c87d15a --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/AppDelegate.swift b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..28c6bf0 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..f091b6b Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..d0ef06e Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..c8f9ed8 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..75b2d16 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c4df70d Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Base.lproj/Main.storyboard b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Info.plist b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Info.plist new file mode 100644 index 0000000..92ba244 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Guru Applovin Flutter + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + guru_applovin_flutter_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Runner-Bridging-Header.h b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/guru_app/plugins/guru_applovin_flutter/example/lib/main.dart b/guru_app/plugins/guru_applovin_flutter/example/lib/main.dart new file mode 100644 index 0000000..ae96830 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/lib/main.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; + +import 'package:flutter/services.dart'; + + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + String _platformVersion = 'Unknown'; + + @override + void initState() { + super.initState(); + initPlatformState(); + } + + // Platform messages are asynchronous, so we initialize in an async method. + Future initPlatformState() async { + String platformVersion; + // Platform messages may fail, so we use a try/catch PlatformException. + // We also handle the message potentially returning null. + try { + platformVersion = 'Unknown platform version'; + // await GuruApplovinFlutter.platformVersion ?? 'Unknown platform version'; + } on PlatformException { + platformVersion = 'Failed to get platform version.'; + } + + // If the widget was removed from the tree while the asynchronous platform + // message was in flight, we want to discard the reply rather than calling + // setState to update our non-existent appearance. + if (!mounted) return; + + setState(() { + _platformVersion = platformVersion; + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: Center( + child: Text('Running on: $_platformVersion\n'), + ), + ), + ); + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/example/pubspec.lock b/guru_app/plugins/guru_applovin_flutter/example/pubspec.lock new file mode 100644 index 0000000..9511594 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/pubspec.lock @@ -0,0 +1,167 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.9.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.16.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + guru_applovin_flutter: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "2.3.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.12" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.12" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" +sdks: + dart: ">=2.17.0-0 <3.0.0" diff --git a/guru_app/plugins/guru_applovin_flutter/example/pubspec.yaml b/guru_app/plugins/guru_applovin_flutter/example/pubspec.yaml new file mode 100644 index 0000000..dd3d5f6 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/pubspec.yaml @@ -0,0 +1,84 @@ +name: guru_applovin_flutter_example +description: Demonstrates how to use the guru_applovin_flutter plugin. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: ">=2.12.2 <3.0.0" + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + guru_applovin_flutter: + # When depending on this package from a real application you should use: + # guru_applovin_flutter: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^1.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/guru_app/plugins/guru_applovin_flutter/example/test/widget_test.dart b/guru_app/plugins/guru_applovin_flutter/example/test/widget_test.dart new file mode 100644 index 0000000..6b9ca25 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/example/test/widget_test.dart @@ -0,0 +1,22 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +void main() { + // testWidgets('Verify Platform version', (WidgetTester tester) async { + // // Build our app and trigger a frame. + // await tester.pumpWidget(const MyApp()); + // + // // Verify that platform version is retrieved. + // expect( + // find.byWidgetPredicate( + // (Widget widget) => widget is Text && + // widget.data!.startsWith('Running on:'), + // ), + // findsOneWidget, + // ); + // }); +} diff --git a/guru_app/plugins/guru_applovin_flutter/ios/.gitignore b/guru_app/plugins/guru_applovin_flutter/ios/.gitignore new file mode 100644 index 0000000..0c88507 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/guru_app/plugins/guru_applovin_flutter/ios/Assets/.gitkeep b/guru_app/plugins/guru_applovin_flutter/ios/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/guru_app/plugins/guru_applovin_flutter/ios/Classes/AdHelper.swift b/guru_app/plugins/guru_applovin_flutter/ios/Classes/AdHelper.swift new file mode 100644 index 0000000..c52b598 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/ios/Classes/AdHelper.swift @@ -0,0 +1,64 @@ +// +// Created by KernelTea on 2022/1/6. +// + +import Foundation +import AppLovinSDK + +public class AdHelper { + + static func toFormatName(_ adFormat: MAAdFormat) -> String { + switch (adFormat) { + case MAAdFormat.banner: + return "BANNER" + case MAAdFormat.interstitial: + return "INTERSTITIAL" + case MAAdFormat.rewarded: + return "REWARDED" + case MAAdFormat.mrec: + return "MREC" + case MAAdFormat.rewardedInterstitial: + return "REWARDED_INTERSTITIAL" + case MAAdFormat.native: + return "NATIVE" + case MAAdFormat.leader: + return "LEADER" + case MAAdFormat.crossPromo: + return "CROSS_PROMO" + default: + return "UNKNOWN" + } + } + + static func toAdPayload(_ ad: MAAd) -> String { + + guard let config = ALSdk.shared()?.configuration else { + return "" + } + let payload: [String: Any] = [ + "ad_platform": "MAX", + "id": ad.creativeIdentifier ?? "", + "adunit_id": ad.adUnitIdentifier, + "adunit_name": ad.placement ?? ad.adUnitIdentifier, + "adunit_format": toFormatName(ad.format), + "adgroup_id": "", + "adgroup_name": "", + "adgroup_type": "", + "currency": "USD", + "country": config.countryCode, + "app_version": "", + "publisher_revenue": ad.revenue, + "network_name": ad.networkName, + "network_placement_id": ad.networkPlacement, + "precision": "publisher_defined", + ] + + do { + let jsonData = try JSONSerialization.data(withJSONObject: payload, options: .prettyPrinted) + return String(data: jsonData, encoding: .utf8) ?? "" + } catch { + print(error.localizedDescription) + } + return "" + } +} \ No newline at end of file diff --git a/guru_app/plugins/guru_applovin_flutter/ios/Classes/AdStatus.swift b/guru_app/plugins/guru_applovin_flutter/ios/Classes/AdStatus.swift new file mode 100644 index 0000000..5216434 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/ios/Classes/AdStatus.swift @@ -0,0 +1,58 @@ +// +// AdStatus.swift +// guru_applovin_flutter +// +// Created by yaqi liu on 2020/3/13. +// + +import Foundation + +class AdStatus { + static let CREATED = 0 + static let LOADING = 1 + static let FAILED = 2 + static let LOADED = 3 + static let HIDDEN = 4 +} + +class ConsentResult { + static let ShouldShow = 0 + static let NotShow = -1 + static let GdprNotApplies = -2 + static let Unknown = -3 +} + + +//public enum UMPRequestErrorCode : Int, @unchecked Sendable { +// +// +// ///< Internal error. +// case `internal` = 1 +// +// ///< The application's app ID is invalid. +// case invalidAppID = 2 +// +// ///< Network error communicating with Funding Choices. +// case network = 3 +// +// case misconfiguration = 4 +//} +// +///// Error codes used when loading and showing forms. +//public enum UMPFormErrorCode : Int, @unchecked Sendable { +// +// +// ///< Internal error. +// case `internal` = 5 +// +// ///< Form was already used. +// case alreadyUsed = 6 +// +// ///< Form is unavailable. +// case unavailable = 7 +// +// ///< Loading a form timed out. +// case timeout = 8 +// +// case invalidViewController = 9 +//} diff --git a/guru_app/plugins/guru_applovin_flutter/ios/Classes/BannerAd.swift b/guru_app/plugins/guru_applovin_flutter/ios/Classes/BannerAd.swift new file mode 100644 index 0000000..a6ba9e7 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/ios/Classes/BannerAd.swift @@ -0,0 +1,207 @@ +// +// Banner.swift +// guru_applovin_flutter +// +// Created by yaqi liu on 2020/3/13. +// + +import Foundation +import AppLovinSDK +import DTBiOSSDK + +public class BannerAd: NSObject, MAAdViewAdDelegate, MAAdRevenueDelegate { + var bannerAd: MAAdView? = nil + + var id: Int + private var channel: FlutterMethodChannel + var status: Int = AdStatus.CREATED + + var anchorOffset = 0.0 + var horizontalCenterOffset = 0.0 + var anchorType = 0 + + private static var allAds: [Int: BannerAd] = [:] + + static func createBannerAd(id: Int, channel: FlutterMethodChannel) -> BannerAd { + if let ad = getAdForId(id: id) { + return ad + } else { + return BannerAd(id: id, channel: channel) + } + } + + static func getAdForId(id: Int) -> BannerAd? { + return allAds[id] + } + + init(id: Int, channel: FlutterMethodChannel) { + self.id = id + self.channel = channel + self.anchorOffset = 0.0 + self.horizontalCenterOffset = 0.0 + self.anchorType = 0 + super.init() + BannerAd.allAds[id] = self + } + + func load(adUnitId: String, adAmazonSlotId: String, placement: String) { + status = AdStatus.LOADING + bannerAd = MAAdView(adUnitIdentifier: adUnitId) + bannerAd?.placement = placement + let delegate = UIApplication.shared.delegate as? FlutterAppDelegate + let rootViewController = delegate?.window.rootViewController + if let ad = bannerAd, let screen = rootViewController?.view { + ad.delegate = self + + screen.addSubview(ad) + + ad.translatesAutoresizingMaskIntoConstraints = false + + let bottomAnchor: NSLayoutYAxisAnchor + if #available(iOS 11.0, *) { + bottomAnchor = screen.safeAreaLayoutGuide.bottomAnchor + } else { + bottomAnchor = screen.bottomAnchor + } + var offset = anchorOffset + if (anchorType == 0) { + offset = -anchorOffset + } + let height = (UIDevice.current.userInterfaceIdiom == .pad) ? 90 : 50 + let width = UIScreen.main.bounds.width + NSLog("Ads !!!!!!!!! anchorType +\(anchorType) 2222222 \(anchorType == 0)") + NSLayoutConstraint.activate([ + ad.centerXAnchor.constraint(equalTo: screen.centerXAnchor, constant: CGFloat(horizontalCenterOffset)), + //anchorType == 0 ? guide.bottomAnchor : guide.topAnchor + ad.bottomAnchor.constraint(equalTo: bottomAnchor, constant: CGFloat(offset)), + ad.heightAnchor.constraint(equalToConstant: CGFloat(height)), + ad.widthAnchor.constraint(equalToConstant: CGFloat(width)) + ]) + + ad.tag = id + ad.isHidden = true + ad.stopAutoRefresh() + internalLoadAd(adAmazonSlotId: adAmazonSlotId) + } + } + + private func internalLoadAd(adAmazonSlotId: String) { + if (!adAmazonSlotId.isEmpty) { + let loader = DTBAdLoader() + loader.setAdSizes([DTBAdSize(bannerAdSizeWithWidth: 320, height: 50, andSlotUUID: adAmazonSlotId)!]) + loader.loadAd(self) + } else { + bannerAd?.loadAd() + } + } + + func show() { + if let bannerAd = bannerAd, bannerAd.isHidden{ + bannerAd.isHidden = false + bannerAd.startAutoRefresh() + } + } + + func hide() { + if let bannerAd = bannerAd, !bannerAd.isHidden{ + bannerAd.isHidden = true + bannerAd.stopAutoRefresh() + } + } + + func dispose() { + BannerAd.allAds.removeValue(forKey: id) + guard let ba = bannerAd else { + return + } + ba.stopAutoRefresh() + ba.delegate = nil + ba.isHidden = true + if let view = bannerAd!.viewWithTag(id) { + view.removeFromSuperview() + } + } + + // MARK: MAAdDelegate Protocol + + public func didLoad(_ ad: MAAd) { + let waterfallName = ad.waterfall.name + status = AdStatus.LOADED + channel.invokeMethod("onBannerAdLoaded", arguments: + ["id": id, + "ad_waterfall_name": waterfallName] as [String : Any]) + + } + + public func didFailToLoadAd(forAdUnitIdentifier adUnitIdentifier: String, withError error: MAError) { + + let waterfallName = error.waterfall?.name ?? "Unknown" + + status = AdStatus.FAILED + channel.invokeMethod("onBannerAdLoadFailed", + arguments: ["id": id, + "errorCode": error.code.rawValue, + "ad_waterfall_name": waterfallName] as [String : Any]) + + // Rewarded ad failed to load. We recommend re-trying in 3 seconds. + // DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + // self.rewardedAd?.load() + // } + } + + public func didDisplay(_ ad: MAAd) { + channel.invokeMethod("onBannerAdDisplayed", arguments: + ["id": id]) +// channel.invokeMethod("onBannerAdDisplayed", arguments: +// ["id" : id, +// "ad_revenue" : ad.revenue , +// "ad_source" : ad.networkName, +// "ad_format" : ad.format, +// "ad_unit_name" : ad.adUnitIdentifier]) + } + + public func didClick(_ ad: MAAd) { + channel.invokeMethod("onBannerAdClicked", arguments: ["id": id]) + } + + public func didHide(_ ad: MAAd) { + // Rewarded ad is hidden. Pre-load the next ad + // rewardedAd?.load() + status = AdStatus.HIDDEN + channel.invokeMethod("onBannerAdHidden", arguments: ["id": id]) + } + + public func didFail(toDisplay ad: MAAd, withError error: MAError) { + // Rewarded ad failed to display. We recommend loading the next ad + // rewardedAd?.load() + status = AdStatus.FAILED + channel.invokeMethod("onBannerAdDisplayFailed", + arguments: ["id": id, + "errorCode": error.code.rawValue]) + } + + public func didExpand(_ ad: MAAd) { + + } + + public func didCollapse(_ ad: MAAd) { + + } + + public func didPayRevenue(for ad: MAAd) { + channel.invokeMethod("onAdImpression", + arguments: ["payload": AdHelper.toAdPayload(ad)] + ) + } +} + +extension BannerAd: DTBAdCallback { + public func onSuccess(_ adResponse: DTBAdResponse!) { + bannerAd?.setLocalExtraParameterForKey("amazon_ad_response", value: adResponse) + bannerAd?.loadAd() + } + public func onFailure(_ error: DTBAdError, dtbAdErrorInfo: DTBAdErrorInfo!) { + bannerAd?.setLocalExtraParameterForKey("amazon_ad_error", value: dtbAdErrorInfo) + bannerAd?.loadAd() + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/ios/Classes/GDPR/Gdpr.swift b/guru_app/plugins/guru_applovin_flutter/ios/Classes/GDPR/Gdpr.swift new file mode 100644 index 0000000..5b3dec8 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/ios/Classes/GDPR/Gdpr.swift @@ -0,0 +1,127 @@ +// +// Gdpr.swift +// GoogleUtilities +// +// Created by cxb on 2023/5/23. +// + +import UIKit +import UserMessagingPlatform + +//NOT_REQUIRED 与REQUIRED sdk中status是相反的,这里转换成与安卓一致 +enum ConsentStatus :Int{ + // Consent Form Not Available + case NOT_AVAILABLE = -100; + // Unknown consent status. + case UNKNOWN = 0; + // User consent required but not yet obtained. + case NOT_REQUIRED = 1; + // User consent not required. For example, the user is not in the EEA or the UK. + case REQUIRED = 2; + // User consent obtained. Personalization not defined. + case OBTAINED = 3; + +} + +///android +//public @interface ConsentStatus { +// int UNKNOWN = 0; +// int NOT_REQUIRED = 1; +// int REQUIRED = 2; +// int OBTAINED = 3; +// } + +/// ios +//typedef NS_ENUM(NSInteger, UMPConsentStatus) { +// UMPConsentStatusUnknown = 0, ///< Unknown consent status. +// UMPConsentStatusRequired = 1, ///< User consent required but not yet obtained. +// UMPConsentStatusNotRequired = 2, ///< Consent not required. +// UMPConsentStatusObtained = +// 3, ///< User consent obtained, personalized vs non-personalized undefined. +//}; + + +//class Gdpr { +// static func initialize(debugGeography:Int,testDeviceId:String,_ completion: @escaping ((Swift.Result) -> Void)) { +// let parameters = UMPRequestParameters() +// // 设置未满同意年龄的标签。此处false表示用户达到年龄 +// //"135F6955-4691-4488-946F-2F627C3DE714" +// parameters.tagForUnderAgeOfConsent = false +// if(!testDeviceId.isEmpty && debugGeography != 0){ +// let debugSettings = UMPDebugSettings() +// debugSettings.testDeviceIdentifiers = [testDeviceId] +// debugSettings.geography = UMPDebugGeography(rawValue: debugGeography) ?? .disabled +// parameters.debugSettings = debugSettings +// } +// +// // 请求最新同意信息 +// UMPConsentInformation.sharedInstance.requestConsentInfoUpdate( +// with: parameters, +// completionHandler: { error in +// if let error = error { +// // 请求同意信息失败 +// completion(.failure(error)) +// +// } else { +// // 判断是否需要加载表单 +// if UMPConsentInformation.sharedInstance.formStatus == .available { +// loadForm(completion) +// +// } else { +// // 无表单需要加载 +// completion(.success(true)) +// } +// } +// } +// ) +// } +// +// private static func loadForm(_ completion: @escaping ((Swift.Result) -> Void)) { +// let information = UMPConsentInformation.sharedInstance +// +// // 加载表单 +// UMPConsentForm.load( +// completionHandler: { form, error in +// if let error = error { +// // 表单加载失败 +// completion(.failure(error)) +// +// } else { +// switch information.consentStatus { +// case .required: // 是否需要用户同意 +// guard let controller = UIViewController.top() else { +// completion(.success(false)) +// return +// } +// // 打开弹窗 +// form?.present( +// from: controller, +// completionHandler: { error in +// if let error = error { +// // 弹窗失败 +// completion(.failure(error)) +// +// } else { +// // 是否已同意 +// completion(.success(information.consentStatus == .obtained)) +// } +// } +// ) +// +// case .notRequired, .obtained: // 是否不需要用户同意。例如,用户不在 EEA 或英国 +// completion(.success(true)) +// +// default: +// completion(.success(false)) +// } +// } +// } +// ) +// } +// +// static func reset() { +// UMPConsentInformation.sharedInstance.reset() +// } +// +//} + diff --git a/guru_app/plugins/guru_applovin_flutter/ios/Classes/GDPR/UIViewControllerExtensions.swift b/guru_app/plugins/guru_applovin_flutter/ios/Classes/GDPR/UIViewControllerExtensions.swift new file mode 100644 index 0000000..24b9521 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/ios/Classes/GDPR/UIViewControllerExtensions.swift @@ -0,0 +1,78 @@ +// +// UIViewControllerExtensions.swift +// GoogleUtilities +// +// Created by cxb on 2023/5/23. +// + +import UIKit +extension UIApplication { + + var window: UIWindow? { + if #available(iOS 13.0, *) { + let windows: [UIWindow] = UIApplication.shared.connectedScenes.compactMap { scene in + guard let scene = scene as? UIWindowScene else { return nil } + guard scene.session.role == .windowApplication else { return nil } + guard let delegate = scene.delegate as? UIWindowSceneDelegate else { return nil } + guard let window = delegate.window else { return nil } + guard let window = window else { return nil } + return window + } + + if windows.isEmpty { + guard let delegate = UIApplication.shared.delegate else { return nil } + guard let window = delegate.window else { return nil } + return window + + } else { + return windows.first + } + + } else { + guard let delegate = UIApplication.shared.delegate else { return nil } + guard let window = delegate.window else { return nil } + return window + } + } +} +extension UIViewController { + + /// 获取Window下的顶层控制器 + open class func top(in window: UIWindow? = .none) -> UIViewController? { + return (window ?? UIApplication.shared.window)?.rootViewController?.top() + } + + open func top() -> UIViewController { + // presented view controller + if let controller = presentedViewController { + return controller.top() + } + + // UITabBarController + if let tabBarController = self as? UITabBarController, + let controller = tabBarController.selectedViewController { + return controller.top() + } + + // UINavigationController + if let navigationController = self as? UINavigationController, + let controller = navigationController.visibleViewController { + return controller.top() + } + + // UIPageController + if let pageViewController = self as? UIPageViewController, + pageViewController.viewControllers?.count == 1 , + let controller = pageViewController.viewControllers?.first { + return controller.top() + } + + // child view controller + // for subview in self.view?.subviews ?? [] { + // if let controller = subview.next as? UIViewController { + // return controller.top() + // } + // } + return self + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/ios/Classes/GuruApplovinFlutterPlugin.h b/guru_app/plugins/guru_applovin_flutter/ios/Classes/GuruApplovinFlutterPlugin.h new file mode 100644 index 0000000..a3b5f3e --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/ios/Classes/GuruApplovinFlutterPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface GuruApplovinFlutterPlugin : NSObject +@end diff --git a/guru_app/plugins/guru_applovin_flutter/ios/Classes/GuruApplovinFlutterPlugin.m b/guru_app/plugins/guru_applovin_flutter/ios/Classes/GuruApplovinFlutterPlugin.m new file mode 100644 index 0000000..8626b92 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/ios/Classes/GuruApplovinFlutterPlugin.m @@ -0,0 +1,15 @@ +#import "GuruApplovinFlutterPlugin.h" +#if __has_include() +#import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "guru_applovin_flutter-Swift.h" +#endif + +@implementation GuruApplovinFlutterPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftGuruApplovinFlutterPlugin registerWithRegistrar:registrar]; +} +@end diff --git a/guru_app/plugins/guru_applovin_flutter/ios/Classes/InterstitialAd.swift b/guru_app/plugins/guru_applovin_flutter/ios/Classes/InterstitialAd.swift new file mode 100644 index 0000000..f2dade6 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/ios/Classes/InterstitialAd.swift @@ -0,0 +1,165 @@ +// +// File.swift +// guru_applovin_flutter +// +// Created by yaqi liu on 2020/3/13. +// + +import Foundation +import AppLovinSDK +import DTBiOSSDK + +public class InterstitialAd: NSObject, MAAdDelegate, MAAdRevenueDelegate { + + + var interstitialAd: MAInterstitialAd? = nil + + var id: Int + private var channel: FlutterMethodChannel + var status: Int = AdStatus.CREATED + + var isFirstLoad = true + + private static var allAds: [Int : InterstitialAd] = [:] + + static func createInterstitialAd(id: Int, channel: FlutterMethodChannel) -> InterstitialAd { + if let ad = getAdForId(id: id) { + return ad + } else { + return InterstitialAd(id: id, channel: channel) + } + } + + static func getAdForId(id: Int) -> InterstitialAd? { + return allAds[id] + } + + init(id: Int, channel: FlutterMethodChannel) { + self.id = id + self.channel = channel + super.init() + InterstitialAd.allAds[id] = self + } + + func load(adUnitId: String, adAmazonSlotId: String) { + status = AdStatus.LOADING + interstitialAd = MAInterstitialAd(adUnitIdentifier: adUnitId) + interstitialAd?.delegate = self + interstitialAd?.revenueDelegate = self + + if (isFirstLoad && !adAmazonSlotId.isEmpty) { + isFirstLoad = false + let loader = DTBAdLoader() + loader.setAdSizes([DTBAdSize(videoAdSizeWithPlayerWidth: 320, height: 480, andSlotUUID: adAmazonSlotId)!]) + loader.loadAd(self) + } else { + interstitialAd?.load() + } + + } + + func show(placement: String) -> Bool { + if let ad = interstitialAd, + ad.isReady { + ad.show(forPlacement: placement) + return true + } + return false + } + + func dispose() { + } + + func getState() -> Int { + switch (status) { + case AdStatus.LOADED: + if (interstitialAd?.isReady == true) { + return status; + } else { + return AdStatus.FAILED; + } + case AdStatus.LOADING: + if (interstitialAd?.isReady == true) { + return AdStatus.LOADED; + } else { + return status; + } + default: + return status; + } + } + + // MARK: MAAdDelegate Protocol + public func didLoad(_ ad: MAAd) { + // Rewarded ad is ready to be shown. '[self.rewardedAd isReady]' will now return 'YES' + let waterfallName = ad.waterfall.name + status = AdStatus.LOADED + channel.invokeMethod("onInterstitialAdLoaded", arguments: ["id" : id, + "ad_waterfall_name": waterfallName] as [String : Any]) + } + + public func didFailToLoadAd(forAdUnitIdentifier adUnitIdentifier: String, withError error: MAError) { + let waterfallName = error.waterfall?.name ?? "Unknown" + status = AdStatus.FAILED + channel.invokeMethod("onInterstitialAdLoadFailed", + arguments: ["id" : id, + "errorCode" : error.code.rawValue, + "ad_waterfall_name": waterfallName] as [String : Any]) + + + // Rewarded ad failed to load. We recommend re-trying in 3 seconds. + // DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + // self.rewardedAd?.load() + // } + } + + public func didDisplay(_ ad: MAAd) { + let arguments: [String: Any] = ["id" : id, + "ad_revenue": ad.revenue, + "ad_source": ad.networkName, + "ad_format": AdHelper.toFormatName(ad.format), + "ad_unit_name": ad.adUnitIdentifier] + channel.invokeMethod("onInterstitialAdDisplayed", arguments: arguments) + } + + public func didClick(_ ad: MAAd) { + channel.invokeMethod("onInterstitialAdClicked", arguments: ["id" : id]) + } + + public func didHide(_ ad: MAAd) { + // Rewarded ad is hidden. Pre-load the next ad + // rewardedAd?.load() + status = AdStatus.CREATED + channel.invokeMethod("onInterstitialAdHidden", arguments: ["id" : id]) + } + + public func didFail(toDisplay ad: MAAd, withError error: MAError) + { + // Rewarded ad failed to display. We recommend loading the next ad + // rewardedAd?.load() + status = AdStatus.FAILED + channel.invokeMethod("onInterstitialAdDisplayFailed", + arguments: ["id" : id, + "errorCode" : error.code.rawValue]) + } + + public func didPayRevenue(for ad: MAAd) { + channel.invokeMethod("onAdImpression", + arguments: ["payload": AdHelper.toAdPayload(ad)] + ) + } + +} + +extension InterstitialAd: DTBAdCallback { + + public func onSuccess(_ adResponse: DTBAdResponse!) { + interstitialAd?.setLocalExtraParameterForKey("amazon_ad_response", value: adResponse) + interstitialAd?.load() + } + + public func onFailure(_ error: DTBAdError, dtbAdErrorInfo: DTBAdErrorInfo!) { + interstitialAd?.setLocalExtraParameterForKey("amazon_ad_error", value: dtbAdErrorInfo) + interstitialAd?.load() + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/ios/Classes/RewardedVideoAd.swift b/guru_app/plugins/guru_applovin_flutter/ios/Classes/RewardedVideoAd.swift new file mode 100644 index 0000000..f4b5248 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/ios/Classes/RewardedVideoAd.swift @@ -0,0 +1,187 @@ +// +// GuruRewardedVideoAd.swift +// Pods +// +// Created by mayue_work on 2020/3/12. +// + +import Foundation +import UIKit +import AppLovinSDK +import DTBiOSSDK + +public class RewardedVideoAd: NSObject, MARewardedAdDelegate, MAAdRevenueDelegate { + + var rewardedAd: MARewardedAd? = nil + var isFirstLoad = true + var id: Int + private var channel: FlutterMethodChannel + var status: Int = AdStatus.CREATED + + private static var allAds: [Int: RewardedVideoAd] = [:] + + static func createRewardedVideo(id: Int, channel: FlutterMethodChannel) -> RewardedVideoAd { + if let ad = getAdForId(id: id) { + return ad + } else { + return RewardedVideoAd(id: id, channel: channel) + } + } + + static func getAdForId(id: Int) -> RewardedVideoAd? { + return allAds[id] + } + + init(id: Int, channel: FlutterMethodChannel) { + self.id = id + self.channel = channel + super.init() + RewardedVideoAd.allAds[id] = self + } + + func load(adUnitId: String, amazonSlotId: String) { + status = AdStatus.LOADING + rewardedAd = MARewardedAd.shared(withAdUnitIdentifier: adUnitId) + rewardedAd?.delegate = self + rewardedAd?.revenueDelegate = self + // If first load - load ad from Amazon's SDK, then load ad for MAX + if isFirstLoad && amazonSlotId.isEmpty == false{ + isFirstLoad = false + let adLoader = DTBAdLoader() + adLoader.setAdSizes([DTBAdSize(videoAdSizeWithPlayerWidth: 320, height: 480, andSlotUUID: amazonSlotId)!]) + adLoader.loadAd(self) + } + else{ + rewardedAd?.load() + } + // Load the first ad + rewardedAd?.load() + } + + func show(placement: String) -> Bool { + if let ad = rewardedAd, + ad.isReady { + ad.show(forPlacement: placement) + return true + } + return false + } + + func dispose() { + } + + func getState() -> Int { + switch (status) { + case AdStatus.LOADED: + if (rewardedAd?.isReady == true) { + return status; + } else { + return AdStatus.FAILED; + } + case AdStatus.LOADING: + if (rewardedAd?.isReady == true) { + return AdStatus.LOADED; + } else { + return status; + } + default: + return status; + } + } + + // MARK: MAAdDelegate Protocol + + public func didLoad(_ ad: MAAd) { + // Rewarded ad is ready to be shown. '[self.rewardedAd isReady]' will now return 'YES' + let waterfallName = ad.waterfall.name + status = AdStatus.LOADED + channel.invokeMethod("onRewardedVideoAdLoaded", arguments: ["id": id, + "ad_waterfall_name": waterfallName] as [String : Any]) + } + + public func didFailToLoadAd(forAdUnitIdentifier adUnitIdentifier: String, + withError error: MAError) { + let waterfallName = error.waterfall?.name ?? "Unknown" + status = AdStatus.FAILED + channel.invokeMethod("onRewardedVideoAdLoadFailed", + arguments: ["id": id, + "errorCode": error.code.rawValue, + "ad_waterfall_name": waterfallName] as [String : Any]) + + + // Rewarded ad failed to load. We recommend re-trying in 3 seconds. +// DispatchQueue.main.asyncAfter(deadline: .now() + 3) { +// self.rewardedAd?.load() +// } + } + + public func didDisplay(_ ad: MAAd) { + let arguments: [String: Any] = ["id": id, + "ad_revenue": ad.revenue, + "ad_source": ad.networkName, + "ad_format": AdHelper.toFormatName(ad.format), + "ad_unit_name": ad.adUnitIdentifier] + channel.invokeMethod("onRewardedVideoAdDisplayed", arguments: arguments) + } + + public func didClick(_ ad: MAAd) { + channel.invokeMethod("onRewardedVideoAdClicked", arguments: ["id": id]) + } + + public func didHide(_ ad: MAAd) { + // Rewarded ad is hidden. Pre-load the next ad +// rewardedAd?.load() + status = AdStatus.CREATED + channel.invokeMethod("onRewardedVideoAdHidden", arguments: ["id": id]) + } + + public func didFail(toDisplay ad: MAAd, withError error: MAError) { + // Rewarded ad failed to display. We recommend loading the next ad +// rewardedAd?.load() + status = AdStatus.FAILED + channel.invokeMethod("onRewardedVideoAdDisplayFailed", + arguments: ["id": id,"errorCode": error.code.rawValue]) + } + + // MARK: MARewardedAdDelegate Protocol + + public func didStartRewardedVideo(for ad: MAAd) { + channel.invokeMethod("onRewardedVideoStarted", arguments: ["id": id]) + } + + public func didCompleteRewardedVideo(for ad: MAAd) { + channel.invokeMethod("onRewardedVideoCompleted", arguments: ["id": id]) + } + + public func didRewardUser(for ad: MAAd, with reward: MAReward) { + // Rewarded ad was displayed and user should receive the reward + channel.invokeMethod("onRewardedVideoUserRewarded", + arguments: ["id": id, + "rewardLabel": reward.label, + "rewardAmount": reward.amount]) + } + + public func didPayRevenue(for ad: MAAd) { + channel.invokeMethod("onAdImpression", + arguments: ["payload": AdHelper.toAdPayload(ad)] + ) + + } + +} + +extension RewardedVideoAd: DTBAdCallback{ + public func onSuccess(_ adResponse: DTBAdResponse!){ + // `interstitialAd` is your instance of MAInterstitialAd + NSLog("RewardAd amazon load success") + rewardedAd?.setLocalExtraParameterForKey("amazon_ad_response", value: adResponse) + rewardedAd?.load() + } + + public func onFailure(_ error: DTBAdError, dtbAdErrorInfo: DTBAdErrorInfo!){ + // `interstitialAd` is your instance of MAInterstitialAd + NSLog("RewardAd amazon load failed \(error.rawValue)") + rewardedAd?.setLocalExtraParameterForKey("amazon_ad_error", value: dtbAdErrorInfo) + rewardedAd?.load() + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/ios/Classes/SwiftGuruApplovinFlutterPlugin.swift b/guru_app/plugins/guru_applovin_flutter/ios/Classes/SwiftGuruApplovinFlutterPlugin.swift new file mode 100644 index 0000000..bc0b62a --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/ios/Classes/SwiftGuruApplovinFlutterPlugin.swift @@ -0,0 +1,560 @@ +import Flutter +import UIKit +import AppLovinSDK +import DTBiOSSDK +import GuruConsent +import FBAudienceNetwork +import OpenWrapSDK + +public class SwiftGuruApplovinFlutterPlugin: NSObject, FlutterPlugin { + + private var channel: FlutterMethodChannel? = nil + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "guru_applovin_flutter", binaryMessenger: registrar.messenger()) + let instance = SwiftGuruApplovinFlutterPlugin() + instance.channel = channel + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? [String: Any] ?? [String: Any]() + let id = arguments["id"] as? Int + NSLog("applovin handle") + switch call.method { + case "initialize": + callInitialize(arguments: arguments, result: result) + break + case "gatherConsentAndInitialize": + callGatherConsentAndInitialize(arguments: arguments, result: result) + break + case "requestGdpr": + callRequestGdpr(arguments: arguments,result:result) + case "resetGdpr": + callRestGdpr(result:result) + case "showPrivacyOptionsForm": + callShowPrivacyOptionsForm(arguments: arguments, result: result) + case "openDebugger": + openDebugger(result: result) + case "loadRewardedVideoAd": + callLoadRewardedVideoAd(id: id, call, result: result) + break + case "showRewardedVideoAd": + callShowRewardedVideoAd(id: id, call, result: result) + break + case "loadInterstitialAd": + callLoadInterstitialAd(id: id, call, result: result) + break + case "showInterstitialAd": + callShowInterstitialAd(id: id, call, result: result) + break + case "isInterstitialAdLoaded": + callInterstitialAdLoaded(id: id, result: result) + break + case "loadBannerAd": + callLoadBannerAd(id: id, call, result: result) + break + case "showBannerAd": + callShowBannerAd(id: id, call, result: result) + break + case "hideBannerAd": + callHideBannerAd(id: id, result: result) + break + case "disposeBannerAd": + callDisposeBannerAd(id: id, result: result) + break + case "disposeInterstitialAd": + callDisposeInterstitialAd(id: id, result: result) + break + case "disposeRewardedAd": + callDisposeRewardedAd(id: id, result: result) + break + case "afterAcceptPrivacy": + callAfterAcceptPrivacy(arguments: arguments, result: result) + break + case "getInterstitialAdState": + callGetInterstitialAdsState(id: id, result: result) + break; + case "getRewardedAdState": + callGetRewardedAdsState(id: id, result: result) + break; + case "checkConsentDialogStatus": + callCheckConsentDialogStatus(result: result) + break + case "hasUserConsent": + callHasUserConsent(result: result) + case "isTablet": + callIsTablet(result: result) + break + case "setKeywords": + callSetKeywords(call, result: result) + break + case "clearTargetingData": + callClearTargetingData(call, result: result) + break; + case "getBannerAdSize": + callGetBannerAdSize(call, result: result) + break; + default: + result(FlutterMethodNotImplemented) + } + } + + private func callInitialize(arguments: [String: Any], result: @escaping FlutterResult) { + FBAdSettings.setAdvertiserTrackingEnabled(true) + let debugLog = arguments["debug_mode"] as? Bool ?? false + //init amazon + let amazonAppId = arguments["amazon_appId"] as? String ?? "" + if (amazonAppId != "") { + DTBAds.sharedInstance().setAppKey(amazonAppId) + DTBAds.sharedInstance().setAdNetworkInfo(DTBAdNetworkInfo.init(networkName: DTBADNETWORK_MAX)) + DTBAds.sharedInstance().mraidCustomVersions = ["1.0", "2.0", "3.0"] + DTBAds.sharedInstance().mraidPolicy = CUSTOM_MRAID + DTBAds.sharedInstance().useGeoLocation = true + + if (debugLog) { + DTBAds.sharedInstance().setLogLevel(DTBLogLevelAll) + } + // DTBAds.sharedInstance().testMode = true + } + + let pubmaticStoreUrl = arguments["pubmatic_store_url"] as? String ?? "" + if(pubmaticStoreUrl != ""){ + let appInfo = POBApplicationInfo() + appInfo.storeURL = URL(string: pubmaticStoreUrl)! + OpenWrapSDK.setApplicationInfo(appInfo) + } + + let userId = arguments["user_id"] as? String ?? "" + if (userId != "") { + ALSdk.shared()!.userIdentifier = userId + NSLog("userIdentifier SET Success \(userId)") + } else { + NSLog("userIdentifier Is Null") + } + ALSdk.shared()!.mediationProvider = "max" + ALSdk.shared()!.settings.isMuted = true + ALSdk.shared()!.initializeSdk { (configuration: ALSdkConfiguration) in + // AppLovin SDK is initialized, start loading ads + result(true) + } + } + + private func callRequestGdpr(arguments: [String: Any],result: @escaping FlutterResult) { + let testGeography = arguments["debug_geography"] as? Int + let testDeviceId = arguments["test_device_id"] as? String + guard let controller = UIViewController.top() else { + result(ConsentStatus.UNKNOWN.rawValue) + return + } + + if let debugGeography = testGeography,let debugDeviceId = testDeviceId{ + + var debug = GuruConsent.DebugSettings() + debug.testDeviceIdentifiers = [debugDeviceId] + debug.geography = GuruConsent.DebugSettings.Geography(rawValue: debugGeography) ?? GuruConsent.DebugSettings.Geography.EEA + GuruConsent.debug = debug + } + GuruConsent.start(from: controller) { res in + switch res{ + case .success(let value): + print("gdpr result\(value)") + switch value{ + case .obtained: + result(ConsentStatus.OBTAINED.rawValue) + case .notRequired: + result(ConsentStatus.NOT_REQUIRED.rawValue) + case .required: + result(ConsentStatus.REQUIRED.rawValue) + case .unknown: + result(ConsentStatus.UNKNOWN.rawValue) + default: + result(ConsentStatus.UNKNOWN.rawValue) + } + case.failure(let error): + result(ConsentStatus.UNKNOWN.rawValue) + } + } + + } + + func callGatherConsentAndInitialize(arguments: [String: Any], result: @escaping FlutterResult){ + let testGeography = arguments["debug_geography"] as? Int + let testDeviceId = arguments["test_device_id"] as? String + + guard let controller = UIViewController.top() else { + result(ConsentStatus.UNKNOWN.rawValue) + return + } + if(GuruConsent.canRequestAds){ + self.callInitialize(arguments: arguments, result: result) + return; + } + + if let debugGeography = testGeography,let debugDeviceId = testDeviceId{ + + var debug = GuruConsent.DebugSettings() + debug.testDeviceIdentifiers = [debugDeviceId] + debug.geography = GuruConsent.DebugSettings.Geography(rawValue: debugGeography) ?? GuruConsent.DebugSettings.Geography.EEA + GuruConsent.debug = debug + } + GuruConsent.start(from: controller) { res in + switch res{ + case .success(let value): + print("gdpr result\(value)") + + + case.failure(let error): +// result(false) + print("\(error)") + } + if(GuruConsent.canRequestAds){ + self.callInitialize(arguments: arguments, result: result) + }else{ + result(false); + } + } + + } + + private func callShowPrivacyOptionsForm(arguments: [String: Any], result: @escaping FlutterResult){ + guard let controller = UIViewController.top() else { + result(ConsentStatus.UNKNOWN.rawValue) + return + } + GuruConsent.openPrivacyOptions(from: controller) { error in + result(ConsentStatus.UNKNOWN.rawValue) + } + } + + private func callRestGdpr(result: @escaping FlutterResult) { + GuruConsent.reset() + DispatchQueue.global().async { + result(true) + } + } + + private func openDebugger(result: @escaping FlutterResult) { + ALSdk.shared()!.showMediationDebugger() + DispatchQueue.global().async { + result(true) + } + } + + private func callLoadRewardedVideoAd(id: Int?, _ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let id = id else { + result(FlutterError(code: "no_hash_id", message: "load rewardedVideoAd lose hash id", details: nil)) + return + } + + guard let channel = channel else { + result(FlutterError(code: "no_channel", message: "load rewardedVideoAd lose channel", details: nil)) + return + } + + let rewardedVideoAd = RewardedVideoAd.createRewardedVideo(id: id, channel: channel) + if (rewardedVideoAd.status == AdStatus.LOADED) { + result(true) + return + } + + let arguments = call.arguments as? [String: Any] ?? [String: Any]() + guard let adUnitId = arguments["adUnitId"] as? String, adUnitId.isEmpty == false else { + result(FlutterError(code: "no_adunit_id", message: "a null or empty adUnitId was provided for rewarded video id=\(rewardedVideoAd.id)", details: nil)) + return + } + let amazonSlotId = arguments["adAmazonSlotId"] as? String ?? "" + rewardedVideoAd.load(adUnitId: adUnitId, amazonSlotId: amazonSlotId) + result(true) + } + + private func callShowRewardedVideoAd(id: Int?, _ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let id = id else { + result(FlutterError(code: "no_hash_id", message: "show rewardedVideoAd lose hash id", details: nil)) + return + } + + guard let rewardedVideoAd = RewardedVideoAd.getAdForId(id: id), rewardedVideoAd.status == AdStatus.LOADED else { + result(FlutterError(code: "ad_not_loaded", message: "show failed for rewardedVideo ad, no ad was loaded", details: nil)) + return + } + + let arguments = call.arguments as? [String: Any] ?? [String: Any]() + let placement = arguments["placement"] as? String ?? "Unknown" + result(rewardedVideoAd.show(placement: placement)) + } + + private func callLoadInterstitialAd(id: Int?, _ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let id = id else { + result(FlutterError(code: "no_hash_id", message: "load interstitialAd lose hash id", details: nil)) + return + } + + guard let channel = channel else { + result(FlutterError(code: "no_channel", message: "load interstitialAd lose channel", details: nil)) + return + } + + let interstitialAd = InterstitialAd.createInterstitialAd(id: id, channel: channel) + if (interstitialAd.status == AdStatus.LOADED) { + result(true) + return + } + + let arguments = call.arguments as? [String: Any] ?? [String: Any]() + guard let adUnitId = arguments["adUnitId"] as? String, adUnitId.isEmpty == false else { + result(FlutterError(code: "no_adunit_id", message: "a null or empty adUnitId was provided for interstitialAd id=\(interstitialAd.id)", details: nil)) + return + } + + let adAmazonSlotId = arguments["adAmazonSlotId"] as? String ?? "" + + interstitialAd.load(adUnitId: adUnitId, adAmazonSlotId: adAmazonSlotId) + result(true) + } + + private func callShowInterstitialAd(id: Int?, _ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let id = id else { + result(FlutterError(code: "no_hash_id", message: "show interstitialAd lose hash id", details: nil)) + return + } + + guard let interstitialAd = InterstitialAd.getAdForId(id: id), interstitialAd.status == AdStatus.LOADED else { + result(FlutterError(code: "ad_not_loaded", message: "show failed for interstitial ad, no ad was loaded", details: nil)) + return + } + + let arguments = call.arguments as? [String: Any] ?? [String: Any]() + let placement = arguments["placement"] as? String ?? "Unknown" + result(interstitialAd.show(placement: placement)) + } + + private func callInterstitialAdLoaded(id: Int?, result: @escaping FlutterResult) { + guard let id = id else { + result(FlutterError(code: "no_hash_id", message: "is interstitialAd loaded lose hash id", details: nil)) + return + } + + guard let interstitialAd = InterstitialAd.getAdForId(id: id) else { + result(FlutterError(code: "no_ad_for_id", message: "isAdLoaded failed, no add exists for interstitialAd", details: nil)) + return + } + + if (interstitialAd.status == AdStatus.LOADED) { + result(true) + } else { + result(false) + } + } + + private func callLoadBannerAd(id: Int?, _ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let id = id else { + result(FlutterError(code: "no_hash_id", message: "load bannerAd lose hash id", details: nil)) + return + } + + guard let channel = channel else { + result(FlutterError(code: "no_channel", message: "load bannerAd lose channel", details: nil)) + return + } + + let bannerAd = BannerAd.createBannerAd(id: id, channel: channel) + + let arguments = call.arguments as? [String: Any] ?? [String: Any]() + guard let adUnitId = arguments["adUnitId"] as? String, adUnitId.isEmpty == false else { + result(FlutterError(code: "no_adunit_id", message: "a null or empty adUnitId was provided for bannerAd id=\(bannerAd.id)", details: nil)) + return + } + + let adAmazonSlotId = arguments["adAmazonSlotId"] as? String ?? "" + + let anchorOffset = arguments["anchorOffset"] as? String + if (anchorOffset != nil) { + bannerAd.anchorOffset = (anchorOffset! as NSString).doubleValue + } + let horizontalCenterOffset = arguments["horizontalCenterOffset"] as? String + if (horizontalCenterOffset != nil) { + bannerAd.horizontalCenterOffset = (horizontalCenterOffset! as NSString).doubleValue + } + let anchorType = arguments["anchorType"] as? Int + if (anchorType != nil) { + bannerAd.anchorType = anchorType ?? 0 + } + + let placement = arguments["placement"] as? String ?? "Unknown" + + bannerAd.load(adUnitId: adUnitId, adAmazonSlotId: adAmazonSlotId, placement: placement) + result(true) + } + + private func callShowBannerAd(id: Int?, _ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let id = id else { + result(FlutterError(code: "no_hash_id", message: "show bannerAd lose hash id", details: nil)) + return + } + + guard let bannerAd = BannerAd.getAdForId(id: id), bannerAd.status == AdStatus.LOADED else { + result(FlutterError(code: "ad_not_loaded", message: "show failed for banner ad, no ad was loaded", details: nil)) + return + } + + let arguments = call.arguments as? [String: Any] ?? [String: Any]() + let anchorOffset = arguments["anchorOffset"] as? String + if (anchorOffset != nil) { + bannerAd.anchorOffset = (anchorOffset! as NSString).doubleValue + } + let horizontalCenterOffset = arguments["horizontalCenterOffset"] as? String + if (horizontalCenterOffset != nil) { + bannerAd.horizontalCenterOffset = (horizontalCenterOffset! as NSString).doubleValue + } + let anchorType = arguments["anchorType"] as? Int + if (anchorType != nil) { + bannerAd.anchorType = anchorType ?? 0 + } + + bannerAd.show() + result(true) + } + + private func callHideBannerAd(id: Int?, result: @escaping FlutterResult) { + guard let id = id else { + result(FlutterError(code: "no_hash_id", message: "hide bannerAd lose hash id", details: nil)) + return + } + + guard let bannerAd = BannerAd.getAdForId(id: id) else { + result(FlutterError(code: "ad_is_null", message: "bannerAd is null", details: nil)) + return + } + + bannerAd.hide() + result(true) + } + + private func callDisposeBannerAd(id: Int?, result: @escaping FlutterResult) { + guard let id = id else { + result(FlutterError(code: "no_hash_id", message: "dispose bannerAd lose hash id", details: nil)) + return + } + + guard let bannerAd = BannerAd.getAdForId(id: id) else { + result(FlutterError(code: "ad_not_initialized", message: "bannerAd is null", details: nil)) + return + } + + bannerAd.dispose() + result(true) + } + + private func callDisposeInterstitialAd(id: Int?, result: @escaping FlutterResult) { + guard let id = id else { + result(FlutterError(code: "no_hash_id", message: "dispose interstitialAd lose hash id", details: nil)) + return + } + + guard let interstitialAd = InterstitialAd.getAdForId(id: id) else { + result(FlutterError(code: "ad_not_initialized", message: "interstitialAd is null", details: nil)) + return + } + + interstitialAd.dispose() + result(true) + } + + private func callDisposeRewardedAd(id: Int?, result: @escaping FlutterResult) { + guard let id = id else { + result(FlutterError(code: "no_hash_id", message: "dispose rewardedVideoAd lose hash id", details: nil)) + return + } + + guard let rewardedAd = RewardedVideoAd.getAdForId(id: id) else { + result(FlutterError(code: "ad_not_initialized", message: "rewardedVideoAd is null", details: nil)) + return + } + + rewardedAd.dispose() + result(true) + } + + private func callAfterAcceptPrivacy(arguments: [String: Any], result: @escaping FlutterResult) { + // let consentResult = arguments["consentResult"] as? Bool + // if (consentResult != nil) { + // DTBAds.sharedInstance().setConsentStatus(consentResult == true ? DTBConsentStatus.EXPLICIT_YES : DTBConsentStatus.EXPLICIT_NO) + // } else { + // DTBAds.sharedInstance().setConsentStatus(DTBConsentStatus.UNKNOWN) + // } + ALPrivacySettings.setHasUserConsent(true) + result(true) + } + + private func callCheckConsentDialogStatus(result: @escaping FlutterResult) { + guard let configuration = ALSdk.shared()?.configuration else { + result(FlutterError(code: "not_found_config", message: "check consent dialog error! not found configuration", details: nil)) + return + } + switch (configuration.consentDialogState) { + case .applies: + result(ConsentResult.ShouldShow) + break + case .doesNotApply: + result(ConsentResult.GdprNotApplies) + break + default: + result(ConsentResult.Unknown) + break + } + } + + private func callGetInterstitialAdsState(id: Int?, result: @escaping FlutterResult) { + guard let id = id else { + result(FlutterError(code: "no_hash_id", message: "get interstitialAd state loss hash id", details: nil)) + return + } + + guard let interstitialAd = InterstitialAd.getAdForId(id: id) else { + result(FlutterError(code: "ad_not_initialized", message: "interstitialAd is null", details: nil)) + return + } + result(interstitialAd.getState()) + } + + private func callGetRewardedAdsState(id: Int?, result: @escaping FlutterResult) { + guard let id = id else { + result(FlutterError(code: "no_hash_id", message: "get rewardedAd state loss hash id", details: nil)) + return + } + + guard let rewardedAd = RewardedVideoAd.getAdForId(id: id) else { + result(FlutterError(code: "ad_not_initialized", message: "rewardedAd is null", details: nil)) + return + } + result(rewardedAd.getState()) + } + + private func callHasUserConsent(result: @escaping FlutterResult) { + result(ALPrivacySettings.hasUserConsent()) + } + + private func callIsTablet(result: @escaping FlutterResult) { + let isTablet = (UIDevice.current.userInterfaceIdiom == .pad) + result(isTablet) + } + + private func callSetKeywords(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let keywords = call.arguments as? [String] ?? [String]() + ALSdk.shared()?.targetingData.keywords = keywords + result(true) + } + + private func callClearTargetingData(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + ALSdk.shared()?.targetingData.clearAll() + result(true) + } + + private func callGetBannerAdSize(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let height = (UIDevice.current.userInterfaceIdiom == .pad) ? 90.0 : 50.0 + let width = UIScreen.main.bounds.width + result(["width": width, "height": height] as [String : Any]) + } + +} diff --git a/guru_app/plugins/guru_applovin_flutter/ios/guru_applovin_flutter.podspec b/guru_app/plugins/guru_applovin_flutter/ios/guru_applovin_flutter.podspec new file mode 100644 index 0000000..3353cb0 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/ios/guru_applovin_flutter.podspec @@ -0,0 +1,49 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint guru_applovin_flutter.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'guru_applovin_flutter' + s.version = '2.3.0' + s.summary = 'A new flutter plugin project.' + s.description = <<-DESC +A new flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.dependency 'AppLovinSDK', '11.11.2' + s.dependency 'AppLovinMediationFacebookAdapter', '6.12.0.3' + s.dependency 'AppLovinMediationFyberAdapter', '8.2.4.0' + s.dependency 'AppLovinMediationGoogleAdapter', '10.9.0.0' + s.dependency 'AppLovinMediationGoogleAdManagerAdapter', '10.9.0.0' + s.dependency 'AppLovinMediationIronSourceAdapter', '7.4.0.0.1' + s.dependency 'AppLovinMediationSmaatoAdapter', '22.3.0.0' + s.dependency 'AppLovinMediationUnityAdsAdapter', '4.8.0.1' + s.dependency 'AppLovinMediationByteDanceAdapter', '5.4.0.8.0' + s.dependency 'AppLovinMediationChartboostAdapter', '9.4.0.0' + s.dependency 'AppLovinMediationInMobiAdapter', '10.1.4.2' + s.dependency 'AppLovinMediationVerveAdapter', '2.19.0.0' + s.dependency 'AppLovinMediationVungleAdapter', '7.0.1.0' + s.dependency 'AppLovinMediationMintegralAdapter', '7.4.2.0.0' + s.dependency 'AmazonPublisherServicesSDK', '4.7.5' + s.dependency 'AppLovinMediationAmazonAdMarketplaceAdapter', '4.7.5.0' + s.dependency 'AppLovinMediationBidMachineAdapter', '2.3.0.2.0' + s.dependency 'AppLovinMediationMobileFuseAdapter', '1.5.2.0' + s.dependency 'MolocoCustomAdapterAppLovin', '1.3.0.0' + s.dependency 'AppLovinMediationOguryPresageAdapter', '4.2.2.0' + s.dependency 'OpenWrapSDK', '3.1.0' + s.dependency 'AppLovinPubMaticAdapter', '1.1.0' + s.dependency 'GuruConsent', '1.4.1' + + s.platform = :ios, '11.0' + + s.ios.deployment_target = '11.0' + s.static_framework = true + # Flutter.framework does not contain a i386 slice. + # s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + # s.swift_version = '5.0' +end diff --git a/guru_app/plugins/guru_applovin_flutter/lib/ad_impression.dart b/guru_app/plugins/guru_applovin_flutter/lib/ad_impression.dart new file mode 100644 index 0000000..585f84b --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/lib/ad_impression.dart @@ -0,0 +1,17 @@ +enum AdImpressionEvent { + onAdImpression, +} + +typedef void AdImpressionCallback(AdImpressionEvent event, Map arguments); + +class AdImpressionListener { + static const Map methodToImpressionAdEvent = { + 'onAdImpression': AdImpressionEvent.onAdImpression, + }; + + static late AdImpressionCallback callback; + + static void addCallback(AdImpressionCallback callback) { + AdImpressionListener.callback = callback; + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/lib/banner_ad.dart b/guru_app/plugins/guru_applovin_flutter/lib/banner_ad.dart new file mode 100644 index 0000000..06bd219 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/lib/banner_ad.dart @@ -0,0 +1,92 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:guru_utils/collection/collectionutils.dart'; +import 'guru_applovin_flutter.dart'; + +enum AnchorType { bottom, top } + +enum BannerAdEvent { + onAdLoaded, + onAdLoadFailed, + onAdHidden, + onAdDisplayFailed, + onAdDisplayed, + onAdClicked, + onAdExpanded, + onAdCollapsed, +} + +typedef void BannerAdListener(BannerAdEvent event, {Map arguments}); + +class BannerAd { + static const Map methodToBannerAdEvent = { + 'onBannerAdLoaded': BannerAdEvent.onAdLoaded, + 'onBannerAdLoadFailed': BannerAdEvent.onAdLoadFailed, + 'onBannerAdDisplayFailed': BannerAdEvent.onAdDisplayFailed, + 'onBannerAdDisplayed': BannerAdEvent.onAdDisplayed, + 'onBannerAdClicked': BannerAdEvent.onAdClicked, + 'onBannerAdHidden': BannerAdEvent.onAdHidden, + 'onBannerAdExpanded': BannerAdEvent.onAdExpanded, + 'onBannerAdCollapsed': BannerAdEvent.onAdCollapsed, + }; + + static final Map allBannerAds = {}; + + BannerAd({ + required this.adUnitId, + this.adAmazonSlotId, + this.listener, + }) { + assert(adUnitId != null && adUnitId.isNotEmpty); + allBannerAds[id] = this; + } + +// static final String testAdUnitId = Platform.isAndroid +// ? 'ca-app-pub-3940256099942544/5224354917' +// : 'ca-app-pub-3940256099942544/1712485313'; + + final String adUnitId; + final String? adAmazonSlotId; + + int get id => hashCode; + + BannerAdListener? listener; + + Future load( + {double anchorOffset = 0.0, double horizontalCenterOffset = 0.0, AnchorType anchorType = AnchorType + .bottom, String? placement}) { + return invokeBooleanMethod("loadBannerAd", MapUtils.filterOutNulls({ + 'id': id, + 'adUnitId': adUnitId, + 'adAmazonSlotId': adAmazonSlotId, + 'anchorOffset': anchorOffset.toString(), + 'horizontalCenterOffset': horizontalCenterOffset.toString(), + 'anchorType': describeEnum(anchorType), + 'placement': placement + })); + } + + Future show( + {double anchorOffset = 0.0, double horizontalCenterOffset = 0.0, AnchorType anchorType = AnchorType + .bottom}) { + return invokeBooleanMethod("showBannerAd", { + 'id': id, + 'anchorOffset': anchorOffset.toString(), + 'horizontalCenterOffset': horizontalCenterOffset.toString(), + 'anchorType': describeEnum(anchorType) + }); + } + + Future hide() { + return invokeBooleanMethod("hideBannerAd", { + 'id': id, + }); + } + + Future dispose() { + assert(allBannerAds[id] != null); + allBannerAds.remove(id); + return invokeBooleanMethod("disposeBannerAd", {'id': id}); + } + +} diff --git a/guru_app/plugins/guru_applovin_flutter/lib/gdpr/gdpr_models.dart b/guru_app/plugins/guru_applovin_flutter/lib/gdpr/gdpr_models.dart new file mode 100644 index 0000000..5f51db3 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/lib/gdpr/gdpr_models.dart @@ -0,0 +1,19 @@ +/// Created by Haoyi on 2022/9/15 + +class ConsentStatus { + static const notAvailable = -100; + static const unknown = 0; + static const notRequired = 1; + static const required = 2; + static const obtained = 3; +} + +class ConsentDebugGeography { + static const DISABLED = 0; + static const EEA = 1; + static const NOT_EEA = 2; + + static final Set geographies = {DISABLED, EEA, NOT_EEA}; + + static final List names = ["DISABLED", "EEA", "NOT_EEA", "DEFAULT"]; +} \ No newline at end of file diff --git a/guru_app/plugins/guru_applovin_flutter/lib/guru_applovin_flutter.dart b/guru_app/plugins/guru_applovin_flutter/lib/guru_applovin_flutter.dart new file mode 100644 index 0000000..d0c39da --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/lib/guru_applovin_flutter.dart @@ -0,0 +1,219 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:guru_applovin_flutter/ad_impression.dart'; +import 'package:guru_applovin_flutter/banner_ad.dart'; +import 'package:guru_applovin_flutter/interstitial_ad.dart'; +import 'package:guru_applovin_flutter/rewarded_video_ad.dart'; +import 'package:guru_applovin_flutter/gdpr/gdpr_models.dart'; + +export 'gdpr/gdpr_models.dart'; + +class ConsentResult { + static const int ShouldShow = 0; + static const int NotShow = -1; + static const int GdprNotApplies = -2; + static const int Unknown = -3; +} + +class AdStatus { + static const int CREATED = 0; + static const int LOADING = 1; + static const int FAILED = 2; + static const int LOADED = 3; + + static String toAdStatusName(int status) { + switch (status) { + case CREATED: + return "CREATED"; + case LOADING: + return "LOADING"; + case LOADED: + return "LOADED"; + default: + return "FAILED"; + } + } +} + +class GuruApplovinFlutter { + @visibleForTesting + GuruApplovinFlutter.private(MethodChannel channel) : _channel = channel { + _channel.setMethodCallHandler(_handleMethod); + } + + static final GuruApplovinFlutter _instance = GuruApplovinFlutter.private( + const MethodChannel('guru_applovin_flutter'), + ); + + /// The single shared instance of this plugin. + + static GuruApplovinFlutter get instance => _instance; + final MethodChannel _channel; + + /// Initialize this plugin for the AdMob app specified by `appId`. + Future initialize( + {bool debugMode = false, String? userId, String? amazonAppId, String? pubmaticStoreUrl}) { + return invokeBooleanMethod("initialize", { + "debug_mode": debugMode, + "user_id": userId, + "amazon_appId": amazonAppId, + "pubmatic_store_url": pubmaticStoreUrl + }); + } + + Future gatherConsentAndInitialize( + {bool debugMode = false, + String? userId, + String? amazonAppId, + String? pubmaticStoreUrl, + int? debugGeography, + String? testDeviceId}) { + final params = { + "debug_mode": debugMode, + "user_id": userId, + "amazon_appId": amazonAppId, + "pubmatic_store_url": pubmaticStoreUrl + }; + if (debugGeography != null) { + params["debug_geography"] = debugGeography; + } + if (testDeviceId != null && testDeviceId != '') { + params["test_device_id"] = testDeviceId; + } + return invokeBooleanMethod("gatherConsentAndInitialize", params); + } + + Future requestGdpr({int? debugGeography, String? testDeviceId}) async { + final params = {}; + if (debugGeography != null) { + params["debug_geography"] = debugGeography; + } + if (testDeviceId != null && testDeviceId != '') { + params["test_device_id"] = testDeviceId; + } + return await invokeIntMethod("requestGdpr", params) ?? ConsentStatus.notAvailable; + } + + Future resetGdpr() async { + return await invokeBooleanMethod("resetGdpr", {}) ?? false; + } + + Future openDebugger() { + return invokeBooleanMethod("openDebugger", {}); + } + + Future afterAcceptPrivacy(bool? consentResult) async { + return await invokeBooleanMethod( + "afterAcceptPrivacy", {"consentResult": consentResult}) ?? + false; + } + + Future hasUserConsent() async { + return await invokeBooleanMethod("hasUserConsent", {}) ?? false; + } + + Future isTablet() { + return invokeBooleanMethod("isTablet", {}); + } + + Future setAdsDebugMode(bool debugMode) { + return invokeBooleanMethod("setDebugMode", {"debug_mode": debugMode}); + } + + Future checkConsentDialogStatus() async { + return await invokeIntMethod("checkConsentDialogStatus", {}) ?? + ConsentResult.Unknown; + } + + Future getRewardedAdsState() async { + return await invokeIntMethod("getRewardedAdState", {}) ?? AdStatus.FAILED; + } + + Future setKeywords(Map keywords) async { + final list = keywords.entries.map((e) => "${e.key}:${e.value}").toList(); + return await invokeBooleanMethod("setKeywords", list); + } + + Future getBannerAdSize() async { + final Map result = + await invokeMapMethod("getBannerAdSize", {}) ?? {}; + return Size(result["width"] ?? -1, result["height"] ?? -1); + } + + Future _handleMethod(MethodCall call) { + assert(call.arguments is Map); + final Map argumentsMap = call.arguments; + final RewardedVideoAdEvent? rewardedEvent = + RewardedVideoAd.methodToRewardedVideoAdEvent[call.method]; + final int? id = argumentsMap['id']; + + if (rewardedEvent != null) { + if (id != null && RewardedVideoAd.allRewardedVideoAds[id] != null) { + var ad = RewardedVideoAd.allRewardedVideoAds[id]; + final RewardedVideoAdEvent? adEvent = + RewardedVideoAd.methodToRewardedVideoAdEvent[call.method]; + if (adEvent != null && ad!.listener != null) { + ad.listener!(adEvent, arguments: argumentsMap); + } + } + } + + final InterstitialAdEvent? interstitialAdEvent = + InterstitialAd.methodToInterstitialAdEvent[call.method]; + if (interstitialAdEvent != null) { + if (id != null && InterstitialAd.allInterstitialAds[id] != null) { + var ad = InterstitialAd.allInterstitialAds[id]; + final InterstitialAdEvent? adEvent = + InterstitialAd.methodToInterstitialAdEvent[call.method]; + if (adEvent != null && ad!.listener != null) { + ad.listener!(adEvent, arguments: argumentsMap); + } + } + } + + final BannerAdEvent? bannerAdEvent = BannerAd.methodToBannerAdEvent[call.method]; + if (bannerAdEvent != null) { + if (id != null && BannerAd.allBannerAds[id] != null) { + var ad = BannerAd.allBannerAds[id]; + final BannerAdEvent? adEvent = BannerAd.methodToBannerAdEvent[call.method]; + if (adEvent != null && ad!.listener != null) { + ad.listener!(adEvent, arguments: argumentsMap); + } + } + } + final AdImpressionEvent? impressionAdEvent = + AdImpressionListener.methodToImpressionAdEvent[call.method]; + if (impressionAdEvent != null) { + AdImpressionListener.callback(impressionAdEvent, argumentsMap); + } + + return Future.value(null); + } +} + +Future invokeBooleanMethod(String method, [dynamic arguments]) async { + final bool? result = await GuruApplovinFlutter.instance._channel.invokeMethod( + method, + arguments, + ); + return result; +} + +Future invokeIntMethod(String method, [dynamic arguments]) async { + final int? result = await GuruApplovinFlutter.instance._channel.invokeMethod( + method, + arguments, + ); + return result; +} + +Future?> invokeMapMethod(String method, [dynamic arguments]) async { + final Map? result = + await GuruApplovinFlutter.instance._channel.invokeMapMethod( + method, + arguments, + ); + return result; +} diff --git a/guru_app/plugins/guru_applovin_flutter/lib/interstitial_ad.dart b/guru_app/plugins/guru_applovin_flutter/lib/interstitial_ad.dart new file mode 100644 index 0000000..0832290 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/lib/interstitial_ad.dart @@ -0,0 +1,84 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:guru_applovin_flutter/guru_applovin_flutter.dart'; +import 'package:guru_utils/collection/collectionutils.dart'; + +enum InterstitialAdEvent { + onAdLoaded, + onAdLoadFailed, + onAdDisplayFailed, + onAdDisplayed, + onAdClicked, + onAdHidden, +} + +typedef void InterstitialAdListener(InterstitialAdEvent event, {Map arguments}); + +class InterstitialAd { + + static const Map methodToInterstitialAdEvent = < + String, + InterstitialAdEvent>{ + 'onInterstitialAdLoaded': InterstitialAdEvent.onAdLoaded, + 'onInterstitialAdLoadFailed': InterstitialAdEvent.onAdLoadFailed, + 'onInterstitialAdDisplayFailed': InterstitialAdEvent.onAdDisplayFailed, + 'onInterstitialAdDisplayed': InterstitialAdEvent.onAdDisplayed, + 'onInterstitialAdClicked': InterstitialAdEvent.onAdClicked, + 'onInterstitialAdHidden': InterstitialAdEvent.onAdHidden, + }; + + static final Map allInterstitialAds = {}; + + InterstitialAd({ + required this.adUnitId, + this.adAmazonSlotId, + this.listener, + }) { + assert(adUnitId != null && adUnitId.isNotEmpty); + allInterstitialAds[id] = this; + } + +// static final String testAdUnitId = Platform.isAndroid +// ? 'ca-app-pub-3940256099942544/5224354917' +// : 'ca-app-pub-3940256099942544/1712485313'; + + final String adUnitId; + final String? adAmazonSlotId; + + int get id => hashCode; + + InterstitialAdListener? listener; + + Future load() { + print("Ads applovin library InterstitialAd load"); + return invokeBooleanMethod("loadInterstitialAd", { + 'id': id, + 'adUnitId': adUnitId, + 'adAmazonSlotId': adAmazonSlotId, + }); + } + + Future show({String? placement}) { + return invokeBooleanMethod("showInterstitialAd", MapUtils.filterOutNulls({ + 'id': id, + "placement": placement + })); + } + + Future dispose() { + assert(allInterstitialAds[id] != null); + allInterstitialAds.remove(id); + return invokeBooleanMethod("disposeInterstitialAd", {'id': id}); + } + + Future isLoaded() { + return invokeBooleanMethod("isInterstitialAdLoaded", { + 'id': id, + }); + } + + Future getAdState() async { + return await invokeIntMethod("getInterstitialAdState", {'id': id}) ?? + AdStatus.FAILED; + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/lib/rewarded_video_ad.dart b/guru_app/plugins/guru_applovin_flutter/lib/rewarded_video_ad.dart new file mode 100644 index 0000000..f8a4f5a --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/lib/rewarded_video_ad.dart @@ -0,0 +1,85 @@ +import 'package:flutter/widgets.dart'; +import 'package:guru_applovin_flutter/guru_applovin_flutter.dart'; +import 'package:guru_utils/collection/collectionutils.dart'; + +enum RewardedVideoAdEvent { + onAdLoaded, + onAdLoadFailed, + onAdDisplayFailed, + onAdDisplayed, + onAdClicked, + onAdHidden, + onRewardedVideoStarted, + onRewardedVideoCompleted, + onUserRewarded, +} + +typedef void RewardedVideoAdListener(RewardedVideoAdEvent event, {Map arguments}); + +class RewardedVideoAd { + static const Map methodToRewardedVideoAdEvent = { + 'onRewardedVideoAdLoaded': RewardedVideoAdEvent.onAdLoaded, + 'onRewardedVideoAdLoadFailed': RewardedVideoAdEvent.onAdLoadFailed, + 'onRewardedVideoAdDisplayFailed': RewardedVideoAdEvent.onAdDisplayFailed, + 'onRewardedVideoAdDisplayed': RewardedVideoAdEvent.onAdDisplayed, + 'onRewardedVideoAdClicked': RewardedVideoAdEvent.onAdClicked, + 'onRewardedVideoAdHidden': RewardedVideoAdEvent.onAdHidden, + 'onRewardedVideoStarted': RewardedVideoAdEvent.onRewardedVideoStarted, + 'onRewardedVideoCompleted': RewardedVideoAdEvent.onRewardedVideoCompleted, + 'onRewardedVideoUserRewarded': RewardedVideoAdEvent.onUserRewarded, + }; + + static final Map allRewardedVideoAds = {}; + + RewardedVideoAd({ + required this.adUnitId, + this.adAmazonSlotId, + this.listener, + }) { + assert(adUnitId != null && adUnitId.isNotEmpty); + allRewardedVideoAds[id] = this; + } + +// static final String testAdUnitId = Platform.isAndroid +// ? 'ca-app-pub-3940256099942544/5224354917' +// : 'ca-app-pub-3940256099942544/1712485313'; + + final String adUnitId; + final String? adAmazonSlotId; + + int get id => hashCode; + + RewardedVideoAdListener? listener; + + Future load() { + print("Ads RewardedVideo applovin library RewardedVideo load"); + return invokeBooleanMethod("loadRewardedVideoAd", { + 'id': id, + 'adUnitId': adUnitId, + 'adAmazonSlotId': adAmazonSlotId + }); + } + + Future show({String? placement}) { + return invokeBooleanMethod("showRewardedVideoAd", MapUtils.filterOutNulls({ + 'id': id, + 'placement' : placement + })); + } + + Future dispose() { + assert(allRewardedVideoAds[id] != null); + allRewardedVideoAds.remove(id); + return invokeBooleanMethod("disposeRewardedAd", {'id': id}); + } + + Future isLoaded() { + return invokeBooleanMethod("isRewardedVideoLoaded", { + 'id': id, + }); + } + + Future getAdState() async { + return await invokeIntMethod("getRewardedAdState", {'id': id}) ?? AdStatus.FAILED; + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/.gitignore b/guru_app/plugins/guru_applovin_flutter/plugins/.gitignore new file mode 100644 index 0000000..02c4c38 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ +.idea/ \ No newline at end of file diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/.gitignore b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/.gitignore new file mode 100644 index 0000000..96486fd --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/CHANGELOG.md b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/LICENSE b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/README.md b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/README.md new file mode 100644 index 0000000..dedf68d --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/README.md @@ -0,0 +1,15 @@ +# max_yandex_adapter + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/developing-packages/), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter development, view the +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/analysis_options.yaml b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/android/.gitignore b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/android/.gitignore new file mode 100644 index 0000000..161bdcd --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/android/build.gradle b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/android/build.gradle new file mode 100644 index 0000000..c2f0838 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/android/build.gradle @@ -0,0 +1,50 @@ +group 'guru.core.ads.max.max_yandex_adapter' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.1.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + minSdkVersion 21 + } +} + +dependencies { + api 'com.applovin.mediation:yandex-adapter:6.1.0.0' +} \ No newline at end of file diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/android/settings.gradle b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/android/settings.gradle new file mode 100644 index 0000000..264c8b8 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'max_yandex_adapter' diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/android/src/main/AndroidManifest.xml b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..093696a --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/android/src/main/kotlin/guru/core/ads/max/max_yandex_adapter/MaxYandexAdapterPlugin.kt b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/android/src/main/kotlin/guru/core/ads/max/max_yandex_adapter/MaxYandexAdapterPlugin.kt new file mode 100644 index 0000000..75850e7 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/android/src/main/kotlin/guru/core/ads/max/max_yandex_adapter/MaxYandexAdapterPlugin.kt @@ -0,0 +1,35 @@ +package guru.core.ads.max.max_yandex_adapter + +import androidx.annotation.NonNull + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +/** MaxYandexAdapterPlugin */ +class MaxYandexAdapterPlugin: FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel : MethodChannel + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "max_yandex_adapter") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + if (call.method == "getPlatformVersion") { + result.success("Android ${android.os.Build.VERSION.RELEASE}") + } else { + result.notImplemented() + } + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/.gitignore b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/.gitignore new file mode 100644 index 0000000..24476c5 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/README.md b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/README.md new file mode 100644 index 0000000..0420d09 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/README.md @@ -0,0 +1,16 @@ +# max_yandex_adapter_example + +Demonstrates how to use the max_yandex_adapter plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/analysis_options.yaml b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/.gitignore b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/build.gradle b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/build.gradle new file mode 100644 index 0000000..748c14f --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/build.gradle @@ -0,0 +1,71 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "guru.core.ads.max.max_yandex_adapter_example" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/debug/AndroidManifest.xml b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..e4e9fc8 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/AndroidManifest.xml b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4d5ce12 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/kotlin/guru/core/ads/max/max_yandex_adapter_example/MainActivity.kt b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/kotlin/guru/core/ads/max/max_yandex_adapter_example/MainActivity.kt new file mode 100644 index 0000000..87bd67a --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/kotlin/guru/core/ads/max/max_yandex_adapter_example/MainActivity.kt @@ -0,0 +1,6 @@ +package guru.core.ads.max.max_yandex_adapter_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/drawable-v21/launch_background.xml b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/drawable/launch_background.xml b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/values-night/styles.xml b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/values/styles.xml b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/profile/AndroidManifest.xml b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..e4e9fc8 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/build.gradle b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/build.gradle new file mode 100644 index 0000000..83ae220 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.1.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/gradle.properties b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/gradle/wrapper/gradle-wrapper.properties b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..cb24abd --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/settings.gradle b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/.gitignore b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Flutter/AppFrameworkInfo.plist b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..9625e10 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Flutter/Debug.xcconfig b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Flutter/Release.xcconfig b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Podfile b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Podfile new file mode 100644 index 0000000..88359b2 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcodeproj/project.pbxproj b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..571586c --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,481 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = guru.core.ads.max.maxYandexAdapterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = guru.core.ads.max.maxYandexAdapterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = guru.core.ads.max.maxYandexAdapterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c87d15a --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/AppDelegate.swift b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..28c6bf0 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..f091b6b Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..d0ef06e Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..c8f9ed8 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..75b2d16 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c4df70d Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Base.lproj/Main.storyboard b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Info.plist b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Info.plist new file mode 100644 index 0000000..a50569a --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Max Yandex Adapter + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + max_yandex_adapter_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Runner-Bridging-Header.h b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/lib/main.dart b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/lib/main.dart new file mode 100644 index 0000000..588bf2d --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/lib/main.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:max_yandex_adapter/max_yandex_adapter.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + String _platformVersion = 'Unknown'; + final _maxYandexAdapterPlugin = MaxYandexAdapter(); + + @override + void initState() { + super.initState(); + initPlatformState(); + } + + // Platform messages are asynchronous, so we initialize in an async method. + Future initPlatformState() async { + String platformVersion; + // Platform messages may fail, so we use a try/catch PlatformException. + // We also handle the message potentially returning null. + try { + platformVersion = + await _maxYandexAdapterPlugin.getPlatformVersion() ?? 'Unknown platform version'; + } on PlatformException { + platformVersion = 'Failed to get platform version.'; + } + + // If the widget was removed from the tree while the asynchronous platform + // message was in flight, we want to discard the reply rather than calling + // setState to update our non-existent appearance. + if (!mounted) return; + + setState(() { + _platformVersion = platformVersion; + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: Center( + child: Text('Running on: $_platformVersion\n'), + ), + ), + ); + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/pubspec.lock b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/pubspec.lock new file mode 100644 index 0000000..82b2e99 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/pubspec.lock @@ -0,0 +1,175 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.9.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.16.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.5" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.2" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.12" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.5" + max_yandex_adapter: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.2" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.5" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.12" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" +sdks: + dart: ">=2.18.6 <3.0.0" + flutter: ">=2.5.0" diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/pubspec.yaml b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/pubspec.yaml new file mode 100644 index 0000000..2db1bca --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/pubspec.yaml @@ -0,0 +1,84 @@ +name: max_yandex_adapter_example +description: Demonstrates how to use the max_yandex_adapter plugin. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: '>=2.18.6 <3.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + max_yandex_adapter: + # When depending on this package from a real application you should use: + # max_yandex_adapter: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/test/widget_test.dart b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/test/widget_test.dart new file mode 100644 index 0000000..450b565 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/example/test/widget_test.dart @@ -0,0 +1,27 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:max_yandex_adapter_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && + widget.data!.startsWith('Running on:'), + ), + findsOneWidget, + ); + }); +} diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/ios/.gitignore b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/ios/.gitignore new file mode 100644 index 0000000..0c88507 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/ios/Assets/.gitkeep b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/ios/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/ios/Classes/MaxYandexAdapterPlugin.h b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/ios/Classes/MaxYandexAdapterPlugin.h new file mode 100644 index 0000000..5d053a9 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/ios/Classes/MaxYandexAdapterPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface MaxYandexAdapterPlugin : NSObject +@end diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/ios/Classes/MaxYandexAdapterPlugin.m b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/ios/Classes/MaxYandexAdapterPlugin.m new file mode 100644 index 0000000..6702f8a --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/ios/Classes/MaxYandexAdapterPlugin.m @@ -0,0 +1,15 @@ +#import "MaxYandexAdapterPlugin.h" +#if __has_include() +#import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "max_yandex_adapter-Swift.h" +#endif + +@implementation MaxYandexAdapterPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftMaxYandexAdapterPlugin registerWithRegistrar:registrar]; +} +@end diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/ios/Classes/SwiftMaxYandexAdapterPlugin.swift b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/ios/Classes/SwiftMaxYandexAdapterPlugin.swift new file mode 100644 index 0000000..939529e --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/ios/Classes/SwiftMaxYandexAdapterPlugin.swift @@ -0,0 +1,14 @@ +import Flutter +import UIKit + +public class SwiftMaxYandexAdapterPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "max_yandex_adapter", binaryMessenger: registrar.messenger()) + let instance = SwiftMaxYandexAdapterPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + result("iOS " + UIDevice.current.systemVersion) + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/ios/max_yandex_adapter.podspec b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/ios/max_yandex_adapter.podspec new file mode 100644 index 0000000..7343f0a --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/ios/max_yandex_adapter.podspec @@ -0,0 +1,25 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint max_yandex_adapter.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'max_yandex_adapter' + s.version = '0.0.1' + s.summary = 'A new Flutter project.' + s.description = <<-DESC +A new Flutter project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.dependency 'AppLovinMediationYandexAdapter', '6.1.0.3' + s.platform = :ios, '13.0' + s.ios.deployment_target = '13.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' +end diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/lib/max_yandex_adapter.dart b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/lib/max_yandex_adapter.dart new file mode 100644 index 0000000..3fdadca --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/lib/max_yandex_adapter.dart @@ -0,0 +1,8 @@ + +import 'max_yandex_adapter_platform_interface.dart'; + +class MaxYandexAdapter { + Future getPlatformVersion() { + return MaxYandexAdapterPlatform.instance.getPlatformVersion(); + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/lib/max_yandex_adapter_method_channel.dart b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/lib/max_yandex_adapter_method_channel.dart new file mode 100644 index 0000000..15bf265 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/lib/max_yandex_adapter_method_channel.dart @@ -0,0 +1,17 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'max_yandex_adapter_platform_interface.dart'; + +/// An implementation of [MaxYandexAdapterPlatform] that uses method channels. +class MethodChannelMaxYandexAdapter extends MaxYandexAdapterPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('max_yandex_adapter'); + + @override + Future getPlatformVersion() async { + final version = await methodChannel.invokeMethod('getPlatformVersion'); + return version; + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/lib/max_yandex_adapter_platform_interface.dart b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/lib/max_yandex_adapter_platform_interface.dart new file mode 100644 index 0000000..cb207f0 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/lib/max_yandex_adapter_platform_interface.dart @@ -0,0 +1,29 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'max_yandex_adapter_method_channel.dart'; + +abstract class MaxYandexAdapterPlatform extends PlatformInterface { + /// Constructs a MaxYandexAdapterPlatform. + MaxYandexAdapterPlatform() : super(token: _token); + + static final Object _token = Object(); + + static MaxYandexAdapterPlatform _instance = MethodChannelMaxYandexAdapter(); + + /// The default instance of [MaxYandexAdapterPlatform] to use. + /// + /// Defaults to [MethodChannelMaxYandexAdapter]. + static MaxYandexAdapterPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [MaxYandexAdapterPlatform] when + /// they register themselves. + static set instance(MaxYandexAdapterPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future getPlatformVersion() { + throw UnimplementedError('platformVersion() has not been implemented.'); + } +} diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/pubspec.yaml b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/pubspec.yaml new file mode 100644 index 0000000..4a4ea74 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/pubspec.yaml @@ -0,0 +1,72 @@ +name: max_yandex_adapter +description: A new Flutter project. +version: 0.0.1 +homepage: + +environment: + sdk: '>=2.18.6 <3.0.0' + flutter: ">=2.5.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: guru.core.ads.max.max_yandex_adapter + pluginClass: MaxYandexAdapterPlugin + ios: + pluginClass: MaxYandexAdapterPlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/test/max_yandex_adapter_method_channel_test.dart b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/test/max_yandex_adapter_method_channel_test.dart new file mode 100644 index 0000000..b2e851e --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/test/max_yandex_adapter_method_channel_test.dart @@ -0,0 +1,24 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:max_yandex_adapter/max_yandex_adapter_method_channel.dart'; + +void main() { + MethodChannelMaxYandexAdapter platform = MethodChannelMaxYandexAdapter(); + const MethodChannel channel = MethodChannel('max_yandex_adapter'); + + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return '42'; + }); + }); + + tearDown(() { + channel.setMockMethodCallHandler(null); + }); + + test('getPlatformVersion', () async { + expect(await platform.getPlatformVersion(), '42'); + }); +} diff --git a/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/test/max_yandex_adapter_test.dart b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/test/max_yandex_adapter_test.dart new file mode 100644 index 0000000..b76b9d3 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/plugins/max_yandex_adapter/test/max_yandex_adapter_test.dart @@ -0,0 +1,29 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:max_yandex_adapter/max_yandex_adapter.dart'; +import 'package:max_yandex_adapter/max_yandex_adapter_platform_interface.dart'; +import 'package:max_yandex_adapter/max_yandex_adapter_method_channel.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class MockMaxYandexAdapterPlatform + with MockPlatformInterfaceMixin + implements MaxYandexAdapterPlatform { + + @override + Future getPlatformVersion() => Future.value('42'); +} + +void main() { + final MaxYandexAdapterPlatform initialPlatform = MaxYandexAdapterPlatform.instance; + + test('$MethodChannelMaxYandexAdapter is the default instance', () { + expect(initialPlatform, isInstanceOf()); + }); + + test('getPlatformVersion', () async { + MaxYandexAdapter maxYandexAdapterPlugin = MaxYandexAdapter(); + MockMaxYandexAdapterPlatform fakePlatform = MockMaxYandexAdapterPlatform(); + MaxYandexAdapterPlatform.instance = fakePlatform; + + expect(await maxYandexAdapterPlugin.getPlatformVersion(), '42'); + }); +} diff --git a/guru_app/plugins/guru_applovin_flutter/pubspec.lock b/guru_app/plugins/guru_applovin_flutter/pubspec.lock new file mode 100644 index 0000000..78e069f --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/pubspec.lock @@ -0,0 +1,139 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.9.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.16.0" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.12" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.12" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" +sdks: + dart: ">=2.17.0-0 <3.0.0" diff --git a/guru_app/plugins/guru_applovin_flutter/pubspec.yaml b/guru_app/plugins/guru_applovin_flutter/pubspec.yaml new file mode 100644 index 0000000..fe72303 --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/pubspec.yaml @@ -0,0 +1,65 @@ +name: guru_applovin_flutter +description: A new flutter plugin project. +version: 2.3.0 +homepage: + +environment: + sdk: ">=2.12.0 <3.0.0" +# flutter: ">=2.5.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter +# flutter_lints: ^1.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' and Android 'package' identifiers should not ordinarily + # be modified. They are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: flutter.guru.guru_applovin_flutter + pluginClass: GuruApplovinFlutterPlugin + ios: + pluginClass: GuruApplovinFlutterPlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/guru_app/plugins/guru_applovin_flutter/test/guru_applovin_flutter_test.dart b/guru_app/plugins/guru_applovin_flutter/test/guru_applovin_flutter_test.dart new file mode 100644 index 0000000..8b90e7a --- /dev/null +++ b/guru_app/plugins/guru_applovin_flutter/test/guru_applovin_flutter_test.dart @@ -0,0 +1,23 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:guru_applovin_flutter/guru_applovin_flutter.dart'; + +void main() { + const MethodChannel channel = MethodChannel('guru_applovin_flutter'); + + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return '42'; + }); + }); + + tearDown(() { + channel.setMockMethodCallHandler(null); + }); + + test('getPlatformVersion', () async { + // expect(await GuruApplovinFlutter.platformVersion, '42'); + }); +} diff --git a/guru_app/plugins/guru_navigator/.gitignore b/guru_app/plugins/guru_navigator/.gitignore new file mode 100644 index 0000000..2ee8465 --- /dev/null +++ b/guru_app/plugins/guru_navigator/.gitignore @@ -0,0 +1,120 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +.vscode +.gradle +.idea +/local.properties +.DS_Store +/build +.metadata +ios/Flutter/flutter_export_environment.sh + +**/.settings +**/.project +**/.classpath + + +# build id +build_id.properties + +# IntelliJ related + +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +/build/ +flutter_* +.flutter-plugins-dependencies + +# goCli related +go-cli/.packages +go-cli/pubspec.lock + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/gradlew +**/android/gradlew.bat +**/android/fastlane/report.xml +**/android/fastlane/README.md + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* +**/ios/build/* +**/ios/fastlane/README.md +**/ios/fastlane/report.xml + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages + +# Crashlytics +debugSymbols/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# i18n +**/android/res/values/strings_en.arb +lib/generated/i18n.dart + +# output +output/ios/adhok/* +output/ios/appstore/* +ios/fastlane/report.xml +android/fastlane/report.xml + +**/android/fastlane/metadata/android/**/images/** +**/ios/fastlane/screenshots/** \ No newline at end of file diff --git a/guru_app/plugins/guru_navigator/CHANGELOG.md b/guru_app/plugins/guru_navigator/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/guru_app/plugins/guru_navigator/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/guru_app/plugins/guru_navigator/LICENSE b/guru_app/plugins/guru_navigator/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/guru_app/plugins/guru_navigator/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/guru_app/plugins/guru_navigator/README.md b/guru_app/plugins/guru_navigator/README.md new file mode 100644 index 0000000..4d13b29 --- /dev/null +++ b/guru_app/plugins/guru_navigator/README.md @@ -0,0 +1,15 @@ +# guru_navigator + +A new Flutter plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/developing-packages/), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter development, view the +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/guru_app/plugins/guru_navigator/analysis_options.yaml b/guru_app/plugins/guru_navigator/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/guru_app/plugins/guru_navigator/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/guru_app/plugins/guru_navigator/android/.gitignore b/guru_app/plugins/guru_navigator/android/.gitignore new file mode 100644 index 0000000..161bdcd --- /dev/null +++ b/guru_app/plugins/guru_navigator/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/guru_app/plugins/guru_navigator/android/build.gradle b/guru_app/plugins/guru_navigator/android/build.gradle new file mode 100644 index 0000000..239b78b --- /dev/null +++ b/guru_app/plugins/guru_navigator/android/build.gradle @@ -0,0 +1,46 @@ +group 'app.guru.guru_navigator' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.1.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 31 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + minSdkVersion 16 + } +} diff --git a/guru_app/plugins/guru_navigator/android/settings.gradle b/guru_app/plugins/guru_navigator/android/settings.gradle new file mode 100644 index 0000000..5960514 --- /dev/null +++ b/guru_app/plugins/guru_navigator/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'guru_navigator' diff --git a/guru_app/plugins/guru_navigator/android/src/main/AndroidManifest.xml b/guru_app/plugins/guru_navigator/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..02158c2 --- /dev/null +++ b/guru_app/plugins/guru_navigator/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/guru_app/plugins/guru_navigator/android/src/main/kotlin/app/guru/guru_navigator/GuruNavigatorPlugin.kt b/guru_app/plugins/guru_navigator/android/src/main/kotlin/app/guru/guru_navigator/GuruNavigatorPlugin.kt new file mode 100644 index 0000000..9c950a9 --- /dev/null +++ b/guru_app/plugins/guru_navigator/android/src/main/kotlin/app/guru/guru_navigator/GuruNavigatorPlugin.kt @@ -0,0 +1,81 @@ +package app.guru.guru_navigator + +import android.content.Intent +import android.os.Handler +import android.os.Looper +import android.util.Log +import androidx.annotation.NonNull +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result +import io.flutter.plugin.common.PluginRegistry.NewIntentListener + +/** GuruNavigatorPlugin */ +class GuruNavigatorPlugin : FlutterPlugin, MethodCallHandler, NewIntentListener { + + companion object { + object MethodNames { + const val navigate = "navigate" + } + } + + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel: MethodChannel + + private val handler = Handler(Looper.getMainLooper()) + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app.guru.guru_navigator") +// channel.setMethodCallHandler(this) + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + if (call.method == "getPlatformVersion") { + result.success("Android ${android.os.Build.VERSION.RELEASE}") + } else { + result.notImplemented() + } + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { +// channel.setMethodCallHandler(null) + } + + override fun onNewIntent(intent: Intent): Boolean { + return handleIntent(intent) + } + + private fun isDynamicLink(intent: Intent): Boolean { + return intent.extras?.containsKey("com.google.firebase.dynamiclinks.DYNAMIC_LINK_DATA") == true + } + + private fun handleIntent(intent: Intent): Boolean { + val isDynamicLink = isDynamicLink(intent) + Log.w( + "MainActivity", + "handleIntent extras:${intent.extras} dynamicLink:${isDynamicLink} data:${intent.data}" + ) + + // 如果是dynamicLink 这里直接忽略,交给Flutter层进行处理 + if (isDynamicLink) { + return true + } + return intent.data?.let { uri -> + Log.d("MainActivity", "handleIntent uri:$uri") + handler.postDelayed({ + navigate(uri.toString()) + }, 100) + return@let true + } ?: false + } + + private fun navigate(uri: String) { + val params = hashMapOf("uri" to uri) + channel.invokeMethod(MethodNames.navigate, params, null) + } +} diff --git a/guru_app/plugins/guru_navigator/example/.gitignore b/guru_app/plugins/guru_navigator/example/.gitignore new file mode 100644 index 0000000..24476c5 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/guru_app/plugins/guru_navigator/example/README.md b/guru_app/plugins/guru_navigator/example/README.md new file mode 100644 index 0000000..4abb48c --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/README.md @@ -0,0 +1,16 @@ +# guru_navigator_example + +Demonstrates how to use the guru_navigator plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/guru_app/plugins/guru_navigator/example/analysis_options.yaml b/guru_app/plugins/guru_navigator/example/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/guru_app/plugins/guru_navigator/example/android/.gitignore b/guru_app/plugins/guru_navigator/example/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/guru_app/plugins/guru_navigator/example/android/app/build.gradle b/guru_app/plugins/guru_navigator/example/android/app/build.gradle new file mode 100644 index 0000000..8b48f8a --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/android/app/build.gradle @@ -0,0 +1,71 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "app.guru.guru_navigator_example" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/guru_app/plugins/guru_navigator/example/android/app/src/debug/AndroidManifest.xml b/guru_app/plugins/guru_navigator/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..9ae3da1 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/guru_app/plugins/guru_navigator/example/android/app/src/main/AndroidManifest.xml b/guru_app/plugins/guru_navigator/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..706b48f --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/guru_app/plugins/guru_navigator/example/android/app/src/main/kotlin/app/guru/guru_navigator_example/MainActivity.kt b/guru_app/plugins/guru_navigator/example/android/app/src/main/kotlin/app/guru/guru_navigator_example/MainActivity.kt new file mode 100644 index 0000000..8b9e17e --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/android/app/src/main/kotlin/app/guru/guru_navigator_example/MainActivity.kt @@ -0,0 +1,6 @@ +package app.guru.guru_navigator_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/guru_app/plugins/guru_navigator/example/android/app/src/main/res/drawable-v21/launch_background.xml b/guru_app/plugins/guru_navigator/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/guru_app/plugins/guru_navigator/example/android/app/src/main/res/drawable/launch_background.xml b/guru_app/plugins/guru_navigator/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/guru_app/plugins/guru_navigator/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/guru_app/plugins/guru_navigator/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_navigator/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/guru_app/plugins/guru_navigator/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_navigator/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/guru_app/plugins/guru_navigator/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_navigator/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/guru_app/plugins/guru_navigator/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_navigator/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/guru_app/plugins/guru_navigator/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/guru_app/plugins/guru_navigator/example/android/app/src/main/res/values-night/styles.xml b/guru_app/plugins/guru_navigator/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/guru_app/plugins/guru_navigator/example/android/app/src/main/res/values/styles.xml b/guru_app/plugins/guru_navigator/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/guru_app/plugins/guru_navigator/example/android/app/src/profile/AndroidManifest.xml b/guru_app/plugins/guru_navigator/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..9ae3da1 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/guru_app/plugins/guru_navigator/example/android/build.gradle b/guru_app/plugins/guru_navigator/example/android/build.gradle new file mode 100644 index 0000000..83ae220 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.1.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/guru_app/plugins/guru_navigator/example/android/gradle.properties b/guru_app/plugins/guru_navigator/example/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/guru_app/plugins/guru_navigator/example/android/gradle/wrapper/gradle-wrapper.properties b/guru_app/plugins/guru_navigator/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..cb24abd --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/guru_app/plugins/guru_navigator/example/android/settings.gradle b/guru_app/plugins/guru_navigator/example/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/guru_app/plugins/guru_navigator/example/ios/.gitignore b/guru_app/plugins/guru_navigator/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/guru_app/plugins/guru_navigator/example/ios/Flutter/AppFrameworkInfo.plist b/guru_app/plugins/guru_navigator/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..9625e10 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/guru_app/plugins/guru_navigator/example/ios/Flutter/Debug.xcconfig b/guru_app/plugins/guru_navigator/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/guru_app/plugins/guru_navigator/example/ios/Flutter/Release.xcconfig b/guru_app/plugins/guru_navigator/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/guru_app/plugins/guru_navigator/example/ios/Podfile b/guru_app/plugins/guru_navigator/example/ios/Podfile new file mode 100644 index 0000000..88359b2 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/guru_app/plugins/guru_navigator/example/ios/Podfile.lock b/guru_app/plugins/guru_navigator/example/ios/Podfile.lock new file mode 100644 index 0000000..9899a68 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/ios/Podfile.lock @@ -0,0 +1,22 @@ +PODS: + - Flutter (1.0.0) + - guru_navigator (0.0.1): + - Flutter + +DEPENDENCIES: + - Flutter (from `Flutter`) + - guru_navigator (from `.symlinks/plugins/guru_navigator/ios`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + guru_navigator: + :path: ".symlinks/plugins/guru_navigator/ios" + +SPEC CHECKSUMS: + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + guru_navigator: bbb8b411005f28d1e8ea139d58bb28d16d4caa9e + +PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 + +COCOAPODS: 1.11.2 diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner.xcodeproj/project.pbxproj b/guru_app/plugins/guru_navigator/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..d9bd51e --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,552 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 2190C8BF7E5848D9283F3620 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F4BF602CC44AB1528BF15A6 /* Pods_Runner.framework */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0E5DA85FEB8F6835B1E7F870 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3F4BF602CC44AB1528BF15A6 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5CF871688851F608650D4DAA /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 72CB9ECBF3364D909953C4FE /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2190C8BF7E5848D9283F3620 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1D3F516BCDBAC63A0A2FCB44 /* Pods */ = { + isa = PBXGroup; + children = ( + 5CF871688851F608650D4DAA /* Pods-Runner.debug.xcconfig */, + 72CB9ECBF3364D909953C4FE /* Pods-Runner.release.xcconfig */, + 0E5DA85FEB8F6835B1E7F870 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 61699A2E2955E65AD9133957 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 3F4BF602CC44AB1528BF15A6 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 1D3F516BCDBAC63A0A2FCB44 /* Pods */, + 61699A2E2955E65AD9133957 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + F0B84292F495E1C025D934DB /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 5774CC973F035793E84B0D30 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 5774CC973F035793E84B0D30 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + F0B84292F495E1C025D934DB /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 39253T242A; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = app.guru.guruNavigatorExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 39253T242A; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = app.guru.guruNavigatorExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 39253T242A; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = app.guru.guruNavigatorExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/guru_app/plugins/guru_navigator/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/guru_app/plugins/guru_navigator/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/guru_app/plugins/guru_navigator/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/guru_app/plugins/guru_navigator/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c87d15a --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/guru_app/plugins/guru_navigator/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/guru_app/plugins/guru_navigator/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/guru_app/plugins/guru_navigator/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/AppDelegate.swift b/guru_app/plugins/guru_navigator/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..28c6bf0 Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..f091b6b Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..d0ef06e Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..c8f9ed8 Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..75b2d16 Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c4df70d Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/guru_app/plugins/guru_navigator/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Base.lproj/Main.storyboard b/guru_app/plugins/guru_navigator/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Info.plist b/guru_app/plugins/guru_navigator/example/ios/Runner/Info.plist new file mode 100644 index 0000000..dedfb77 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/ios/Runner/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Guru Navigator + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + guru_navigator_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/guru_app/plugins/guru_navigator/example/ios/Runner/Runner-Bridging-Header.h b/guru_app/plugins/guru_navigator/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/guru_app/plugins/guru_navigator/example/lib/main.dart b/guru_app/plugins/guru_navigator/example/lib/main.dart new file mode 100644 index 0000000..2798a35 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/lib/main.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:guru_navigator/guru_navigator.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + String _platformVersion = 'Unknown'; + final _guruNavigatorPlugin = GuruNavigator(); + + @override + void initState() { + super.initState(); + initPlatformState(); + } + + // Platform messages are asynchronous, so we initialize in an async method. + Future initPlatformState() async { + String platformVersion; + // Platform messages may fail, so we use a try/catch PlatformException. + // We also handle the message potentially returning null. + + // If the widget was removed from the tree while the asynchronous platform + // message was in flight, we want to discard the reply rather than calling + // setState to update our non-existent appearance. + if (!mounted) return; + + // setState(() { + // _platformVersion = platformVersion; + // }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: Center( + child: Text('Running on: $_platformVersion\n'), + ), + ), + ); + } +} diff --git a/guru_app/plugins/guru_navigator/example/pubspec.lock b/guru_app/plugins/guru_navigator/example/pubspec.lock new file mode 100644 index 0000000..dade865 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/pubspec.lock @@ -0,0 +1,175 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.9.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.16.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.5" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + guru_navigator: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.12" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.2" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.12" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" +sdks: + dart: ">=2.18.6 <3.0.0" + flutter: ">=2.5.0" diff --git a/guru_app/plugins/guru_navigator/example/pubspec.yaml b/guru_app/plugins/guru_navigator/example/pubspec.yaml new file mode 100644 index 0000000..30d8b4c --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/pubspec.yaml @@ -0,0 +1,84 @@ +name: guru_navigator_example +description: Demonstrates how to use the guru_navigator plugin. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: '>=2.18.6 <3.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + guru_navigator: + # When depending on this package from a real application you should use: + # guru_navigator: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/guru_app/plugins/guru_navigator/example/test/widget_test.dart b/guru_app/plugins/guru_navigator/example/test/widget_test.dart new file mode 100644 index 0000000..03994c7 --- /dev/null +++ b/guru_app/plugins/guru_navigator/example/test/widget_test.dart @@ -0,0 +1,27 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:guru_navigator_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && + widget.data!.startsWith('Running on:'), + ), + findsOneWidget, + ); + }); +} diff --git a/guru_app/plugins/guru_navigator/ios/.gitignore b/guru_app/plugins/guru_navigator/ios/.gitignore new file mode 100644 index 0000000..0c88507 --- /dev/null +++ b/guru_app/plugins/guru_navigator/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/guru_app/plugins/guru_navigator/ios/Assets/.gitkeep b/guru_app/plugins/guru_navigator/ios/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/guru_app/plugins/guru_navigator/ios/Classes/GuruNavigatorPlugin.h b/guru_app/plugins/guru_navigator/ios/Classes/GuruNavigatorPlugin.h new file mode 100644 index 0000000..a50962c --- /dev/null +++ b/guru_app/plugins/guru_navigator/ios/Classes/GuruNavigatorPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface GuruNavigatorPlugin : NSObject +@end diff --git a/guru_app/plugins/guru_navigator/ios/Classes/GuruNavigatorPlugin.m b/guru_app/plugins/guru_navigator/ios/Classes/GuruNavigatorPlugin.m new file mode 100644 index 0000000..2d47ddd --- /dev/null +++ b/guru_app/plugins/guru_navigator/ios/Classes/GuruNavigatorPlugin.m @@ -0,0 +1,15 @@ +#import "GuruNavigatorPlugin.h" +#if __has_include() +#import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "guru_navigator-Swift.h" +#endif + +@implementation GuruNavigatorPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftGuruNavigatorPlugin registerWithRegistrar:registrar]; +} +@end diff --git a/guru_app/plugins/guru_navigator/ios/Classes/SwiftGuruNavigatorPlugin.swift b/guru_app/plugins/guru_navigator/ios/Classes/SwiftGuruNavigatorPlugin.swift new file mode 100644 index 0000000..cf729ab --- /dev/null +++ b/guru_app/plugins/guru_navigator/ios/Classes/SwiftGuruNavigatorPlugin.swift @@ -0,0 +1,16 @@ +import Flutter +import UIKit + +public class SwiftGuruNavigatorPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "app.guru.guru_navigator", binaryMessenger: registrar.messenger()) + let instance = SwiftGuruNavigatorPlugin() + registrar.addApplicationDelegate(instance) + registrar.addMethodCallDelegate(instance, channel: channel) + + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + result("iOS " + UIDevice.current.systemVersion) + } +} diff --git a/guru_app/plugins/guru_navigator/ios/guru_navigator.podspec b/guru_app/plugins/guru_navigator/ios/guru_navigator.podspec new file mode 100644 index 0000000..180fd68 --- /dev/null +++ b/guru_app/plugins/guru_navigator/ios/guru_navigator.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint guru_navigator.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'guru_navigator' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin project.' + s.description = <<-DESC +A new Flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '9.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' +end diff --git a/guru_app/plugins/guru_navigator/lib/guru_navigator.dart b/guru_app/plugins/guru_navigator/lib/guru_navigator.dart new file mode 100644 index 0000000..570486b --- /dev/null +++ b/guru_app/plugins/guru_navigator/lib/guru_navigator.dart @@ -0,0 +1,9 @@ +import 'package:flutter/services.dart'; + +import 'guru_navigator_platform_interface.dart'; + +class GuruNavigator { + static void init(Future Function(MethodCall call) handler) { + GuruNavigatorPlatform.instance.init(handler); + } +} diff --git a/guru_app/plugins/guru_navigator/lib/guru_navigator_method_channel.dart b/guru_app/plugins/guru_navigator/lib/guru_navigator_method_channel.dart new file mode 100644 index 0000000..b580f1f --- /dev/null +++ b/guru_app/plugins/guru_navigator/lib/guru_navigator_method_channel.dart @@ -0,0 +1,16 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'guru_navigator_platform_interface.dart'; + +/// An implementation of [GuruNavigatorPlatform] that uses method channels. +class MethodChannelGuruNavigator extends GuruNavigatorPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('app.guru.guru_navigator'); + + @override + void init(Future Function(MethodCall call) handler) { + methodChannel.setMethodCallHandler(handler); + } +} diff --git a/guru_app/plugins/guru_navigator/lib/guru_navigator_platform_interface.dart b/guru_app/plugins/guru_navigator/lib/guru_navigator_platform_interface.dart new file mode 100644 index 0000000..a84a232 --- /dev/null +++ b/guru_app/plugins/guru_navigator/lib/guru_navigator_platform_interface.dart @@ -0,0 +1,30 @@ +import 'package:flutter/services.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'guru_navigator_method_channel.dart'; + +abstract class GuruNavigatorPlatform extends PlatformInterface { + /// Constructs a GuruNavigatorPlatform. + GuruNavigatorPlatform() : super(token: _token); + + static final Object _token = Object(); + + static GuruNavigatorPlatform _instance = MethodChannelGuruNavigator(); + + /// The default instance of [GuruNavigatorPlatform] to use. + /// + /// Defaults to [MethodChannelGuruNavigator]. + static GuruNavigatorPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [GuruNavigatorPlatform] when + /// they register themselves. + static set instance(GuruNavigatorPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + void init(Future Function(MethodCall call) handler) { + throw UnimplementedError('init() has not been implemented.'); + } +} diff --git a/guru_app/plugins/guru_navigator/pubspec.lock b/guru_app/plugins/guru_navigator/pubspec.lock new file mode 100644 index 0000000..d5c5db5 --- /dev/null +++ b/guru_app/plugins/guru_navigator/pubspec.lock @@ -0,0 +1,161 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.9.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.16.0" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.12" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.2" + plugin_platform_interface: + dependency: "direct main" + description: + name: plugin_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.12" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" +sdks: + dart: ">=2.18.6 <3.0.0" + flutter: ">=2.5.0" diff --git a/guru_app/plugins/guru_navigator/pubspec.yaml b/guru_app/plugins/guru_navigator/pubspec.yaml new file mode 100644 index 0000000..b35c153 --- /dev/null +++ b/guru_app/plugins/guru_navigator/pubspec.yaml @@ -0,0 +1,72 @@ +name: guru_navigator +description: A new Flutter plugin project. +version: 0.0.1 +homepage: + +environment: + sdk: '>=2.18.6 <4.0.0' + flutter: ">=2.5.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: app.guru.guru_navigator + pluginClass: GuruNavigatorPlugin + ios: + pluginClass: GuruNavigatorPlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/guru_app/plugins/guru_navigator/test/guru_navigator_method_channel_test.dart b/guru_app/plugins/guru_navigator/test/guru_navigator_method_channel_test.dart new file mode 100644 index 0000000..3e35292 --- /dev/null +++ b/guru_app/plugins/guru_navigator/test/guru_navigator_method_channel_test.dart @@ -0,0 +1,24 @@ +// import 'package:flutter/services.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:guru_navigator/guru_navigator_method_channel.dart'; +// +// void main() { +// MethodChannelGuruNavigator platform = MethodChannelGuruNavigator(); +// const MethodChannel channel = MethodChannel('guru_navigator'); +// +// TestWidgetsFlutterBinding.ensureInitialized(); +// +// setUp(() { +// channel.setMockMethodCallHandler((MethodCall methodCall) async { +// return '42'; +// }); +// }); +// +// tearDown(() { +// channel.setMockMethodCallHandler(null); +// }); +// +// test('getPlatformVersion', () async { +// expect(await platform.getPlatformVersion(), '42'); +// }); +// } diff --git a/guru_app/plugins/guru_navigator/test/guru_navigator_test.dart b/guru_app/plugins/guru_navigator/test/guru_navigator_test.dart new file mode 100644 index 0000000..54134c2 --- /dev/null +++ b/guru_app/plugins/guru_navigator/test/guru_navigator_test.dart @@ -0,0 +1,27 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:guru_navigator/guru_navigator.dart'; +import 'package:guru_navigator/guru_navigator_platform_interface.dart'; +import 'package:guru_navigator/guru_navigator_method_channel.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +// class MockGuruNavigatorPlatform +// with MockPlatformInterfaceMixin +// implements GuruNavigatorPlatform { +// +// } + +void main() { + final GuruNavigatorPlatform initialPlatform = GuruNavigatorPlatform.instance; + + // test('$MethodChannelGuruNavigator is the default instance', () { + // expect(initialPlatform, isInstanceOf()); + // }); + // + // test('getPlatformVersion', () async { + // GuruNavigator guruNavigatorPlugin = GuruNavigator(); + // MockGuruNavigatorPlatform fakePlatform = MockGuruNavigatorPlatform(); + // GuruNavigatorPlatform.instance = fakePlatform; + // + // expect(await guruNavigatorPlugin.getPlatformVersion(), '42'); + // }); +} diff --git a/guru_app/plugins/guru_platform_data/.gitignore b/guru_app/plugins/guru_platform_data/.gitignore new file mode 100644 index 0000000..9be145f --- /dev/null +++ b/guru_app/plugins/guru_platform_data/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/guru_app/plugins/guru_platform_data/CHANGELOG.md b/guru_app/plugins/guru_platform_data/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/guru_app/plugins/guru_platform_data/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/guru_app/plugins/guru_platform_data/LICENSE b/guru_app/plugins/guru_platform_data/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/guru_app/plugins/guru_platform_data/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/guru_app/plugins/guru_platform_data/README.md b/guru_app/plugins/guru_platform_data/README.md new file mode 100644 index 0000000..c87c472 --- /dev/null +++ b/guru_app/plugins/guru_platform_data/README.md @@ -0,0 +1,15 @@ +# guru_platform_data + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/developing-packages/), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/guru_app/plugins/guru_platform_data/analysis_options.yaml b/guru_app/plugins/guru_platform_data/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/guru_app/plugins/guru_platform_data/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/guru_app/plugins/guru_platform_data/android/.gitignore b/guru_app/plugins/guru_platform_data/android/.gitignore new file mode 100644 index 0000000..c6cbe56 --- /dev/null +++ b/guru_app/plugins/guru_platform_data/android/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/guru_app/plugins/guru_platform_data/android/build.gradle b/guru_app/plugins/guru_platform_data/android/build.gradle new file mode 100644 index 0000000..3fef5c5 --- /dev/null +++ b/guru_app/plugins/guru_platform_data/android/build.gradle @@ -0,0 +1,54 @@ +group 'app.guru.guru_platform_data' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + mavenLocal() + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + mavenLocal() + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + minSdkVersion 21 + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'guru.core.checker:GuruChecker:1.0.0' + +} diff --git a/guru_app/plugins/guru_platform_data/android/gradle/wrapper/gradle-wrapper.properties b/guru_app/plugins/guru_platform_data/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..69a9715 --- /dev/null +++ b/guru_app/plugins/guru_platform_data/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/guru_app/plugins/guru_platform_data/android/settings.gradle b/guru_app/plugins/guru_platform_data/android/settings.gradle new file mode 100644 index 0000000..009e232 --- /dev/null +++ b/guru_app/plugins/guru_platform_data/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'guru_platform_data' diff --git a/guru_app/plugins/guru_platform_data/android/src/main/AndroidManifest.xml b/guru_app/plugins/guru_platform_data/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..61cd940 --- /dev/null +++ b/guru_app/plugins/guru_platform_data/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/guru_app/plugins/guru_platform_data/android/src/main/kotlin/app/guru/guru_platform_data/GuruPlatformData.kt b/guru_app/plugins/guru_platform_data/android/src/main/kotlin/app/guru/guru_platform_data/GuruPlatformData.kt new file mode 100644 index 0000000..62ca270 --- /dev/null +++ b/guru_app/plugins/guru_platform_data/android/src/main/kotlin/app/guru/guru_platform_data/GuruPlatformData.kt @@ -0,0 +1,224 @@ +package app.guru.guru_platform_data + +import android.app.Activity +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.provider.Settings +import android.view.WindowManager; +import android.util.Log +import androidx.annotation.NonNull +import guru.core.checker.GuruChecker +import io.flutter.embedding.engine.FlutterJNI +import java.time.ZoneId +import java.util.* + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +/** GuruPlatformDataPlugin */ +class GuruPlatformData : FlutterPlugin, MethodCallHandler, ActivityAware { + private var activity: Activity? = null + + companion object { + object MethodNames { + const val GetAdsLimitTracking = "get_ads_limit_tracking" + const val ShowMediationDebugger = "show_mediation_debugger" + const val GetObservatoryUri = "get_observatory_uri" + const val IsCutoutScreen = "is_cutout_screen" + const val IsAppInstalled = "is_app_installed" + const val GetBrightness = "get_brightness" + const val SetBrightness = "set_brightness" + const val ResetBrightness = "reset_brightness" + const val IsKeptScreenOn = "is_kept_screen_on" + const val KeepScreenOn = "keep_screen_on" + const val GetLocalTimezone = "getLocalTimezone" + const val GetAvailableTimezones = "getAvailableTimezones" + } + } + + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel: MethodChannel + + private val handler = Handler(Looper.getMainLooper()) + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app.guru.guru_platform_data") + channel.setMethodCallHandler(this) + } + + private fun dispatch(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + MethodNames.GetObservatoryUri -> { + handler.post { + result.success(FlutterJNI.getObservatoryUri()) + } + } + + MethodNames.IsCutoutScreen -> { + handler.post { + val cutoutScreen = isCutoutScreen() + Log.d("PDC", "cutoutScreen:$cutoutScreen") + result.success(isCutoutScreen()) + } + } + + MethodNames.IsAppInstalled -> { + handler.post { + val pkg = call.argument("package_name") ?: "" + Log.d("PDC", "isAppInstalled:$pkg") + result.success(GuruChecker.isAppInstalled(activity!!, pkg)) + } + } + + MethodNames.GetBrightness -> { + handler.post { + val brightness = getBrightness() + Log.d("PDC", "GetBrightness:$brightness") + result.success(brightness) + } + } + + MethodNames.SetBrightness -> { + handler.post { + val brightness = call.argument("brightness") ?: 1.0 + Log.d("PDC", "brightness:$brightness") + setBrightness(brightness.toFloat()) + result.success(true) + } + } + + MethodNames.IsKeptScreenOn -> { + handler.post { + val flags = activity?.window?.attributes?.flags ?: 0; + Log.d("PDC", "IsKeptScreenOn flags:$flags") + result.success((flags and WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0) + } + } + + MethodNames.KeepScreenOn -> { + handler.post { + val on: kotlin.Boolean? = call.argument("on") + Log.d("PDC", "KeepScreenOn on:$on") + val success = if (on == null) { + false + } else if (on) { + activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + true + } else { + activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + true + } + result.success(success) + } + } + + MethodNames.ResetBrightness -> { + handler.post { + val lp = activity?.window?.attributes?.apply { + screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE + } + activity?.window?.attributes = lp + result.success(true) + } + } + + MethodNames.GetLocalTimezone -> { + handler.post { + result.success(getLocalTimezone()) + } + } + + MethodNames.GetAvailableTimezones -> { + handler.post { + result.success(getAvailableTimezones()) + } + } + + else -> { + handler.post { + result.error("not impl!!!", "${call.method} not impl!", "") + } + } + } + + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + dispatch(call, result); + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } + + + private fun isCutoutScreen(): Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + activity?.let { + it.window.decorView.rootWindowInsets.displayCutout != null + } ?: false + } else { + false + } + + private fun getBrightness(): Float { + var result: Float = activity?.window?.attributes?.screenBrightness ?: 1.0f + if (result < 0) { // the application is using the system brightness + try { + result = Settings.System.getInt( + activity?.contentResolver, + Settings.System.SCREEN_BRIGHTNESS + ) / 255f + } catch (e: Settings.SettingNotFoundException) { + result = 1.0f + e.printStackTrace() + } + } + return result + } + + private fun getLocalTimezone(): String { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ZoneId.systemDefault().id + } else { + TimeZone.getDefault().id + } + } + + private fun getAvailableTimezones(): List { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ZoneId.getAvailableZoneIds().toCollection(ArrayList()) + } else { + TimeZone.getAvailableIDs().toCollection(ArrayList()) + } + } + + private fun setBrightness(brightness: Float) { + val lp = activity?.window?.attributes?.apply { + screenBrightness = brightness + } + activity?.window?.attributes = lp + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + activity = binding.activity + } + + override fun onDetachedFromActivityForConfigChanges() { + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + } + + override fun onDetachedFromActivity() { + activity = null + } +} diff --git a/guru_app/plugins/guru_platform_data/ios/.gitignore b/guru_app/plugins/guru_platform_data/ios/.gitignore new file mode 100644 index 0000000..0c88507 --- /dev/null +++ b/guru_app/plugins/guru_platform_data/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/guru_app/plugins/guru_platform_data/ios/Assets/.gitkeep b/guru_app/plugins/guru_platform_data/ios/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/guru_app/plugins/guru_platform_data/ios/Classes/GuruPlatformDataPlugin.h b/guru_app/plugins/guru_platform_data/ios/Classes/GuruPlatformDataPlugin.h new file mode 100644 index 0000000..c19f527 --- /dev/null +++ b/guru_app/plugins/guru_platform_data/ios/Classes/GuruPlatformDataPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface GuruPlatformDataPlugin : NSObject +@end diff --git a/guru_app/plugins/guru_platform_data/ios/Classes/GuruPlatformDataPlugin.m b/guru_app/plugins/guru_platform_data/ios/Classes/GuruPlatformDataPlugin.m new file mode 100644 index 0000000..ccae74a --- /dev/null +++ b/guru_app/plugins/guru_platform_data/ios/Classes/GuruPlatformDataPlugin.m @@ -0,0 +1,15 @@ +#import "GuruPlatformDataPlugin.h" +#if __has_include() +#import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "guru_platform_data-Swift.h" +#endif + +@implementation GuruPlatformDataPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftGuruPlatformDataPlugin registerWithRegistrar:registrar]; +} +@end diff --git a/guru_app/plugins/guru_platform_data/ios/Classes/SwiftGuruPlatformDataPlugin.swift b/guru_app/plugins/guru_platform_data/ios/Classes/SwiftGuruPlatformDataPlugin.swift new file mode 100644 index 0000000..1bae2a4 --- /dev/null +++ b/guru_app/plugins/guru_platform_data/ios/Classes/SwiftGuruPlatformDataPlugin.swift @@ -0,0 +1,100 @@ +import Flutter +import UIKit +import AppTrackingTransparency + +public class SwiftGuruPlatformDataPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "app.guru.guru_platform_data", binaryMessenger: registrar.messenger()) + let instance = SwiftGuruPlatformDataPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? [String: Any] ?? [String: Any]() + NSLog("[PDC] handle method:\(call.method)") + switch call.method { + case "requestTrackingAuthorization": + if #available(iOS 14, *) { + self.requestIDFA(handler: { status in + result(status.rawValue) + }) + } else { + // Fallback on earlier versions + result(0) + } + break + case "getTrackingAuthorizationStatus": + if #available(iOS 14, *) { + result(self.getTrackingAuthorizationStatus().rawValue) + } else { + // Fallback on earlier versions + result(-1) + }; + break; + case "jumpToAppPrivacySettings": + self.jumpToAppPrivacySettings() + result(true) + break; + case "get_brightness": + let brightness = UIScreen.main.brightness + NSLog("[PDC] ==> get_brightness \(brightness)") + result(brightness) + break; + case "set_brightness": + let brightness = arguments["brightness"] as? Double ?? 1.0 + NSLog("[PDC] ==> set_brightness \(brightness) \(arguments["brightness"])") + UIScreen.main.brightness = CGFloat(brightness) + result(true) + break; + case "is_kept_screen_on": + let isIdleTimerDisabled = UIApplication.shared.isIdleTimerDisabled + result(isIdleTimerDisabled) + break; + case "keep_screen_on": + let on = arguments["on"] as? Bool + if (on == nil) { + result(false) + } else { + UIApplication.shared.isIdleTimerDisabled = on! + result(true) + } + break; + case "getLocalTimezone": + let timeZone = TimeZone.current + let tzName = timeZone.identifier + result(tzName) + break; + case "getAvailableTimezones": + let knownTimeZoneNames = TimeZone.knownTimeZoneIdentifiers + result(knownTimeZoneNames) + break; + default: + result(FlutterMethodNotImplemented) + } + } + + func jumpToAppPrivacySettings() { + if let appSetting = URL(string:UIApplication.openSettingsURLString) { + if #available(iOS 10, *) { + UIApplication.shared.open(appSetting, options: [:], completionHandler: nil) + } else { + UIApplication.shared.openURL(appSetting) + } + } + } + + @available(iOS 14, *) + func requestIDFA(handler: @escaping (ATTrackingManager.AuthorizationStatus) -> Void) { + // notDetermined = 0 + // restricted = 1 + // denied = 2 + // authorized = 3 + // let _result = result + ATTrackingManager.requestTrackingAuthorization(completionHandler: handler) + } + + @available(iOS 14, *) + func getTrackingAuthorizationStatus()-> ATTrackingManager.AuthorizationStatus { + return ATTrackingManager.trackingAuthorizationStatus; + } +} diff --git a/guru_app/plugins/guru_platform_data/ios/guru_platform_data.podspec b/guru_app/plugins/guru_platform_data/ios/guru_platform_data.podspec new file mode 100644 index 0000000..8c5c64a --- /dev/null +++ b/guru_app/plugins/guru_platform_data/ios/guru_platform_data.podspec @@ -0,0 +1,25 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint guru_platform_data.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'guru_platform_data' + s.version = '0.0.1' + s.summary = 'A new Flutter project.' + s.description = <<-DESC +A new Flutter project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '10.0' + + s.ios.deployment_target = '10.0' + s.static_framework = true + # Flutter.framework does not contain a i386 slice. +# s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } +# s.swift_version = '5.0' +end diff --git a/guru_app/plugins/guru_platform_data/lib/guru_platform_data.dart b/guru_app/plugins/guru_platform_data/lib/guru_platform_data.dart new file mode 100644 index 0000000..2ec4bd0 --- /dev/null +++ b/guru_app/plugins/guru_platform_data/lib/guru_platform_data.dart @@ -0,0 +1,148 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/services.dart'; + +class TrackingAuthorizationStatus { + static const notDetermined = 0; + static const restricted = 1; + static const denied = 2; + static const authorized = 3; + static const unknown = 4; + + static String toSummary(int status) { + switch (status) { + case notDetermined: + return "notDetermined"; + case restricted: + return "restricted"; + case denied: + return "denied"; + case authorized: + return "authorized"; + default: + return "unknown"; + } + } +} + +class GuruPlatformData { + static const perform = MethodChannel("app.guru.guru_platform_data"); + + static const _getAdsLimitTracking = "get_ads_limit_tracking"; + static const _requestTrackingAuthorization = "requestTrackingAuthorization"; + static const _showMediationDebugger = "show_mediation_debugger"; + static const _getTrackingAuthorizationStatus = "getTrackingAuthorizationStatus"; + static const _getObservatoryUri = "get_observatory_uri"; + static const _isCutoutScreen = "is_cutout_screen"; + static const _jumpToAppPrivacySettings = "jumpToAppPrivacySettings"; + static const _isAppInstalled = "is_app_installed"; + + static const _getBrightness = "get_brightness"; + static const _setBrightness = "set_brightness"; + static const _resetBrightness = "reset_brightness"; + static const _isKeptScreenOn = "is_kept_screen_on"; + static const _keepScreenOn = "keep_screen_on"; + + static Future getAdsLimitTracking() { + return perform.invokeMethod(_getAdsLimitTracking); + } + + static Future showMediationDebugger() { + return perform.invokeMethod(_showMediationDebugger); + } + + static Future requestTrackingAuthorization() async { + if (Platform.isIOS) { + return (await perform.invokeMethod(_requestTrackingAuthorization)) ?? + TrackingAuthorizationStatus.unknown; + } else { + return TrackingAuthorizationStatus.authorized; + } + } + + static Future getTrackingAuthorizationStatus() async { + if (Platform.isIOS) { + return (await perform.invokeMethod( + _getTrackingAuthorizationStatus)) ?? + TrackingAuthorizationStatus.unknown; + } else { + return TrackingAuthorizationStatus.authorized; + } + } + + static Future getObservatoryUri() async { + return perform.invokeMethod(_getObservatoryUri); + } + + static Future isCutoutScreen() async { + if (Platform.isAndroid) { + return perform.invokeMethod(_isCutoutScreen); + } else { + return false; + } + } + + static Future isAppInstalled(String packageName) async { + if (Platform.isAndroid) { + return (await perform.invokeMethod( + _isAppInstalled, {"package_name": packageName})) ?? + false; + } else { + return false; + } + } + + static Future jumpToAppPrivacySettings() async { + if (Platform.isIOS) { + return await perform.invokeMethod(_jumpToAppPrivacySettings) ?? + false; + } else { + return true; + } + } + + static Future getBrightness() async { + return await perform.invokeMethod(_getBrightness) ?? 1.0; + } + + static Future setBrightness(double brightness) async { + return await perform.invokeMethod( + _setBrightness, {"brightness": brightness.clamp(0.0, 1.0)}) ?? false; + } + + static Future keepScreenOn(bool on) async { + return await perform.invokeMethod( + _keepScreenOn, {"on": on}) ?? false; + } + + static Future isKeptScreenOn() async { + return await perform.invokeMethod(_isKeptScreenOn) ?? false; + } + + /// + /// Returns local timezone from the native layer. + /// + static Future getLocalTimezone() async { + final String? localTimezone = + await perform.invokeMethod("getLocalTimezone"); + if (localTimezone == null) { + throw ArgumentError("Invalid return from platform getLocalTimezone()"); + } + return localTimezone; + } + + /// + /// Gets the list of available timezones from the native layer. + /// + static Future> getAvailableTimezones() async { + final List? availableTimezones = + await perform.invokeListMethod("getAvailableTimezones"); + if (availableTimezones == null) { + throw ArgumentError( + "Invalid return from platform getAvailableTimezones()"); + } + return availableTimezones; + } + +} diff --git a/guru_app/plugins/guru_platform_data/lib/guru_platform_data_web.dart b/guru_app/plugins/guru_platform_data/lib/guru_platform_data_web.dart new file mode 100644 index 0000000..a45283d --- /dev/null +++ b/guru_app/plugins/guru_platform_data/lib/guru_platform_data_web.dart @@ -0,0 +1,44 @@ +import 'dart:async'; +// In order to *not* need this ignore, consider extracting the "web" version +// of your plugin as a separate package, instead of inlining it in the same +// package as the core of your plugin. +// ignore: avoid_web_libraries_in_flutter +import 'dart:html' as html show window; + +import 'package:flutter/services.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +/// A web implementation of the GuruPlatformData plugin. +class GuruPlatformDataWeb { + static void registerWith(Registrar registrar) { + final MethodChannel channel = MethodChannel( + 'guru_platform_data', + const StandardMethodCodec(), + registrar, + ); + + final pluginInstance = GuruPlatformDataWeb(); + channel.setMethodCallHandler(pluginInstance.handleMethodCall); + } + + /// Handles method calls over the MethodChannel of this plugin. + /// Note: Check the "federated" architecture for a new way of doing this: + /// https://flutter.dev/go/federated-plugins + Future handleMethodCall(MethodCall call) async { + switch (call.method) { + case 'getPlatformVersion': + return getPlatformVersion(); + default: + throw PlatformException( + code: 'Unimplemented', + details: 'guru_platform_data for web doesn\'t implement \'${call.method}\'', + ); + } + } + + /// Returns a [String] containing the version of the platform. + Future getPlatformVersion() { + final version = html.window.navigator.userAgent; + return Future.value(version); + } +} diff --git a/guru_app/plugins/guru_platform_data/pubspec.yaml b/guru_app/plugins/guru_platform_data/pubspec.yaml new file mode 100644 index 0000000..ea21dea --- /dev/null +++ b/guru_app/plugins/guru_platform_data/pubspec.yaml @@ -0,0 +1,70 @@ +name: guru_platform_data +description: A new Flutter project. +version: 0.0.1 +homepage: + +environment: + sdk: ">=2.16.2 <3.0.0" + flutter: ">=2.10.5" + +dependencies: + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^1.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' and Android 'package' identifiers should not ordinarily + # be modified. They are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: app.guru.guru_platform_data + pluginClass: GuruPlatformData + ios: + pluginClass: GuruPlatformDataPlugin + web: + pluginClass: GuruPlatformDataWeb + fileName: guru_platform_data_web.dart + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/guru_app/plugins/guru_platform_data/test/guru_platform_data_test.dart b/guru_app/plugins/guru_platform_data/test/guru_platform_data_test.dart new file mode 100644 index 0000000..fe86c27 --- /dev/null +++ b/guru_app/plugins/guru_platform_data/test/guru_platform_data_test.dart @@ -0,0 +1,19 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:guru_platform_data/guru_platform_data.dart'; + +void main() { + const MethodChannel channel = MethodChannel('guru_platform_data'); + + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return '42'; + }); + }); + + tearDown(() { + channel.setMockMethodCallHandler(null); + }); +} diff --git a/guru_app/plugins/persistent/.gitignore b/guru_app/plugins/persistent/.gitignore new file mode 100644 index 0000000..4d4ec2c --- /dev/null +++ b/guru_app/plugins/persistent/.gitignore @@ -0,0 +1,121 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +.vscode +.gradle +.idea +/local.properties +.DS_Store +/build +.metadata +ios/Flutter/flutter_export_environment.sh + +**/.settings +**/.project +**/.classpath + + +# build id +build_id.properties + +# IntelliJ related + +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +/build/ +flutter_* +.flutter-plugins-dependencies + +# goCli related +go-cli/.packages +go-cli/pubspec.lock + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/gradlew +**/android/gradlew.bat +**/android/fastlane/report.xml +**/android/fastlane/README.md + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* +**/ios/build/* +**/ios/fastlane/README.md +**/ios/fastlane/report.xml + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages + +# Crashlytics +debugSymbols/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# i18n +**/android/res/values/strings_en.arb +lib/generated/i18n.dart + +# output +output/ios/adhok/* +output/ios/appstore/* +ios/fastlane/report.xml +android/fastlane/report.xml + +**/android/fastlane/metadata/android/**/images/** +**/ios/fastlane/screenshots/** +**/android/.safedk \ No newline at end of file diff --git a/guru_app/plugins/persistent/CHANGELOG.md b/guru_app/plugins/persistent/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/guru_app/plugins/persistent/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/guru_app/plugins/persistent/LICENSE b/guru_app/plugins/persistent/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/guru_app/plugins/persistent/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/guru_app/plugins/persistent/README.md b/guru_app/plugins/persistent/README.md new file mode 100644 index 0000000..dd92fc0 --- /dev/null +++ b/guru_app/plugins/persistent/README.md @@ -0,0 +1,15 @@ +# persistent + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/developing-packages/), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter development, view the +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/guru_app/plugins/persistent/analysis_options.yaml b/guru_app/plugins/persistent/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/guru_app/plugins/persistent/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/guru_app/plugins/persistent/android/.gitignore b/guru_app/plugins/persistent/android/.gitignore new file mode 100644 index 0000000..161bdcd --- /dev/null +++ b/guru_app/plugins/persistent/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/guru_app/plugins/persistent/android/build.gradle b/guru_app/plugins/persistent/android/build.gradle new file mode 100644 index 0000000..1708a3f --- /dev/null +++ b/guru_app/plugins/persistent/android/build.gradle @@ -0,0 +1,50 @@ +group 'app.guru.persistent' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.2.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 31 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + minSdkVersion 21 + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/guru_app/plugins/persistent/android/settings.gradle b/guru_app/plugins/persistent/android/settings.gradle new file mode 100644 index 0000000..6b539f9 --- /dev/null +++ b/guru_app/plugins/persistent/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'persistent' diff --git a/guru_app/plugins/persistent/android/src/main/AndroidManifest.xml b/guru_app/plugins/persistent/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b16ebe5 --- /dev/null +++ b/guru_app/plugins/persistent/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/guru_app/plugins/persistent/android/src/main/kotlin/app/guru/persistent/PersistentPlugin.kt b/guru_app/plugins/persistent/android/src/main/kotlin/app/guru/persistent/PersistentPlugin.kt new file mode 100644 index 0000000..cb0e14b --- /dev/null +++ b/guru_app/plugins/persistent/android/src/main/kotlin/app/guru/persistent/PersistentPlugin.kt @@ -0,0 +1,30 @@ +package app.guru.persistent + +import androidx.annotation.NonNull +import app.guru.persistent.log.PersistentLog + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +/** PersistentPlugin */ +class PersistentPlugin : FlutterPlugin { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var persistentLog: PersistentLog + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + persistentLog = PersistentLog( + flutterPluginBinding.applicationContext, + flutterPluginBinding.binaryMessenger + ) + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + persistentLog.dispose() + } +} diff --git a/guru_app/plugins/persistent/android/src/main/kotlin/app/guru/persistent/log/PersistentLog.kt b/guru_app/plugins/persistent/android/src/main/kotlin/app/guru/persistent/log/PersistentLog.kt new file mode 100644 index 0000000..483fac4 --- /dev/null +++ b/guru_app/plugins/persistent/android/src/main/kotlin/app/guru/persistent/log/PersistentLog.kt @@ -0,0 +1,221 @@ +package app.guru.persistent.log + +import android.content.Context +import android.os.Environment +import android.os.Handler +import android.os.Looper +import android.util.Log +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import java.io.File +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.Executors +import java.util.logging.* +import java.util.logging.Formatter +import kotlin.collections.HashMap + + +object MethodNames { + const val CREATE = "create_log" + const val CLEAR = "clear_log" + const val LOG = "log" + const val GET_LOG_ROOT_DIR = "get_log_root_dir" +} + +object PersistentLoggerConstants { + object ParamNames { + const val LOGGER_ID = "logger_id" + const val LOG_NAME = "log_name" + const val FILE_SIZE_LIMIT = "file_size" + const val FILE_COUNT = "file_count" + const val LEVEL = "level" + const val FORMATTER = "formatter" + const val ECHO = "echo" + const val MESSAGE = "msg" + } + + object Formatter { + const val MAIN = 0 + const val RAW = 1 + } +} + +class MainFormatter : Formatter() { + + private val dateTimeFormatter = SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.getDefault()) + + override fun format(record: LogRecord?): String { + if (record == null) return "" + val formatTime = dateTimeFormatter.format(record.millis) + val message = formatMessage(record) + return "$formatTime ${record.level.name} $message\n" + } +} + +class RawFormatter : Formatter() { + + override fun format(record: LogRecord?): String { + if (record == null) return "" + return "${formatMessage(record)}\n" + } +} + +class PersistentLog(private val context: Context, messenger: BinaryMessenger) { + + companion object { + const val channelName = "app.guru.persistent.LOG" + private const val LOGS_DIRECTORY = "guru/logs" + private val defaultLevel = object : Level("default", 2627, "app.guru.log") {} + } + + private val channel: MethodChannel = + MethodChannel(messenger, channelName) + + init { + channel.setMethodCallHandler { call, result -> + dispatch(call, result) + } + } + + private val handler = Handler(Looper.getMainLooper()) + + private val deliverExecutor = Executors.newSingleThreadExecutor() + + private val resolvedLogsDirectory by lazy { + File( + if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState() && !Environment.isExternalStorageRemovable()) { + context.getExternalFilesDir(null) + } else { + context.filesDir + }, LOGS_DIRECTORY + ).also { it.mkdirs() } + } + + private val loggers = HashMap() + + fun dispose() { + channel.setMethodCallHandler(null) + } + + private fun getLogger(name: String): Logger? { + return loggers[name] + } + + private fun createLogger(call: MethodCall, result: MethodChannel.Result) { + val logName = + call.argument(PersistentLoggerConstants.ParamNames.LOG_NAME) + val fileSizeLimit = + call.argument(PersistentLoggerConstants.ParamNames.FILE_SIZE_LIMIT) + val fileCount = + call.argument(PersistentLoggerConstants.ParamNames.FILE_COUNT) + val echo = + call.argument(PersistentLoggerConstants.ParamNames.ECHO) ?: false + Log.d("PLC", "createLogger: $logName $fileSizeLimit $fileCount ") + + if (logName == null || fileSizeLimit == null || fileCount == null) { + result.error("INVALID_ARGUMENTS", "Invalid arguments", null) + return + } + val filePattern = resolvedLogsDirectory.path + "/" + logName + val l = getLogger(logName) + if (l != null) { + result.error("ALREADY_EXISTS", "Logger already exists", null) + return + } + val formatter = + when (call.argument(PersistentLoggerConstants.ParamNames.FORMATTER)) { + PersistentLoggerConstants.Formatter.RAW -> RawFormatter() + else -> MainFormatter() + } + val logger = Logger.getLogger(logName).also { + kotlin.runCatching { + FileHandler(filePattern, fileSizeLimit, fileCount, true) + }.getOrNull()?.let { fileHandler -> + fileHandler.formatter = formatter + it.useParentHandlers = echo + it.addHandler(fileHandler) + } + } + loggers[logName] = logger + result.success(true) + } + + private fun clearLogger(call: MethodCall, result: MethodChannel.Result) { + val logName = + call.argument(PersistentLoggerConstants.ParamNames.LOG_NAME) + if (logName == null) { + result.error("INVALID_ARGUMENTS", "Invalid arguments", null) + return + } + val logDir = resolvedLogsDirectory.path + val file = File(logDir) + file.listFiles { _, name -> + name.startsWith(logName) + }?.forEach { + it.delete() + } + loggers.remove(logName) + result.success(true) + } + + private fun log(call: MethodCall, result: MethodChannel.Result) { + val logName = + call.argument(PersistentLoggerConstants.ParamNames.LOG_NAME) + val msg = call.argument(PersistentLoggerConstants.ParamNames.MESSAGE) + if (logName == null || msg == null) { + result.error("INVALID_ARGUMENTS", "Invalid arguments", null) + return + } + + getLogger(logName)?.run { + deliverExecutor.execute { + Log.d("PLC", "[log]: $logName $msg") + log(defaultLevel, msg) + } + } + Log.d("PLC", "log COMPLETE: $logName $msg") + result.success(true) + } + + private fun getLogRootDir(call: MethodCall, result: MethodChannel.Result) { + result.success(resolvedLogsDirectory.path) + } + + private fun dispatch(call: MethodCall, result: MethodChannel.Result) { + Log.d("PLC", "dispatch: ${call.method}") + when (call.method) { + MethodNames.CREATE -> { + handler.post { + createLogger(call, result) + } + } + + MethodNames.CLEAR -> { + handler.post { + clearLogger(call, result) + } + } + + MethodNames.LOG -> { + handler.post { + log(call, result) + } + } + + MethodNames.GET_LOG_ROOT_DIR -> { + handler.post { + getLogRootDir(call, result) + } + } + else -> { + handler.post { + result.error("not impl!!!", "${call.method} not impl!", "") + } + } + } + + } + +} \ No newline at end of file diff --git a/guru_app/plugins/persistent/example/.gitignore b/guru_app/plugins/persistent/example/.gitignore new file mode 100644 index 0000000..24476c5 --- /dev/null +++ b/guru_app/plugins/persistent/example/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/guru_app/plugins/persistent/example/README.md b/guru_app/plugins/persistent/example/README.md new file mode 100644 index 0000000..923cf95 --- /dev/null +++ b/guru_app/plugins/persistent/example/README.md @@ -0,0 +1,16 @@ +# persistent_example + +Demonstrates how to use the persistent plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/guru_app/plugins/persistent/example/analysis_options.yaml b/guru_app/plugins/persistent/example/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/guru_app/plugins/persistent/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/guru_app/plugins/persistent/example/android/.gitignore b/guru_app/plugins/persistent/example/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/guru_app/plugins/persistent/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/guru_app/plugins/persistent/example/android/app/build.gradle b/guru_app/plugins/persistent/example/android/app/build.gradle new file mode 100644 index 0000000..bfbd212 --- /dev/null +++ b/guru_app/plugins/persistent/example/android/app/build.gradle @@ -0,0 +1,75 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "app.guru.persistent_example" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} +android { + defaultConfig { + minSdkVersion 21 + } +} +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/guru_app/plugins/persistent/example/android/app/src/debug/AndroidManifest.xml b/guru_app/plugins/persistent/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..25f9cee --- /dev/null +++ b/guru_app/plugins/persistent/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/guru_app/plugins/persistent/example/android/app/src/main/AndroidManifest.xml b/guru_app/plugins/persistent/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..1ccaa80 --- /dev/null +++ b/guru_app/plugins/persistent/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/guru_app/plugins/persistent/example/android/app/src/main/kotlin/app/guru/persistent_example/MainActivity.kt b/guru_app/plugins/persistent/example/android/app/src/main/kotlin/app/guru/persistent_example/MainActivity.kt new file mode 100644 index 0000000..7e8581e --- /dev/null +++ b/guru_app/plugins/persistent/example/android/app/src/main/kotlin/app/guru/persistent_example/MainActivity.kt @@ -0,0 +1,6 @@ +package app.guru.persistent_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/guru_app/plugins/persistent/example/android/app/src/main/res/drawable-v21/launch_background.xml b/guru_app/plugins/persistent/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/guru_app/plugins/persistent/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/guru_app/plugins/persistent/example/android/app/src/main/res/drawable/launch_background.xml b/guru_app/plugins/persistent/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/guru_app/plugins/persistent/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/guru_app/plugins/persistent/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/guru_app/plugins/persistent/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/guru_app/plugins/persistent/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/guru_app/plugins/persistent/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/guru_app/plugins/persistent/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/guru_app/plugins/persistent/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/guru_app/plugins/persistent/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/guru_app/plugins/persistent/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/guru_app/plugins/persistent/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/guru_app/plugins/persistent/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/guru_app/plugins/persistent/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/guru_app/plugins/persistent/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/guru_app/plugins/persistent/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/guru_app/plugins/persistent/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/guru_app/plugins/persistent/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/guru_app/plugins/persistent/example/android/app/src/main/res/values-night/styles.xml b/guru_app/plugins/persistent/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/guru_app/plugins/persistent/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/guru_app/plugins/persistent/example/android/app/src/main/res/values/styles.xml b/guru_app/plugins/persistent/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/guru_app/plugins/persistent/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/guru_app/plugins/persistent/example/android/app/src/profile/AndroidManifest.xml b/guru_app/plugins/persistent/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..25f9cee --- /dev/null +++ b/guru_app/plugins/persistent/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/guru_app/plugins/persistent/example/android/build.gradle b/guru_app/plugins/persistent/example/android/build.gradle new file mode 100644 index 0000000..83ae220 --- /dev/null +++ b/guru_app/plugins/persistent/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.1.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/guru_app/plugins/persistent/example/android/gradle.properties b/guru_app/plugins/persistent/example/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/guru_app/plugins/persistent/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/guru_app/plugins/persistent/example/android/gradle/wrapper/gradle-wrapper.properties b/guru_app/plugins/persistent/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..cb24abd --- /dev/null +++ b/guru_app/plugins/persistent/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/guru_app/plugins/persistent/example/android/settings.gradle b/guru_app/plugins/persistent/example/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/guru_app/plugins/persistent/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/guru_app/plugins/persistent/example/ios/.gitignore b/guru_app/plugins/persistent/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/guru_app/plugins/persistent/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/guru_app/plugins/persistent/example/ios/Flutter/AppFrameworkInfo.plist b/guru_app/plugins/persistent/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..9625e10 --- /dev/null +++ b/guru_app/plugins/persistent/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/guru_app/plugins/persistent/example/ios/Flutter/Debug.xcconfig b/guru_app/plugins/persistent/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/guru_app/plugins/persistent/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/guru_app/plugins/persistent/example/ios/Flutter/Release.xcconfig b/guru_app/plugins/persistent/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/guru_app/plugins/persistent/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/guru_app/plugins/persistent/example/ios/Podfile b/guru_app/plugins/persistent/example/ios/Podfile new file mode 100644 index 0000000..88359b2 --- /dev/null +++ b/guru_app/plugins/persistent/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/guru_app/plugins/persistent/example/ios/Podfile.lock b/guru_app/plugins/persistent/example/ios/Podfile.lock new file mode 100644 index 0000000..cede304 --- /dev/null +++ b/guru_app/plugins/persistent/example/ios/Podfile.lock @@ -0,0 +1,22 @@ +PODS: + - Flutter (1.0.0) + - persistent (0.0.1): + - Flutter + +DEPENDENCIES: + - Flutter (from `Flutter`) + - persistent (from `.symlinks/plugins/persistent/ios`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + persistent: + :path: ".symlinks/plugins/persistent/ios" + +SPEC CHECKSUMS: + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + persistent: 827091a151187057e32982f0c7bba6d7228f29df + +PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 + +COCOAPODS: 1.11.3 diff --git a/guru_app/plugins/persistent/example/ios/Runner.xcodeproj/project.pbxproj b/guru_app/plugins/persistent/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..6b0de7b --- /dev/null +++ b/guru_app/plugins/persistent/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,552 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + C333BF7506D60350C820C230 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8F460E95299AD6592A682656 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 8F460E95299AD6592A682656 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8F55701568C9C9D0DBC5BDEF /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A2A80FB1A7572041F58B5B02 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + F63B3173248D66ECC93AA186 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C333BF7506D60350C820C230 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 4A4E96D86979401B03FDCEB9 /* Pods */ = { + isa = PBXGroup; + children = ( + F63B3173248D66ECC93AA186 /* Pods-Runner.debug.xcconfig */, + 8F55701568C9C9D0DBC5BDEF /* Pods-Runner.release.xcconfig */, + A2A80FB1A7572041F58B5B02 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 5FC3E6066965601CBD3C3DFD /* Frameworks */ = { + isa = PBXGroup; + children = ( + 8F460E95299AD6592A682656 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 4A4E96D86979401B03FDCEB9 /* Pods */, + 5FC3E6066965601CBD3C3DFD /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 2A4C74FE3110D63F20022B71 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 3F2A962FB3198F3419FC9A07 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 2A4C74FE3110D63F20022B71 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 3F2A962FB3198F3419FC9A07 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 39253T242A; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = app.guru.persistentExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 39253T242A; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = app.guru.persistentExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 39253T242A; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = app.guru.persistentExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/guru_app/plugins/persistent/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/guru_app/plugins/persistent/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/guru_app/plugins/persistent/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/guru_app/plugins/persistent/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/guru_app/plugins/persistent/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/guru_app/plugins/persistent/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/guru_app/plugins/persistent/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/guru_app/plugins/persistent/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/guru_app/plugins/persistent/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/guru_app/plugins/persistent/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/guru_app/plugins/persistent/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c87d15a --- /dev/null +++ b/guru_app/plugins/persistent/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_app/plugins/persistent/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/guru_app/plugins/persistent/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/guru_app/plugins/persistent/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/guru_app/plugins/persistent/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/guru_app/plugins/persistent/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/guru_app/plugins/persistent/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/guru_app/plugins/persistent/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/guru_app/plugins/persistent/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/guru_app/plugins/persistent/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/guru_app/plugins/persistent/example/ios/Runner/AppDelegate.swift b/guru_app/plugins/persistent/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/guru_app/plugins/persistent/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..28c6bf0 Binary files /dev/null and b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..f091b6b Binary files /dev/null and b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..d0ef06e Binary files /dev/null and b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..c8f9ed8 Binary files /dev/null and b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..75b2d16 Binary files /dev/null and b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c4df70d Binary files /dev/null and b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/guru_app/plugins/persistent/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/guru_app/plugins/persistent/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/guru_app/plugins/persistent/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/guru_app/plugins/persistent/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_app/plugins/persistent/example/ios/Runner/Base.lproj/Main.storyboard b/guru_app/plugins/persistent/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/guru_app/plugins/persistent/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_app/plugins/persistent/example/ios/Runner/Info.plist b/guru_app/plugins/persistent/example/ios/Runner/Info.plist new file mode 100644 index 0000000..98b64ce --- /dev/null +++ b/guru_app/plugins/persistent/example/ios/Runner/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Persistent + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + persistent_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/guru_app/plugins/persistent/example/ios/Runner/Runner-Bridging-Header.h b/guru_app/plugins/persistent/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/guru_app/plugins/persistent/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/guru_app/plugins/persistent/example/lib/main.dart b/guru_app/plugins/persistent/example/lib/main.dart new file mode 100644 index 0000000..3b85939 --- /dev/null +++ b/guru_app/plugins/persistent/example/lib/main.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; +import 'dart:io'; +import 'package:persistent/log/persistent_log.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + String _platformVersion = 'Unknown'; + + @override + void initState() { + super.initState(); + initPlatformState(); + } + + // Platform messages are asynchronous, so we initialize in an async method. + Future initPlatformState() async { + const String platformVersion = "Unknown"; + + // If the widget was removed from the tree while the asynchronous platform + // message was in flight, we want to discard the reply rather than calling + // setState to update our non-existent appearance. + if (!mounted) return; + + setState(() { + _platformVersion = platformVersion; + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: Center( + child: Column( + children: [ + TextButton( + onPressed: () { + PersistentLog.createLogger(logName: "aa_1",formatter: Formatter.raw,echo: false); + }, + child: Container( + color: Colors.grey, + width: 100, + height: 40, + child: Center( + child: Text("create logger"), + ), + )), + TextButton( + onPressed: () { + PersistentLog.clearLogger(logName: "aa_1"); + }, + child: Container( + color: Colors.grey, + width: 100, + height: 40, + child: Center( + child: Text("clear logger"), + ), + )), + TextButton( + onPressed: () { + PersistentLog.log(logName: "aa_1", message: "message"); + }, + child: Container( + color: Colors.grey, + width: 100, + height: 40, + child: Center( + child: Text(" log"), + ), + )), + TextButton( + onPressed: ()async{ + PersistentLog.getLogRootDir().then((file) { + final path = file.path; + print("path:$path"); + }); + + }, + child: Container( + color: Colors.grey, + width: 100, + height: 40, + child: Center( + child: Text("root dir"), + ), + )), + ], + ), + ), + ), + ); + } +} diff --git a/guru_app/plugins/persistent/example/pubspec.lock b/guru_app/plugins/persistent/example/pubspec.lock new file mode 100644 index 0000000..2a6126e --- /dev/null +++ b/guru_app/plugins/persistent/example/pubspec.lock @@ -0,0 +1,175 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.9.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.16.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.5" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.12" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.2" + persistent: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.12" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" +sdks: + dart: ">=2.18.6 <3.0.0" + flutter: ">=2.5.0" diff --git a/guru_app/plugins/persistent/example/pubspec.yaml b/guru_app/plugins/persistent/example/pubspec.yaml new file mode 100644 index 0000000..a8cb40d --- /dev/null +++ b/guru_app/plugins/persistent/example/pubspec.yaml @@ -0,0 +1,84 @@ +name: persistent_example +description: Demonstrates how to use the persistent plugin. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: '>=2.18.6 <3.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + persistent: + # When depending on this package from a real application you should use: + # persistent: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/guru_app/plugins/persistent/example/test/widget_test.dart b/guru_app/plugins/persistent/example/test/widget_test.dart new file mode 100644 index 0000000..2c95875 --- /dev/null +++ b/guru_app/plugins/persistent/example/test/widget_test.dart @@ -0,0 +1,27 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:persistent_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && + widget.data!.startsWith('Running on:'), + ), + findsOneWidget, + ); + }); +} diff --git a/guru_app/plugins/persistent/ios/.gitignore b/guru_app/plugins/persistent/ios/.gitignore new file mode 100644 index 0000000..0c88507 --- /dev/null +++ b/guru_app/plugins/persistent/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/guru_app/plugins/persistent/ios/Assets/.gitkeep b/guru_app/plugins/persistent/ios/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDFileLogger.h b/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDFileLogger.h new file mode 100644 index 0000000..2435e37 --- /dev/null +++ b/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDFileLogger.h @@ -0,0 +1,226 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2023, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software in source and binary forms, +// with or without modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Neither the name of Deusty nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific +// prior written permission of Deusty, LLC. + +// Disable legacy macros +#ifndef DD_LEGACY_MACROS + #define DD_LEGACY_MACROS 0 +#endif +#ifndef DD_NSLOG_LEVEL + #define DD_NSLOG_LEVEL 2 +#endif + + + + + + +#import "DDLog.h" +#import "DDFileLogger.h" +#import "DDLogFileInfo.h" +#import "DDLogFileManager.h" +#import "DDLogFileManagerDefault.h" +#import "DDLogFileFormatterDefault.h" + +#define NSLogError(frmt, ...) do{ if(DD_NSLOG_LEVEL >= 1) NSLog((frmt), ##__VA_ARGS__); } while(0) +#define NSLogWarn(frmt, ...) do{ if(DD_NSLOG_LEVEL >= 2) NSLog((frmt), ##__VA_ARGS__); } while(0) +#define NSLogInfo(frmt, ...) do{ if(DD_NSLOG_LEVEL >= 3) NSLog((frmt), ##__VA_ARGS__); } while(0) +#define NSLogDebug(frmt, ...) do{ if(DD_NSLOG_LEVEL >= 4) NSLog((frmt), ##__VA_ARGS__); } while(0) +#define NSLogVerbose(frmt, ...) do{ if(DD_NSLOG_LEVEL >= 5) NSLog((frmt), ##__VA_ARGS__); } while(0) + + +NS_ASSUME_NONNULL_BEGIN + +/** + * This class provides a logger to write log statements to a file. + **/ + + +// Default configuration and safety/sanity values. +// +// maximumFileSize -> kDDDefaultLogMaxFileSize +// rollingFrequency -> kDDDefaultLogRollingFrequency +// maximumNumberOfLogFiles -> kDDDefaultLogMaxNumLogFiles +// logFilesDiskQuota -> kDDDefaultLogFilesDiskQuota +// +// You should carefully consider the proper configuration values for your application. + +extern unsigned long long const kDDDefaultLogMaxFileSize; +extern NSTimeInterval const kDDDefaultLogRollingFrequency; +extern NSUInteger const kDDDefaultLogMaxNumLogFiles; +extern unsigned long long const kDDDefaultLogFilesDiskQuota; + + + +/** + * The standard implementation for a file logger + */ +@interface DDFileLogger : DDAbstractLogger + + + +/** + * Designated initializer, requires a `DDLogFileManager` instance. + * A global queue w/ default priority is used to run callbacks. + * If needed, specify queue using `initWithLogFileManager:completionQueue:`. + */ +- (instancetype)initWithLogFileManager:(id )logFileManager logfileFormatter:(nullable DDLogFileFormatterDefault *)logfileFormatter; + +/** + * Designated initializer, requires a `DDLogFileManager` instance. + * The completionQueue is used to execute `didArchiveLogFile:wasRolled:`, + * and the callback in `rollLogFileWithCompletionBlock:`. + * If nil, a global queue w/ default priority is used. + */ +- (instancetype)initWithLogFileManager:(id )logFileManager logfileFormatter:(nullable DDLogFileFormatterDefault *)logfileFormatter completionQueue:(nullable dispatch_queue_t)dispatchQueue NS_DESIGNATED_INITIALIZER; + +/** + * Deprecated. Use `willLogMessage:` + */ +- (void)willLogMessage __attribute__((deprecated("Use -willLogMessage:"))) NS_REQUIRES_SUPER; + +/** + * Deprecated. Use `didLogMessage:` + */ +- (void)didLogMessage __attribute__((deprecated("Use -didLogMessage:"))) NS_REQUIRES_SUPER; + +/** + * Called when the logger is about to write message. Call super before your implementation. + */ +- (void)willLogMessage:(DDLogFileInfo *)logFileInfo NS_REQUIRES_SUPER; + +/** + * Called when the logger wrote message. Call super after your implementation. + */ +- (void)didLogMessage:(DDLogFileInfo *)logFileInfo NS_REQUIRES_SUPER; + +/** + * Writes all in-memory log data to the permanent storage. Call super before your implementation. + * Don't call this method directly, instead use the `[DDLog flushLog]` to ensure all log messages are included in flush. + */ +- (void)flush NS_REQUIRES_SUPER; + +/** + * Called when the logger checks archive or not current log file. + * Override this method to extend standard behavior. By default returns NO. + * This is executed directly on the logger's internal queue, so keep processing light! + */ +- (BOOL)shouldArchiveRecentLogFileInfo:(DDLogFileInfo *)recentLogFileInfo; + +/** + * Log File Rolling: + * + * `maximumFileSize`: + * The approximate maximum size (in bytes) to allow log files to grow. + * If a log file is larger than this value after a log statement is appended, + * then the log file is rolled. + * + * `rollingFrequency` + * How often to roll the log file. + * The frequency is given as an `NSTimeInterval`, which is a double that specifies the interval in seconds. + * Once the log file gets to be this old, it is rolled. + * + * `doNotReuseLogFiles` + * When set, will always create a new log file at application launch. + * + * Both the `maximumFileSize` and the `rollingFrequency` are used to manage rolling. + * Whichever occurs first will cause the log file to be rolled. + * + * For example: + * The `rollingFrequency` is 24 hours, + * but the log file surpasses the `maximumFileSize` after only 20 hours. + * The log file will be rolled at that 20 hour mark. + * A new log file will be created, and the 24 hour timer will be restarted. + * + * You may optionally disable rolling due to filesize by setting `maximumFileSize` to zero. + * If you do so, rolling is based solely on `rollingFrequency`. + * + * You may optionally disable rolling due to time by setting `rollingFrequency` to zero (or any non-positive number). + * If you do so, rolling is based solely on `maximumFileSize`. + * + * If you disable both `maximumFileSize` and `rollingFrequency`, then the log file won't ever be rolled. + * This is strongly discouraged. + **/ +@property (readwrite, assign) unsigned long long maximumFileSize; + +/** + * See description for `maximumFileSize` + */ +@property (readwrite, assign) NSTimeInterval rollingFrequency; + +/** + * See description for `maximumFileSize` + */ +@property (readwrite, assign, atomic) BOOL doNotReuseLogFiles; + +/** + * The DDLogFileManager instance can be used to retrieve the list of log files, + * and configure the maximum number of archived log files to keep. + * + * @see DDLogFileManager.maximumNumberOfLogFiles + **/ +@property (strong, nonatomic, readonly) id logFileManager; + +/** + * When using a custom formatter you can set the `logMessage` method not to append + * `\n` character after each output. This allows for some greater flexibility with + * custom formatters. Default value is YES. + **/ +@property (nonatomic, readwrite, assign) BOOL automaticallyAppendNewlineForCustomFormatters; + +/** + * You can optionally force the current log file to be rolled with this method. + * CompletionBlock will be called on main queue. + */ +- (void)rollLogFileWithCompletionBlock:(nullable void (^)(void))completionBlock + NS_SWIFT_NAME(rollLogFile(withCompletion:)); + +/** + * Method is deprecated. + * @deprecated Use `rollLogFileWithCompletionBlock:` method instead. + */ +- (void)rollLogFile __attribute__((deprecated("Use -rollLogFileWithCompletionBlock:"))); + + + + +- (void)logData:(NSData *)data; + +// Will assert if used outside logger's queue. +- (void)lt_logData:(NSData *)data; + +- (nullable NSData *)lt_dataForMessage:(DDLogMessage *)message; + + +// Inherited from DDAbstractLogger + +// - (id )logFormatter; +// - (void)setLogFormatter:(id )formatter; + +/** + * Returns the log file that should be used. + * If there is an existing log file that is suitable, + * within the constraints of `maximumFileSize` and `rollingFrequency`, then it is returned. + * + * Otherwise a new file is created and returned. If this failes, `NULL` is returned. + **/ +@property (nonatomic, nullable, readonly, strong) DDLogFileInfo *currentLogFileInfo; + +@end + + + + + +NS_ASSUME_NONNULL_END diff --git a/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDFileLogger.m b/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDFileLogger.m new file mode 100644 index 0000000..7aa8336 --- /dev/null +++ b/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDFileLogger.m @@ -0,0 +1,899 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2023, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software in source and binary forms, +// with or without modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Neither the name of Deusty nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific +// prior written permission of Deusty, LLC. + +#if !__has_feature(objc_arc) +#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#import + +#import "DDFileLogger.h" +#import "DDTTYLogger.h" +#import +// We probably shouldn't be using DDLog() statements within the DDLog implementation. +// But we still want to leave our log statements for any future debugging, +// and to allow other developers to trace the implementation (which is a great learning tool). +// +// So we use primitive logging macros around NSLog. +// We maintain the NS prefix on the macros to be explicit about the fact that we're using NSLog. + + + +unsigned long long const kDDDefaultLogMaxFileSize = 1024 * 1024 * 5; // 5 MB +NSTimeInterval const kDDDefaultLogRollingFrequency = 60 * 60 * 24 * 15; // 24 * 15 Hours +NSUInteger const kDDDefaultLogMaxNumLogFiles = 5; // 5 Files +unsigned long long const kDDDefaultLogFilesDiskQuota = 20 * 1024 * 1024; // 20 MB + +NSTimeInterval const kDDRollingLeeway = 1.0; // 1s + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface DDFileLogger () { + id _logFileManager; + + DDLogFileInfo *_currentLogFileInfo; + NSFileHandle *_currentLogFileHandle; + + dispatch_source_t _currentLogFileVnode; + + NSTimeInterval _rollingFrequency; + dispatch_source_t _rollingTimer; + + unsigned long long _maximumFileSize; + + dispatch_queue_t _completionQueue; +} + +@end + + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wincomplete-implementation" +@implementation DDFileLogger +#pragma clang diagnostic pop + + + +- (instancetype)init { + DDLogFileManagerDefault *defaultLogFileManager = [[DDLogFileManagerDefault alloc] init]; + return [self initWithLogFileManager:defaultLogFileManager logfileFormatter:nil completionQueue:nil]; +} + +- (instancetype)initWithLogFileManager:(id)logFileManager logfileFormatter:(nullable DDLogFileFormatterDefault *)logfileFormatter{ + return [self initWithLogFileManager:logFileManager logfileFormatter:logfileFormatter completionQueue:nil]; +} + +- (instancetype)initWithLogFileManager:(id )aLogFileManager logfileFormatter:(nullable DDLogFileFormatterDefault *)logfileFormatter completionQueue:(nullable dispatch_queue_t)dispatchQueue { + if ((self = [super initWithTag:aLogFileManager.directoryName])) { + _completionQueue = dispatchQueue ?: dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + + _maximumFileSize = kDDDefaultLogMaxFileSize; + _rollingFrequency = kDDDefaultLogRollingFrequency; + _automaticallyAppendNewlineForCustomFormatters = YES; + + _logFileManager = aLogFileManager; + _logFormatter = logfileFormatter; + self.loggerTag = aLogFileManager.directoryName; + } + + return self; +} + +- (void)lt_cleanup { + NSAssert([self isOnInternalLoggerQueue], @"lt_ methods should be on logger queue."); + + if (_currentLogFileHandle != nil) { + if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)) { + __autoreleasing NSError *error = nil; + BOOL success = [_currentLogFileHandle synchronizeAndReturnError:&error]; + if (!success) { + NSLogError(@"DDFileLogger: Failed to synchronize file: %@", error); + } + success = [_currentLogFileHandle closeAndReturnError:&error]; + if (!success) { + NSLogError(@"DDFileLogger: Failed to close file: %@", error); + } + } else { + @try { + [_currentLogFileHandle synchronizeFile]; + } @catch (NSException *exception) { + NSLogError(@"DDFileLogger: Failed to synchronize file: %@", exception); + } + [_currentLogFileHandle closeFile]; + } + _currentLogFileHandle = nil; + } + + if (_currentLogFileVnode) { + dispatch_source_cancel(_currentLogFileVnode); + _currentLogFileVnode = NULL; + } + + if (_rollingTimer) { + dispatch_source_cancel(_rollingTimer); + _rollingTimer = NULL; + } +} + +- (void)dealloc { + if (self.isOnInternalLoggerQueue) { + [self lt_cleanup]; + } else { + dispatch_sync(self.loggerQueue, ^{ + [self lt_cleanup]; + }); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (unsigned long long)maximumFileSize { + __block unsigned long long result; + + dispatch_block_t block = ^{ + result = self->_maximumFileSize; + }; + + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the maximumFileSize variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(self.loggerQueue, block); + }); + + return result; +} + +- (void)setMaximumFileSize:(unsigned long long)newMaximumFileSize { + dispatch_block_t block = ^{ + @autoreleasepool { + self->_maximumFileSize = newMaximumFileSize; + if (self->_currentLogFileHandle != nil || [self lt_currentLogFileHandle] != nil) { + [self lt_maybeRollLogFileDueToSize]; + } + } + }; + + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the maximumFileSize variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(self.loggerQueue, block); + }); +} + +- (NSTimeInterval)rollingFrequency { + __block NSTimeInterval result; + + dispatch_block_t block = ^{ + result = self->_rollingFrequency; + }; + + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation should access the rollingFrequency variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(self.loggerQueue, block); + }); + + return result; +} + +- (void)setRollingFrequency:(NSTimeInterval)newRollingFrequency { + dispatch_block_t block = ^{ + @autoreleasepool { + self->_rollingFrequency = newRollingFrequency; + if (self->_currentLogFileHandle != nil || [self lt_currentLogFileHandle] != nil) { + [self lt_maybeRollLogFileDueToAge]; + } + } + }; + + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation should access the rollingFrequency variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(self.loggerQueue, block); + }); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark File Rolling +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)lt_scheduleTimerToRollLogFileDueToAge { + NSAssert([self isOnInternalLoggerQueue], @"lt_ methods should be on logger queue."); + + if (_rollingTimer) { + dispatch_source_cancel(_rollingTimer); + _rollingTimer = NULL; + } + + if (_currentLogFileInfo == nil || _rollingFrequency <= 0.0) { + return; + } + + NSDate *logFileCreationDate = [_currentLogFileInfo creationDate]; + NSTimeInterval frequency = MIN(_rollingFrequency, DBL_MAX - [logFileCreationDate timeIntervalSinceReferenceDate]); + NSDate *logFileRollingDate = [logFileCreationDate dateByAddingTimeInterval:frequency]; + + NSLogVerbose(@"DDFileLogger: scheduleTimerToRollLogFileDueToAge"); + NSLogVerbose(@"DDFileLogger: logFileCreationDate : %@", logFileCreationDate); + NSLogVerbose(@"DDFileLogger: actual rollingFrequency: %f", frequency); + NSLogVerbose(@"DDFileLogger: logFileRollingDate : %@", logFileRollingDate); + + _rollingTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _loggerQueue); + + __weak __auto_type weakSelf = self; + dispatch_source_set_event_handler(_rollingTimer, ^{ @autoreleasepool { + [weakSelf lt_maybeRollLogFileDueToAge]; + } }); + +#if !OS_OBJECT_USE_OBJC + dispatch_source_t theRollingTimer = _rollingTimer; + dispatch_source_set_cancel_handler(_rollingTimer, ^{ + dispatch_release(theRollingTimer); + }); +#endif + + static NSTimeInterval const kDDMaxTimerDelay = LLONG_MAX / NSEC_PER_SEC; + int64_t delay = (int64_t)(MIN([logFileRollingDate timeIntervalSinceNow], kDDMaxTimerDelay) * (NSTimeInterval) NSEC_PER_SEC); + __auto_type fireTime = dispatch_walltime(NULL, delay); // `NULL` uses `gettimeofday` internally + + dispatch_source_set_timer(_rollingTimer, fireTime, DISPATCH_TIME_FOREVER, (uint64_t)kDDRollingLeeway * NSEC_PER_SEC); + dispatch_activate(_rollingTimer); +} + +- (void)rollLogFile { + [self rollLogFileWithCompletionBlock:nil]; +} + +- (void)rollLogFileWithCompletionBlock:(nullable void (^)(void))completionBlock { + // This method is public. + // We need to execute the rolling on our logging thread/queue. + + dispatch_block_t block = ^{ + @autoreleasepool { + [self lt_rollLogFileNow]; + + if (completionBlock) { + dispatch_async(self->_completionQueue, ^{ + completionBlock(); + }); + } + } + }; + + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) { + block(); + } else { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(self.loggerQueue, block); + }); + } +} + +- (void)lt_rollLogFileNow { + NSAssert([self isOnInternalLoggerQueue], @"lt_ methods should be on logger queue."); + NSLogVerbose(@"DDFileLogger: rollLogFileNow"); + + if (_currentLogFileHandle == nil) { + return; + } + + if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)) { + __autoreleasing NSError *error = nil; + BOOL success = [_currentLogFileHandle synchronizeAndReturnError:&error]; + if (!success) { + NSLogError(@"DDFileLogger: Failed to synchronize file: %@", error); + } + success = [_currentLogFileHandle closeAndReturnError:&error]; + if (!success) { + NSLogError(@"DDFileLogger: Failed to close file: %@", error); + } + } else { + @try { + [_currentLogFileHandle synchronizeFile]; + } @catch (NSException *exception) { + NSLogError(@"DDFileLogger: Failed to synchronize file: %@", exception); + } + [_currentLogFileHandle closeFile]; + } + _currentLogFileHandle = nil; + + _currentLogFileInfo.isArchived = YES; + + const BOOL logFileManagerRespondsToNewArchiveSelector = [_logFileManager respondsToSelector:@selector(didArchiveLogFile:wasRolled:)]; + const BOOL logFileManagerRespondsToSelector = (logFileManagerRespondsToNewArchiveSelector + || [_logFileManager respondsToSelector:@selector(didRollAndArchiveLogFile:)]); + NSString *archivedFilePath = (logFileManagerRespondsToSelector) ? [_currentLogFileInfo.filePath copy] : nil; + _currentLogFileInfo = nil; + + if (logFileManagerRespondsToSelector) { + dispatch_block_t block; + if (logFileManagerRespondsToNewArchiveSelector) { + block = ^{ + [self->_logFileManager didArchiveLogFile:archivedFilePath wasRolled:YES]; + }; + } else { + block = ^{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [self->_logFileManager didRollAndArchiveLogFile:archivedFilePath]; +#pragma clang diagnostic pop + }; + } + dispatch_async(_completionQueue, block); + } + + if (_currentLogFileVnode) { + dispatch_source_cancel(_currentLogFileVnode); + _currentLogFileVnode = nil; + } + + if (_rollingTimer) { + dispatch_source_cancel(_rollingTimer); + _rollingTimer = nil; + } +} + +- (void)lt_maybeRollLogFileDueToAge { + NSAssert([self isOnInternalLoggerQueue], @"lt_ methods should be on logger queue."); + + if (_rollingFrequency > 0.0 && (_currentLogFileInfo.age + kDDRollingLeeway) >= _rollingFrequency) { + NSLogVerbose(@"DDFileLogger: Rolling log file due to age..."); + [self lt_rollLogFileNow]; + } else { + [self lt_scheduleTimerToRollLogFileDueToAge]; + } +} + +- (void)lt_maybeRollLogFileDueToSize { + NSAssert([self isOnInternalLoggerQueue], @"lt_ methods should be on logger queue."); + + // This method is called from logMessage. + // Keep it FAST. + + // Note: Use direct access to maximumFileSize variable. + // We specifically wrote our own getter/setter method to allow us to do this (for performance reasons). + + if (_currentLogFileHandle != nil && _maximumFileSize > 0) { + unsigned long long fileSize; + if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)) { + __autoreleasing NSError *error = nil; + BOOL success = [_currentLogFileHandle getOffset:&fileSize error:&error]; + if (!success) { + NSLogError(@"DDFileLogger: Failed to get offset: %@", error); + return; + } + } else { + fileSize = [_currentLogFileHandle offsetInFile]; + } + + if (fileSize >= _maximumFileSize) { + NSLogVerbose(@"DDFileLogger: Rolling log file due to size (%qu)...", fileSize); + + [self lt_rollLogFileNow]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark File Logging +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)lt_shouldLogFileBeArchived:(DDLogFileInfo *)mostRecentLogFileInfo { + NSAssert([self isOnInternalLoggerQueue], @"lt_ methods should be on logger queue."); + + if ([self shouldArchiveRecentLogFileInfo:mostRecentLogFileInfo]) { + return YES; + } else if (_maximumFileSize > 0 && mostRecentLogFileInfo.fileSize >= _maximumFileSize) { + return YES; + } else if (_rollingFrequency > 0.0 && mostRecentLogFileInfo.age >= _rollingFrequency) { + return YES; + } + +#if TARGET_OS_IPHONE + // When creating log file on iOS we're setting NSFileProtectionKey attribute to NSFileProtectionCompleteUnlessOpen. + // + // But in case if app is able to launch from background we need to have an ability to open log file any time we + // want (even if device is locked). Thats why that attribute have to be changed to + // NSFileProtectionCompleteUntilFirstUserAuthentication. + // + // If previous log was created when app wasn't running in background, but now it is - we archive it and create + // a new one. + // + // If user has overwritten to NSFileProtectionNone there is no need to create a new one. + if (doesAppRunInBackground()) { + NSFileProtectionType key = mostRecentLogFileInfo.fileAttributes[NSFileProtectionKey]; + BOOL isUntilFirstAuth = [key isEqualToString:NSFileProtectionCompleteUntilFirstUserAuthentication]; + BOOL isNone = [key isEqualToString:NSFileProtectionNone]; + + if (key != nil && !isUntilFirstAuth && !isNone) { + return YES; + } + } +#endif + + return NO; +} + +/** + * Returns the log file that should be used. + * If there is an existing log file that is suitable, within the + * constraints of maximumFileSize and rollingFrequency, then it is returned. + * + * Otherwise a new file is created and returned. + **/ +- (DDLogFileInfo *)currentLogFileInfo { + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + // Do not access this method on any Lumberjack queue, will deadlock. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + __block DDLogFileInfo *info = nil; + dispatch_block_t block = ^{ + info = [self lt_currentLogFileInfo]; + }; + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(self->_loggerQueue, block); + }); + + return info; +} + +- (DDLogFileInfo *)lt_currentLogFileInfo { + NSAssert([self isOnInternalLoggerQueue], @"lt_ methods should be on logger queue."); + + // Get the current log file info ivar (might be nil). + DDLogFileInfo *newCurrentLogFile = _currentLogFileInfo; + + // Check if we're resuming and if so, get the first of the sorted log file infos. + BOOL isResuming = newCurrentLogFile == nil; + if (isResuming) { + NSArray *sortedLogFileInfos = [_logFileManager sortedLogFileInfos]; + newCurrentLogFile = sortedLogFileInfos.firstObject; + } + + // Check if the file we've found is still valid. Otherwise create a new one. + if (newCurrentLogFile != nil && [self lt_shouldUseLogFile:newCurrentLogFile isResuming:isResuming]) { + if (isResuming) { + NSLogVerbose(@"DDFileLogger: Resuming logging with file %@", newCurrentLogFile.fileName); + } + _currentLogFileInfo = newCurrentLogFile; + } else { + NSString *currentLogFilePath; + if ([_logFileManager respondsToSelector:@selector(createNewLogFileWithError:)]) { + __autoreleasing NSError *error; // Don't initialize error to nil since it will be done in -createNewLogFileWithError: + currentLogFilePath = [_logFileManager createNewLogFileWithError:&error]; + if (!currentLogFilePath) { + NSLogError(@"DDFileLogger: Failed to create new log file: %@", error); + } + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + NSAssert([_logFileManager respondsToSelector:@selector(createNewLogFile)], + @"Invalid log file manager! Responds neither to `-createNewLogFileWithError:` nor `-createNewLogFile`!"); + currentLogFilePath = [_logFileManager createNewLogFile]; +#pragma clang diagnostic pop + if (!currentLogFilePath) { + NSLogError(@"DDFileLogger: Failed to create new log file"); + } + } + // Use static factory method here, since it checks for nil (and is unavailable to Swift). + _currentLogFileInfo = [DDLogFileInfo logFileWithPath:currentLogFilePath]; + } + + return _currentLogFileInfo; +} + +- (BOOL)lt_shouldUseLogFile:(nonnull DDLogFileInfo *)logFileInfo isResuming:(BOOL)isResuming { + NSAssert([self isOnInternalLoggerQueue], @"lt_ methods should be on logger queue."); + NSParameterAssert(logFileInfo); + + // Check if the log file is archived. We must not use archived log files. + if (logFileInfo.isArchived) { + return NO; + } + + // Don't follow symlink + if (logFileInfo.isSymlink) { + return NO; + } + + // If we're resuming, we need to check if the log file is allowed for reuse or needs to be archived. + if (isResuming && (_doNotReuseLogFiles || [self lt_shouldLogFileBeArchived:logFileInfo])) { + logFileInfo.isArchived = YES; + + const BOOL logFileManagerRespondsToNewArchiveSelector = [_logFileManager respondsToSelector:@selector(didArchiveLogFile:wasRolled:)]; + if (logFileManagerRespondsToNewArchiveSelector || [_logFileManager respondsToSelector:@selector(didArchiveLogFile:)]) { + NSString *archivedFilePath = [logFileInfo.filePath copy]; + dispatch_block_t block; + if (logFileManagerRespondsToNewArchiveSelector) { + block = ^{ + [self->_logFileManager didArchiveLogFile:archivedFilePath wasRolled:NO]; + }; + } else { + block = ^{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [self->_logFileManager didArchiveLogFile:archivedFilePath]; +#pragma clang diagnostic pop + }; + } + dispatch_async(_completionQueue, block); + } + + return NO; + } + + // All checks have passed. It's valid. + return YES; +} + +- (void)lt_monitorCurrentLogFileForExternalChanges { + NSAssert([self isOnInternalLoggerQueue], @"lt_ methods should be on logger queue."); + NSAssert(_currentLogFileHandle, @"Can not monitor without handle."); + + _currentLogFileVnode = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, + (uintptr_t)[_currentLogFileHandle fileDescriptor], + DISPATCH_VNODE_DELETE | DISPATCH_VNODE_RENAME | DISPATCH_VNODE_REVOKE, + _loggerQueue); + + __weak __auto_type weakSelf = self; + dispatch_source_set_event_handler(_currentLogFileVnode, ^{ @autoreleasepool { + NSLogInfo(@"DDFileLogger: Current logfile was moved. Rolling it and creating a new one"); + [weakSelf lt_rollLogFileNow]; + } }); + +#if !OS_OBJECT_USE_OBJC + dispatch_source_t vnode = _currentLogFileVnode; + dispatch_source_set_cancel_handler(_currentLogFileVnode, ^{ + dispatch_release(vnode); + }); +#endif + + dispatch_activate(_currentLogFileVnode); +} + +- (NSFileHandle *)lt_currentLogFileHandle { + NSAssert([self isOnInternalLoggerQueue], @"lt_ methods should be on logger queue."); + + if (_currentLogFileHandle == nil) { + NSString *logFilePath = [[self lt_currentLogFileInfo] filePath]; + _currentLogFileHandle = [NSFileHandle fileHandleForWritingAtPath:logFilePath]; + if (_currentLogFileHandle != nil) { + if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)) { + __autoreleasing NSError *error = nil; + BOOL success = [_currentLogFileHandle seekToEndReturningOffset:nil error:&error]; + if (!success) { + NSLogError(@"DDFileLogger: Failed to seek to end of file: %@", error); + } + } else { + [_currentLogFileHandle seekToEndOfFile]; + } + + [self lt_scheduleTimerToRollLogFileDueToAge]; + [self lt_monitorCurrentLogFileForExternalChanges]; + } + } + + return _currentLogFileHandle; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark DDLogger Protocol +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + +- (void)logMessage:(DDLogMessage *)logMessage { + // Don't need to check for isOnInternalLoggerQueue, -lt_dataForMessage: will do it for us. + NSData *data = [self lt_dataForMessage:logMessage]; + + if (data.length == 0) { + return; + } + + [self lt_logData:data]; + + if(self.canEchoMessage == YES){ + [[DDTTYLogger sharedInstance] logMessage:logMessage]; + } +} + + +- (void)willLogMessage:(DDLogFileInfo *)logFileInfo {} + +- (void)didLogMessage:(DDLogFileInfo *)logFileInfo { + [self lt_maybeRollLogFileDueToSize]; +} + +- (BOOL)shouldArchiveRecentLogFileInfo:(__unused DDLogFileInfo *)recentLogFileInfo { + return NO; +} + +- (void)willRemoveLogger { + [self lt_rollLogFileNow]; +} + +- (void)flush { + // This method is public. + // We need to execute the rolling on our logging thread/queue. + + dispatch_block_t block = ^{ + @autoreleasepool { + [self lt_flush]; + } + }; + + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) { + block(); + } else { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(self.loggerQueue, block); + }); + } +} + +- (void)lt_flush { + NSAssert([self isOnInternalLoggerQueue], @"flush should only be executed on internal queue."); + + if (_currentLogFileHandle != nil) { + if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)) { + __autoreleasing NSError *error = nil; + BOOL success = [_currentLogFileHandle synchronizeAndReturnError:&error]; + if (!success) { + NSLogError(@"DDFileLogger: Failed to synchronize file: %@", error); + } + } else { + @try { + [_currentLogFileHandle synchronizeFile]; + } @catch (NSException *exception) { + NSLogError(@"DDFileLogger: Failed to synchronize file: %@", exception); + } + } + } +} + +static int exception_count = 0; + +- (void)logData:(NSData *)data { + // This method is public. + // We need to execute the rolling on our logging thread/queue. + + dispatch_block_t block = ^{ + @autoreleasepool { + [self lt_logData:data]; + } + }; + + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) { + block(); + } else { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(self.loggerQueue, block); + }); + } +} + +- (void)lt_deprecationCatchAll {} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { + if (aSelector == @selector(willLogMessage) || aSelector == @selector(didLogMessage)) { + // Ignore calls to deprecated methods. + return [self methodSignatureForSelector:@selector(lt_deprecationCatchAll)]; + } + + return [super methodSignatureForSelector:aSelector]; +} + +- (void)forwardInvocation:(NSInvocation *)anInvocation { + if (anInvocation.selector != @selector(lt_deprecationCatchAll)) { + [super forwardInvocation:anInvocation]; + } +} + +- (void)lt_logData:(NSData *)data { + static BOOL implementsDeprecatedWillLog = NO; + static BOOL implementsDeprecatedDidLog = NO; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + implementsDeprecatedWillLog = [self respondsToSelector:@selector(willLogMessage)]; + implementsDeprecatedDidLog = [self respondsToSelector:@selector(didLogMessage)]; + }); + + NSAssert([self isOnInternalLoggerQueue], @"logMessage should only be executed on internal queue."); + + if (data.length == 0) { + return; + } + + @try { + // Make sure that _currentLogFileInfo is initialised before being used. + NSFileHandle *handle = [self lt_currentLogFileHandle]; + + if (implementsDeprecatedWillLog) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [self willLogMessage]; +#pragma clang diagnostic pop + } else { + [self willLogMessage:_currentLogFileInfo]; + } + + if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)) { + __autoreleasing NSError *error = nil; + BOOL success = [handle seekToEndReturningOffset:nil error:&error]; + if (!success) { + NSLogError(@"DDFileLogger: Failed to seek to end of file: %@", error); + } + success = [handle writeData:data error:&error]; + if (!success) { + NSLogError(@"DDFileLogger: Failed to write data: %@", error); + } + } else { + [handle seekToEndOfFile]; + [handle writeData:data]; + } + + if (implementsDeprecatedDidLog) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [self didLogMessage]; +#pragma clang diagnostic pop + } else { + [self didLogMessage:_currentLogFileInfo]; + } + + } @catch (NSException *exception) { + exception_count++; + + if (exception_count <= 10) { + NSLogError(@"DDFileLogger.logMessage: %@", exception); + + if (exception_count == 10) { + NSLogError(@"DDFileLogger.logMessage: Too many exceptions -- will not log any more of them."); + } + } + } +} + +- (NSData *)lt_dataForMessage:(DDLogMessage *)logMessage { + NSAssert([self isOnInternalLoggerQueue], @"logMessage should only be executed on internal queue."); + + NSString *message = logMessage->_message; + BOOL isFormatted = NO; + + if (_logFormatter != nil) { + message = [_logFormatter formatLogMessage:logMessage]; + isFormatted = message != logMessage->_message; + } + + if (message.length == 0) { + return nil; + } + + BOOL shouldFormat = !isFormatted || _automaticallyAppendNewlineForCustomFormatters; + if (shouldFormat && ![message hasSuffix:@"\n"]) { + message = [message stringByAppendingString:@"\n"]; + } + + return [message dataUsingEncoding:NSUTF8StringEncoding]; +} + + +- (DDLoggerName)loggerName { + if(self.loggerTag.length > 0){ + return [NSString stringWithFormat:@"%@/%@",DDLoggerNameFile,self.loggerTag]; + } + return DDLoggerNameFile; +} + +BOOL doesAppRunInBackground(void) { + BOOL answer = NO; + + NSArray *backgroundModes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIBackgroundModes"]; + + for (NSString *mode in backgroundModes) { + if (mode.length > 0) { + answer = YES; + break; + } + } + + return answer; +} + +@end + + + diff --git a/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLog.h b/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLog.h new file mode 100644 index 0000000..455bb61 --- /dev/null +++ b/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLog.h @@ -0,0 +1,887 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2023, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software in source and binary forms, +// with or without modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Neither the name of Deusty nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific +// prior written permission of Deusty, LLC. + +#import + +// The Swift Package integration has no support for the legacy macros. +#if __has_include() + // Enable 1.9.x legacy macros if imported directly and it's not a swift package build. + #ifndef DD_LEGACY_MACROS + #define DD_LEGACY_MACROS 1 + #endif + // DD_LEGACY_MACROS is checked in the file itself + #import +#endif + +#ifndef DD_LEGACY_MESSAGE_TAG + #define DD_LEGACY_MESSAGE_TAG 1 +#endif + +// Names of loggers. +#import "DDLoggerNames.h" + +#if OS_OBJECT_USE_OBJC + #define DISPATCH_QUEUE_REFERENCE_TYPE strong +#else + #define DISPATCH_QUEUE_REFERENCE_TYPE assign +#endif + +@class DDLogMessage; +@class DDLoggerInformation; +@protocol DDLogger; +@protocol DDLogFormatter; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Define the standard options. + * + * We default to only 4 levels because it makes it easier for beginners + * to make the transition to a logging framework. + * + * More advanced users may choose to completely customize the levels (and level names) to suite their needs. + * For more information on this see the "Custom Log Levels" page: + * Documentation/CustomLogLevels.md + * + * Advanced users may also notice that we're using a bitmask. + * This is to allow for custom fine grained logging: + * Documentation/FineGrainedLogging.md + * + * -- Flags -- + * + * Typically you will use the LOG_LEVELS (see below), but the flags may be used directly in certain situations. + * For example, say you have a lot of warning log messages, and you wanted to disable them. + * However, you still needed to see your error and info log messages. + * You could accomplish that with the following: + * + * static const DDLogLevel ddLogLevel = DDLogFlagError | DDLogFlagInfo; + * + * When LOG_LEVEL_DEF is defined as ddLogLevel. + * + * Flags may also be consulted when writing custom log formatters, + * as the DDLogMessage class captures the individual flag that caused the log message to fire. + * + * -- Levels -- + * + * Log levels are simply the proper bitmask of the flags. + * + * -- Booleans -- + * + * The booleans may be used when your logging code involves more than one line. + * For example: + * + * if (LOG_VERBOSE) { + * for (id sprocket in sprockets) + * DDLogVerbose(@"sprocket: %@", [sprocket description]) + * } + * + * -- Async -- + * + * Defines the default asynchronous options. + * The default philosophy for asynchronous logging is very simple: + * + * Log messages with errors should be executed synchronously. + * After all, an error just occurred. The application could be unstable. + * + * All other log messages, such as debug output, are executed asynchronously. + * After all, if it wasn't an error, then it was just informational output, + * or something the application was easily able to recover from. + * + * -- Changes -- + * + * You are strongly discouraged from modifying this file. + * If you do, you make it more difficult on yourself to merge future bug fixes and improvements from the project. + * Instead, create your own MyLogging.h or ApplicationNameLogging.h or CompanyLogging.h + * + * For an example of customizing your logging experience, see the "Custom Log Levels" page: + * Documentation/CustomLogLevels.md + **/ + +/** + * Flags accompany each log. They are used together with levels to filter out logs. + */ +typedef NS_OPTIONS(NSUInteger, DDLogFlag){ + /** + * 0...00001 DDLogFlagError + */ + DDLogFlagError = (1 << 0), + + /** + * 0...00010 DDLogFlagWarning + */ + DDLogFlagWarning = (1 << 1), + + /** + * 0...00100 DDLogFlagInfo + */ + DDLogFlagInfo = (1 << 2), + + /** + * 0...01000 DDLogFlagDebug + */ + DDLogFlagDebug = (1 << 3), + + /** + * 0...10000 DDLogFlagVerbose + */ + DDLogFlagVerbose = (1 << 4) +}; + +/** + * Log levels are used to filter out logs. Used together with flags. + */ +typedef NS_ENUM(NSUInteger, DDLogLevel){ + /** + * No logs + */ + DDLogLevelOff = 0, + + /** + * Error logs only + */ + DDLogLevelError = (DDLogFlagError), + + /** + * Error and warning logs + */ + DDLogLevelWarning = (DDLogLevelError | DDLogFlagWarning), + + /** + * Error, warning and info logs + */ + DDLogLevelInfo = (DDLogLevelWarning | DDLogFlagInfo), + + /** + * Error, warning, info and debug logs + */ + DDLogLevelDebug = (DDLogLevelInfo | DDLogFlagDebug), + + /** + * Error, warning, info, debug and verbose logs + */ + DDLogLevelVerbose = (DDLogLevelDebug | DDLogFlagVerbose), + + /** + * All logs (1...11111) + */ + DDLogLevelAll = NSUIntegerMax +}; + +/** + * Extracts just the file name, no path or extension + * + * @param filePath input file path + * @param copy YES if we want the result to be copied + * + * @return the file name + */ +FOUNDATION_EXTERN NSString * __nullable DDExtractFileNameWithoutExtension(const char *filePath, BOOL copy); + +/** + * The THIS_FILE macro gives you an NSString of the file name. + * For simplicity and clarity, the file name does not include the full path or file extension. + * + * For example: DDLogWarn(@"%@: Unable to find thingy", THIS_FILE) -> @"MyViewController: Unable to find thingy" + **/ +#define THIS_FILE (DDExtractFileNameWithoutExtension(__FILE__, NO)) + +/** + * The THIS_METHOD macro gives you the name of the current objective-c method. + * + * For example: DDLogWarn(@"%@ - Requires non-nil strings", THIS_METHOD) -> @"setMake:model: requires non-nil strings" + * + * Note: This does NOT work in straight C functions (non objective-c). + * Instead you should use the predefined __FUNCTION__ macro. + **/ +#define THIS_METHOD NSStringFromSelector(_cmd) + +/** + * Makes a declaration "Sendable" in Swift (if supported by the compiler). + */ +#ifndef DD_SENDABLE +#ifdef __has_attribute +#if __has_attribute(swift_attr) +#define DD_SENDABLE __attribute__((swift_attr("@Sendable"))) +#endif +#endif +#endif +#ifndef DD_SENDABLE +#define DD_SENDABLE +#endif + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The main class, exposes all logging mechanisms, loggers, ... + * For most of the users, this class is hidden behind the logging functions like `DDLogInfo` + */ +DD_SENDABLE +@interface DDLog : NSObject + +/** + * Returns the singleton `DDLog`. + * The instance is used by `DDLog` class methods. + */ +@property (class, nonatomic, strong, readonly) DDLog *sharedInstance; + +/** + * Provides access to the underlying logging queue. + * This may be helpful to Logger classes for things like thread synchronization. + **/ +@property (class, nonatomic, DISPATCH_QUEUE_REFERENCE_TYPE, readonly) dispatch_queue_t loggingQueue; + +/** + * Logging Primitive. + * + * This method is used by the macros or logging functions. + * It is suggested you stick with the macros as they're easier to use. + * + * @param asynchronous YES if the logging is done async, NO if you want to force sync + * @param level the log level + * @param flag the log flag + * @param format the log format + */ ++ (void)log:(BOOL)asynchronous + level:(DDLogLevel)level + flag:(DDLogFlag)flag +destinationFile:(NSString *)fileName + format:(NSString *)format, ... NS_FORMAT_FUNCTION(5,6); + +/** + * Logging Primitive. + * + * This method is used by the macros or logging functions. + * It is suggested you stick with the macros as they're easier to use. + * + * @param asynchronous YES if the logging is done async, NO if you want to force sync + * @param level the log level + * @param flag the log flag + * @param format the log format + */ +- (void)log:(BOOL)asynchronous + level:(DDLogLevel)level + flag:(DDLogFlag)flag +destinationFile:(NSString *)fileName + format:(NSString *)format, ... NS_FORMAT_FUNCTION(5,6); + +/** + * Logging Primitive. + * + * This method can be used if you have a prepared va_list. + * Similar to `log:level:flag:context:file:function:line:tag:format:...` + * + * @param asynchronous YES if the logging is done async, NO if you want to force sync + * @param level the log level + * @param flag the log flag + * @param format the log format + * @param argList the arguments list as a va_list + */ ++ (void)log:(BOOL)asynchronous + level:(DDLogLevel)level + flag:(DDLogFlag)flag +destinationFile:(NSString *)fileName + format:(NSString *)format + args:(va_list)argList NS_SWIFT_NAME(log(asynchronous:level:destinationFile:format:arguments:)); + +/** + * Logging Primitive. + * + * This method can be used if you have a prepared va_list. + * Similar to `log:level:flag:context:file:function:line:tag:format:...` + * + * @param asynchronous YES if the logging is done async, NO if you want to force sync + * @param level the log level + * @param flag the log flag + * @param format the log format + * @param argList the arguments list as a va_list + */ +- (void)log:(BOOL)asynchronous + level:(DDLogLevel)level + flag:(DDLogFlag)flag +destinationFile:(NSString *)fileName + format:(NSString *)format + args:(va_list)argList NS_SWIFT_NAME(log(asynchronous:level:destinationFile:format:arguments:)); + +/** + * Logging Primitive. + * + * This method can be used if you manually prepared DDLogMessage. + * + * @param asynchronous YES if the logging is done async, NO if you want to force sync + * @param logMessage the log message stored in a `DDLogMessage` model object + */ ++ (void)log:(BOOL)asynchronous targetFile:(NSString *)targetFile logMessage:(DDLogMessage *)logMessage NS_SWIFT_NAME(log(asynchronous:targetFile:logMessage:)); + +/** + * Logging Primitive. + * + * This method can be used if you manually prepared DDLogMessage. + * + * @param asynchronous YES if the logging is done async, NO if you want to force sync + * @param logMessage the log message stored in a `DDLogMessage` model object + */ +- (void)log:(BOOL)asynchronous targetFile:(NSString *)targetFile logMessage:(DDLogMessage *)logMessage NS_SWIFT_NAME(log(asynchronous:targetFile:logMessage:)); + +/** + * Since logging can be asynchronous, there may be times when you want to flush the logs. + * The framework invokes this automatically when the application quits. + **/ ++ (void)flushLog; + +/** + * Since logging can be asynchronous, there may be times when you want to flush the logs. + * The framework invokes this automatically when the application quits. + **/ +- (void)flushLog; + +/** + * Loggers + * + * In order for your log statements to go somewhere, you should create and add a logger. + * + * You can add multiple loggers in order to direct your log statements to multiple places. + * And each logger can be configured separately. + * So you could have, for example, verbose logging to the console, but a concise log file with only warnings & errors. + **/ + +/** + * Adds the logger to the system. + * + * This is equivalent to invoking `[DDLog addLogger:logger withLogLevel:DDLogLevelAll]`. + **/ ++ (void)addLogger:(id )logger; + +/** + * Adds the logger to the system. + * + * This is equivalent to invoking `[DDLog addLogger:logger withLogLevel:DDLogLevelAll]`. + **/ +- (void)addLogger:(id )logger; + +/** + * Adds the logger to the system. + * + * The level that you provide here is a preemptive filter (for performance). + * That is, the level specified here will be used to filter out logMessages so that + * the logger is never even invoked for the messages. + * + * More information: + * When you issue a log statement, the logging framework iterates over each logger, + * and checks to see if it should forward the logMessage to the logger. + * This check is done using the level parameter passed to this method. + * + * For example: + * + * `[DDLog addLogger:consoleLogger withLogLevel:DDLogLevelVerbose];` + * `[DDLog addLogger:fileLogger withLogLevel:DDLogLevelWarning];` + * + * `DDLogError(@"oh no");` => gets forwarded to consoleLogger & fileLogger + * `DDLogInfo(@"hi");` => gets forwarded to consoleLogger only + * + * It is important to remember that Lumberjack uses a BITMASK. + * Many developers & third party frameworks may define extra log levels & flags. + * For example: + * + * `#define SOME_FRAMEWORK_LOG_FLAG_TRACE (1 << 6) // 0...1000000` + * + * So if you specify `DDLogLevelVerbose` to this method, you won't see the framework's trace messages. + * + * `(SOME_FRAMEWORK_LOG_FLAG_TRACE & DDLogLevelVerbose) => (01000000 & 00011111) => NO` + * + * Consider passing `DDLogLevelAll` to this method, which has all bits set. + * You can also use the exclusive-or bitwise operator to get a bitmask that has all flags set, + * except the ones you explicitly don't want. For example, if you wanted everything except verbose & debug: + * + * `((DDLogLevelAll ^ DDLogLevelVerbose) | DDLogLevelInfo)` + **/ ++ (void)addLogger:(id )logger withLevel:(DDLogLevel)level; + +/** + * Adds the logger to the system. + * + * The level that you provide here is a preemptive filter (for performance). + * That is, the level specified here will be used to filter out logMessages so that + * the logger is never even invoked for the messages. + * + * More information: + * When you issue a log statement, the logging framework iterates over each logger, + * and checks to see if it should forward the logMessage to the logger. + * This check is done using the level parameter passed to this method. + * + * For example: + * + * `[DDLog addLogger:consoleLogger withLogLevel:DDLogLevelVerbose];` + * `[DDLog addLogger:fileLogger withLogLevel:DDLogLevelWarning];` + * + * `DDLogError(@"oh no");` => gets forwarded to consoleLogger & fileLogger + * `DDLogInfo(@"hi");` => gets forwarded to consoleLogger only + * + * It is important to remember that Lumberjack uses a BITMASK. + * Many developers & third party frameworks may define extra log levels & flags. + * For example: + * + * `#define SOME_FRAMEWORK_LOG_FLAG_TRACE (1 << 6) // 0...1000000` + * + * So if you specify `DDLogLevelVerbose` to this method, you won't see the framework's trace messages. + * + * `(SOME_FRAMEWORK_LOG_FLAG_TRACE & DDLogLevelVerbose) => (01000000 & 00011111) => NO` + * + * Consider passing `DDLogLevelAll` to this method, which has all bits set. + * You can also use the exclusive-or bitwise operator to get a bitmask that has all flags set, + * except the ones you explicitly don't want. For example, if you wanted everything except verbose & debug: + * + * `((DDLogLevelAll ^ DDLogLevelVerbose) | DDLogLevelInfo)` + **/ +- (void)addLogger:(id )logger withLevel:(DDLogLevel)level; + +/** + * Remove the logger from the system + */ ++ (void)removeLogger:(id )logger; + +/** + * Remove the logger from the system + */ +- (void)removeLogger:(id )logger; + +/** + * Remove all the current loggers + */ ++ (void)removeAllLoggers; + +/** + * Remove all the current loggers + */ +- (void)removeAllLoggers; + +/** + * Return all the current loggers + */ +@property (class, nonatomic, copy, readonly) NSArray> *allLoggers; + +/** + * Return all the current loggers + */ +@property (nonatomic, copy, readonly) NSArray> *allLoggers; + +/** + * Return all the current loggers with their level (aka DDLoggerInformation). + */ +@property (class, nonatomic, copy, readonly) NSArray *allLoggersWithLevel; + +/** + * Return all the current loggers with their level (aka DDLoggerInformation). + */ +@property (nonatomic, copy, readonly) NSArray *allLoggersWithLevel; + +/** + * Registered Dynamic Logging + * + * These methods allow you to obtain a list of classes that are using registered dynamic logging, + * and also provides methods to get and set their log level during run time. + **/ + +/** + * Returns an array with the classes that are using registered dynamic logging + */ +@property (class, nonatomic, copy, readonly) NSArray *registeredClasses; + +/** + * Returns an array with the classes names that are using registered dynamic logging + */ +@property (class, nonatomic, copy, readonly) NSArray *registeredClassNames; + +/** + * Returns the current log level for a certain class + * + * @param aClass `Class` param + */ ++ (DDLogLevel)levelForClass:(Class)aClass; + +/** + * Returns the current log level for a certain class + * + * @param aClassName string param + */ ++ (DDLogLevel)levelForClassWithName:(NSString *)aClassName; + +/** + * Set the log level for a certain class + * + * @param level the new level + * @param aClass `Class` param + */ ++ (void)setLevel:(DDLogLevel)level forClass:(Class)aClass; + +/** + * Set the log level for a certain class + * + * @param level the new level + * @param aClassName string param + */ ++ (void)setLevel:(DDLogLevel)level forClassWithName:(NSString *)aClassName; + + ++ (NSString *)rootLogsDirectory; ++ (BOOL)getLogDirectoryWithFile:(NSString *)fileName; ++ (BOOL)removeLogFileWithName:(NSString *)fileName; +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This protocol describes a basic logger behavior. + * Basically, it can log messages, store a logFormatter plus a bunch of optional behaviors. + * (i.e. flush, get its loggerQueue, get its name, ... + */ +@protocol DDLogger + +/** + * The log message method + * + * @param logMessage the message (model) + */ +- (void)logMessage:(DDLogMessage *)logMessage NS_SWIFT_NAME(log(logMessage:)); + +/** + * Formatters may optionally be added to any logger. + * + * If no formatter is set, the logger simply logs the message as it is given in logMessage, + * or it may use its own built in formatting style. + **/ +@property (nonatomic, strong, nullable) id logFormatter; + +@property (nonatomic, strong) NSString *loggerTag; + +@optional + +/** + * Since logging is asynchronous, adding and removing loggers is also asynchronous. + * In other words, the loggers are added and removed at appropriate times with regards to log messages. + * + * - Loggers will not receive log messages that were executed prior to when they were added. + * - Loggers will not receive log messages that were executed after they were removed. + * + * These methods are executed in the logging thread/queue. + * This is the same thread/queue that will execute every logMessage: invocation. + * Loggers may use these methods for thread synchronization or other setup/teardown tasks. + **/ +- (void)didAddLogger; + +/** + * Since logging is asynchronous, adding and removing loggers is also asynchronous. + * In other words, the loggers are added and removed at appropriate times with regards to log messages. + * + * - Loggers will not receive log messages that were executed prior to when they were added. + * - Loggers will not receive log messages that were executed after they were removed. + * + * These methods are executed in the logging thread/queue given in parameter. + * This is the same thread/queue that will execute every logMessage: invocation. + * Loggers may use the queue parameter to set specific values on the queue with dispatch_set_specific() function. + **/ +- (void)didAddLoggerInQueue:(dispatch_queue_t)queue; + +/** + * See the above description for `didAddLogger` + */ +- (void)willRemoveLogger; + +/** + * Some loggers may buffer IO for optimization purposes. + * For example, a database logger may only save occasionally as the disk IO is slow. + * In such loggers, this method should be implemented to flush any pending IO. + * + * This allows invocations of DDLog's flushLog method to be propagated to loggers that need it. + * + * Note that DDLog's flushLog method is invoked automatically when the application quits, + * and it may be also invoked manually by the developer prior to application crashes, or other such reasons. + **/ +- (void)flush; + +/** + * Each logger is executed concurrently with respect to the other loggers. + * Thus, a dedicated dispatch queue is used for each logger. + * Logger implementations may optionally choose to provide their own dispatch queue. + **/ +@property (nonatomic, DISPATCH_QUEUE_REFERENCE_TYPE, readonly) dispatch_queue_t loggerQueue; + +/** + * If the logger implementation does not choose to provide its own queue, + * one will automatically be created for it. + * The created queue will receive its name from this method. + * This may be helpful for debugging or profiling reasons. + **/ +@property (copy, nonatomic, readonly) DDLoggerName loggerName; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This protocol describes the behavior of a log formatter + */ +@protocol DDLogFormatter +@required + +/** + * Formatters may optionally be added to any logger. + * This allows for increased flexibility in the logging environment. + * For example, log messages for log files may be formatted differently than log messages for the console. + * + * For more information about formatters, see the "Custom Formatters" page: + * Documentation/CustomFormatters.md + * + * The formatter may also optionally filter the log message by returning nil, + * in which case the logger will not log the message. + **/ +- (nullable NSString *)formatLogMessage:(DDLogMessage *)logMessage NS_SWIFT_NAME(format(message:)); + +@optional + +/** + * A single formatter instance can be added to multiple loggers. + * These methods provides hooks to notify the formatter of when it's added/removed. + * + * This is primarily for thread-safety. + * If a formatter is explicitly not thread-safe, it may wish to throw an exception if added to multiple loggers. + * Or if a formatter has potentially thread-unsafe code (e.g. NSDateFormatter with 10.0 behavior), + * it could possibly use these hooks to switch to thread-safe versions of the code. + **/ +- (void)didAddToLogger:(id )logger; + +/** + * A single formatter instance can be added to multiple loggers. + * These methods provides hooks to notify the formatter of when it's added/removed. + * + * This is primarily for thread-safety. + * If a formatter is explicitly not thread-safe, it may wish to throw an exception if added to multiple loggers. + * Or if a formatter has potentially thread-unsafe code (e.g. NSDateFormatter with 10.0 behavior), + * it could possibly use these hooks to switch to thread-safe versions of the code or use dispatch_set_specific() +.* to add its own specific values. + **/ +- (void)didAddToLogger:(id )logger inQueue:(dispatch_queue_t)queue; + +/** + * See the above description for `didAddToLogger:` + */ +- (void)willRemoveFromLogger:(id )logger; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This protocol describes a dynamic logging component + */ +@protocol DDRegisteredDynamicLogging + +/** + * Implement these methods to allow a file's log level to be managed from a central location. + * + * This is useful if you'd like to be able to change log levels for various parts + * of your code from within the running application. + * + * Imagine pulling up the settings for your application, + * and being able to configure the logging level on a per file basis. + * + * The implementation can be very straight-forward: + * + * ``` + * + (int)ddLogLevel + * { + * return ddLogLevel; + * } + * + * + (void)ddSetLogLevel:(DDLogLevel)level + * { + * ddLogLevel = level; + * } + * ``` + **/ +@property (class, nonatomic, readwrite, setter=ddSetLogLevel:) DDLogLevel ddLogLevel; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef NS_DESIGNATED_INITIALIZER + #define NS_DESIGNATED_INITIALIZER +#endif + +/** + * Log message options, allow copying certain log elements + */ +typedef NS_OPTIONS(NSInteger, DDLogMessageOptions){ + /** + * Use this to use a copy of the file path + */ + DDLogMessageCopyFile = 1 << 0, + /** + * Use this to use a copy of the function name + */ + DDLogMessageCopyFunction = 1 << 1, + /** + * Use this to use avoid a copy of the message + */ + DDLogMessageDontCopyMessage = 1 << 2 +}; + +/** + * The `DDLogMessage` class encapsulates information about the log message. + * If you write custom loggers or formatters, you will be dealing with objects of this class. + **/ +DD_SENDABLE +@interface DDLogMessage : NSObject +{ + // Direct accessors to be used only for performance + @public + NSString *_message; + DDLogLevel _level; + DDLogFlag _flag; + DDLogMessageOptions _options; + NSDate * _timestamp; + NSString *_threadID; + NSString *_threadName; + NSString *_queueLabel; + NSUInteger _qos; +} + +/** + * Default `init` for empty messages. + */ +- (instancetype)init NS_DESIGNATED_INITIALIZER; + +/** + * Standard init method for a log message object. + * Used by the logging primitives. (And the macros use the logging primitives.) + * + * If you find need to manually create logMessage objects, there is one thing you should be aware of: + * + * If no flags are passed, the method expects the file and function parameters to be string literals. + * That is, it expects the given strings to exist for the duration of the object's lifetime, + * and it expects the given strings to be immutable. + * In other words, it does not copy these strings, it simply points to them. + * This is due to the fact that __FILE__ and __FUNCTION__ are usually used to specify these parameters, + * so it makes sense to optimize and skip the unnecessary allocations. + * However, if you need them to be copied you may use the options parameter to specify this. + * + * @param message the message + * @param level the log level + * @param flag the log flag + * @param options a bitmask which supports DDLogMessageCopyFile and DDLogMessageCopyFunction. + * @param timestamp the log timestamp + * + * @return a new instance of a log message model object + */ +- (instancetype)initWithMessage:(NSString *)message + level:(DDLogLevel)level + flag:(DDLogFlag)flag + options:(DDLogMessageOptions)options + timestamp:(NSDate *)timestamp NS_DESIGNATED_INITIALIZER; + +/** + * Read-only properties + **/ + +/** + * The log message + */ +@property (readonly, nonatomic) NSString *message; +@property (readonly, nonatomic) DDLogLevel level; +@property (readonly, nonatomic) DDLogFlag flag; +@property (readonly, nonatomic) DDLogMessageOptions options; +@property (readonly, nonatomic) NSDate *timestamp; +@property (readonly, nonatomic) NSString *threadID; // ID as it appears in NSLog calculated from the machThreadID +@property (readonly, nonatomic, nullable) NSString *threadName; +@property (readonly, nonatomic) NSString *queueLabel; +@property (readonly, nonatomic) NSUInteger qos API_AVAILABLE(macos(10.10), ios(8.0)); + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The `DDLogger` protocol specifies that an optional formatter can be added to a logger. + * Most (but not all) loggers will want to support formatters. + * + * However, writing getters and setters in a thread safe manner, + * while still maintaining maximum speed for the logging process, is a difficult task. + * + * To do it right, the implementation of the getter/setter has strict requirements: + * - Must NOT require the `logMessage:` method to acquire a lock. + * - Must NOT require the `logMessage:` method to access an atomic property (also a lock of sorts). + * + * To simplify things, an abstract logger is provided that implements the getter and setter. + * + * Logger implementations may simply extend this class, + * and they can ACCESS THE FORMATTER VARIABLE DIRECTLY from within their `logMessage:` method! + **/ +@interface DDAbstractLogger : NSObject +{ + // Direct accessors to be used only for performance + @public + id _logFormatter; + dispatch_queue_t _loggerQueue; +} + +@property (nonatomic, strong, nullable) id logFormatter; +@property (nonatomic, strong) NSString *loggerTag; +@property (nonatomic, DISPATCH_QUEUE_REFERENCE_TYPE) dispatch_queue_t loggerQueue; +@property (nonatomic,assign) BOOL canEchoMessage; +// For thread-safety assertions + +/** + * Return YES if the current logger uses a global queue for logging + */ +@property (nonatomic, readonly, getter=isOnGlobalLoggingQueue) BOOL onGlobalLoggingQueue; + +/** + * Return YES if the current logger uses the internal designated queue for logging + */ +@property (nonatomic, readonly, getter=isOnInternalLoggerQueue) BOOL onInternalLoggerQueue; + +- (instancetype)initWithTag:(NSString *)tagName; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +DD_SENDABLE +@interface DDLoggerInformation : NSObject + +@property (nonatomic, readonly) id logger; +@property (nonatomic, readonly) DDLogLevel level; + ++ (instancetype)informationWithLogger:(id )logger + andLevel:(DDLogLevel)level; + +@end + +NS_ASSUME_NONNULL_END diff --git a/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLog.m b/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLog.m new file mode 100644 index 0000000..056c310 --- /dev/null +++ b/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLog.m @@ -0,0 +1,1274 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2023, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software in source and binary forms, +// with or without modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Neither the name of Deusty nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific +// prior written permission of Deusty, LLC. + +#if !__has_feature(objc_arc) +#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#import +#import +#import + +#if TARGET_OS_IOS + #import + #import +#elif !defined(DD_CLI) && __has_include() + #import +#endif + +// Disable legacy macros +#ifndef DD_LEGACY_MACROS + #define DD_LEGACY_MACROS 0 +#endif + +#import "DDLog.h" +#import "DDTTYLogger.h" +// We probably shouldn't be using DDLog() statements within the DDLog implementation. +// But we still want to leave our log statements for any future debugging, +// and to allow other developers to trace the implementation (which is a great learning tool). +// +// So we use a primitive logging macro around NSLog. +// We maintain the NS prefix on the macros to be explicit about the fact that we're using NSLog. + +#ifndef DD_DEBUG + #define DD_DEBUG 0 +#endif + +#define NSLogDebug(frmt, ...) do{ if(DD_DEBUG) NSLog((frmt), ##__VA_ARGS__); } while(0) + +// The "global logging queue" refers to [DDLog loggingQueue]. +// It is the queue that all log statements go through. +// +// The logging queue sets a flag via dispatch_queue_set_specific using this key. +// We can check for this key via dispatch_get_specific() to see if we're on the "global logging queue". + +static void *const GlobalLoggingQueueIdentityKey = (void *)&GlobalLoggingQueueIdentityKey; + +@interface DDLoggerNode : NSObject +{ + // Direct accessors to be used only for performance + @public + id _logger; + DDLogLevel _level; + dispatch_queue_t _loggerQueue; +} + +@property (nonatomic, readonly) id logger; +@property (nonatomic, readonly) DDLogLevel level; +@property (nonatomic, readonly) dispatch_queue_t loggerQueue; + ++ (instancetype)nodeWithLogger:(id )logger + loggerQueue:(dispatch_queue_t)loggerQueue + level:(DDLogLevel)level; + +@end + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface DDLog () + +// An array used to manage all the individual loggers. +// The array is only modified on the loggingQueue/loggingThread. +@property (nonatomic, strong) NSMutableArray *_loggers; + +@end + +@implementation DDLog + +// All logging statements are added to the same queue to ensure FIFO operation. +static dispatch_queue_t _loggingQueue; + +// Individual loggers are executed concurrently per log statement. +// Each logger has it's own associated queue, and a dispatch group is used for synchronization. +static dispatch_group_t _loggingGroup; + +// Minor optimization for uniprocessor machines +static NSUInteger _numProcessors; + +/** + * Returns the singleton `DDLog`. + * The instance is used by `DDLog` class methods. + * + * @return The singleton `DDLog`. + */ ++ (instancetype)sharedInstance { + static id sharedInstance = nil; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] init]; + }); + + return sharedInstance; +} + +/** + * The runtime sends initialize to each class in a program exactly one time just before the class, + * or any class that inherits from it, is sent its first message from within the program. (Thus the + * method may never be invoked if the class is not used.) The runtime sends the initialize message to + * classes in a thread-safe manner. Superclasses receive this message before their subclasses. + * + * This method may also be called directly, hence the safety mechanism. + **/ ++ (void)initialize { + static dispatch_once_t DDLogOnceToken; + + dispatch_once(&DDLogOnceToken, ^{ + NSLogDebug(@"DDLog: Using grand central dispatch"); + + _loggingQueue = dispatch_queue_create("cocoa.lumberjack", NULL); + _loggingGroup = dispatch_group_create(); + + void *nonNullValue = GlobalLoggingQueueIdentityKey; // Whatever, just not null + dispatch_queue_set_specific(_loggingQueue, GlobalLoggingQueueIdentityKey, nonNullValue, NULL); + + // Figure out how many processors are available. + // This may be used later for an optimization on uniprocessor machines. + + _numProcessors = MAX([NSProcessInfo processInfo].processorCount, (NSUInteger) 1); + + NSLogDebug(@"DDLog: numProcessors = %@", @(_numProcessors)); + }); +} + ++ (BOOL)getLogDirectoryWithFile:(NSString *)fileName{ + NSString *rootDir = [DDLog rootLogsDirectory]; + BOOL isDir = YES; + NSString *path = [rootDir stringByAppendingPathComponent:fileName]; + BOOL exit = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]; + return exit; +} + ++ (BOOL)removeLogFileWithName:(NSString *)fileName{ + if([DDLog getLogDirectoryWithFile:fileName] == YES){ + NSString *rootDir = [DDLog rootLogsDirectory]; + NSString *path = [rootDir stringByAppendingPathComponent:fileName]; + [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; + return YES; + } + return NO; +} +/** + * The `DDLog` initializer. + * Static variables are set only once. + * + * @return An initialized `DDLog` instance. + */ +- (instancetype)init { + self = [super init]; + + if (self) { + self._loggers = [[NSMutableArray alloc] init]; + +#if TARGET_OS_IOS + NSString *notificationName = UIApplicationWillTerminateNotification; +#else + NSString *notificationName = nil; + + // On Command Line Tool apps AppKit may not be available +#if !defined(DD_CLI) && __has_include() + if (NSApp) { + notificationName = NSApplicationWillTerminateNotification; + } +#endif + + if (!notificationName) { + // If there is no NSApp -> we are running Command Line Tool app. + // In this case terminate notification wouldn't be fired, so we use workaround. + __weak __auto_type weakSelf = self; + atexit_b (^{ + [weakSelf applicationWillTerminate:nil]; + }); + } + +#endif /* if TARGET_OS_IOS */ + + if (notificationName) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationWillTerminate:) + name:notificationName + object:nil]; + } + } + + return self; +} + +/** + * Provides access to the logging queue. + **/ ++ (dispatch_queue_t)loggingQueue { + return _loggingQueue; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Notifications +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)applicationWillTerminate:(NSNotification * __attribute__((unused)))notification { + [self flushLog]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Logger Management +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (void)addLogger:(id )logger { + [self.sharedInstance addLogger:logger]; +} + +- (void)addLogger:(id )logger { + [self addLogger:logger withLevel:DDLogLevelAll]; // DDLogLevelAll has all bits set +} + ++ (void)addLogger:(id )logger withLevel:(DDLogLevel)level { + [self.sharedInstance addLogger:logger withLevel:level]; +} + +- (void)addLogger:(id )logger withLevel:(DDLogLevel)level { + if (!logger) { + return; + } + + dispatch_async(_loggingQueue, ^{ @autoreleasepool { + [self lt_addLogger:logger level:level]; + } }); +} + ++ (void)removeLogger:(id )logger { + [self.sharedInstance removeLogger:logger]; +} + +- (void)removeLogger:(id )logger { + if (!logger) { + return; + } + + dispatch_async(_loggingQueue, ^{ @autoreleasepool { + [self lt_removeLogger:logger]; + } }); +} + ++ (void)removeAllLoggers { + [self.sharedInstance removeAllLoggers]; +} + +- (void)removeAllLoggers { + dispatch_async(_loggingQueue, ^{ @autoreleasepool { + [self lt_removeAllLoggers]; + } }); +} + ++ (NSArray> *)allLoggers { + return [self.sharedInstance allLoggers]; +} + +- (NSArray> *)allLoggers { + __block NSArray *theLoggers; + + dispatch_sync(_loggingQueue, ^{ @autoreleasepool { + theLoggers = [self lt_allLoggers]; + } }); + + return theLoggers; +} + ++ (NSArray *)allLoggersWithLevel { + return [self.sharedInstance allLoggersWithLevel]; +} + +- (NSArray *)allLoggersWithLevel { + __block NSArray *theLoggersWithLevel; + + dispatch_sync(_loggingQueue, ^{ @autoreleasepool { + theLoggersWithLevel = [self lt_allLoggersWithLevel]; + } }); + + return theLoggersWithLevel; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - Master Logging +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)queueLogMessageToFile:(NSString *)fileName withLogMessage:(DDLogMessage *)logMessage asynchronously:(BOOL)asyncFlag { + // We have a tricky situation here... + // + // In the common case, when the queueSize is below the maximumQueueSize, + // we want to simply enqueue the logMessage. And we want to do this as fast as possible, + // which means we don't want to block and we don't want to use any locks. + // + // However, if the queueSize gets too big, we want to block. + // But we have very strict requirements as to when we block, and how long we block. + // + // The following example should help illustrate our requirements: + // + // Imagine that the maximum queue size is configured to be 5, + // and that there are already 5 log messages queued. + // Let us call these 5 queued log messages A, B, C, D, and E. (A is next to be executed) + // + // Now if our thread issues a log statement (let us call the log message F), + // it should block before the message is added to the queue. + // Furthermore, it should be unblocked immediately after A has been unqueued. + // + // The requirements are strict in this manner so that we block only as long as necessary, + // and so that blocked threads are unblocked in the order in which they were blocked. + // + // Returning to our previous example, let us assume that log messages A through E are still queued. + // Our aforementioned thread is blocked attempting to queue log message F. + // Now assume we have another separate thread that attempts to issue log message G. + // It should block until log messages A and B have been unqueued. + + dispatch_block_t logBlock = ^{ + // We're now sure we won't overflow the queue. + // It is time to queue our log message. + @autoreleasepool { + [self lt_logMessageToFile:fileName withMessage:logMessage]; + } + }; + + if (asyncFlag) { + dispatch_async(_loggingQueue, logBlock); + } else if (dispatch_get_specific(GlobalLoggingQueueIdentityKey)) { + // We've logged an error message while on the logging queue... + logBlock(); + } else { + dispatch_sync(_loggingQueue, logBlock); + } +} + ++ (void)log:(BOOL)asynchronous + level:(DDLogLevel)level + flag:(DDLogFlag)flag +destinationFile:(NSString *)fileName + format:(NSString *)format, ... { + va_list args; + + if (format) { + va_start(args, format); + + NSString *message = [[NSString alloc] initWithFormat:format arguments:args]; + + va_end(args); + + va_start(args, format); + + [self log:asynchronous + destinationFile:fileName + message:message + level:level flag:flag + ]; + + va_end(args); + } +} + +- (void)log:(BOOL)asynchronous + level:(DDLogLevel)level + flag:(DDLogFlag)flag +destinationFile:(NSString *)fileName + format:(NSString *)format, ... { + va_list args; + + if (format) { + va_start(args, format); + + NSString *message = [[NSString alloc] initWithFormat:format arguments:args]; + + va_end(args); + + va_start(args, format); + + [self log:asynchronous + destinationFile:fileName + message:message + level:level + flag:flag + ]; + + va_end(args); + } +} + ++ (void)log:(BOOL)asynchronous + level:(DDLogLevel)level + flag:(DDLogFlag)flag +destinationFile:(NSString *)fileName + format:(NSString *)format + args:(va_list)args { + [self.sharedInstance log:asynchronous level:level flag:flag destinationFile:fileName format:format args:args]; +} + +- (void)log:(BOOL)asynchronous + level:(DDLogLevel)level + flag:(DDLogFlag)flag +destinationFile:(NSString *)fileName + format:(NSString *)format + args:(va_list)args { + if (format) { + NSString *message = [[NSString alloc] initWithFormat:format arguments:args]; + [self log:asynchronous + destinationFile:fileName + message:message + level:level flag:flag]; + } +} + ++ (void)log:(BOOL)asynchronous +destinationFile:(NSString *)destinationFileName + message:(NSString *)message + level:(DDLogLevel)level + flag:(DDLogFlag)flag{ + [self.sharedInstance log:asynchronous destinationFile:destinationFileName message:message level:level flag:flag]; +} + +- (void)log:(BOOL)asynchronous +destinationFile:(NSString *)destinationFileName + message:(NSString *)message + level:(DDLogLevel)level + flag:(DDLogFlag)flag{ + +// Nullity checks are handled by -initWithMessage: +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnullable-to-nonnull-conversion" + DDLogMessage *logMessage = [[DDLogMessage alloc] initWithMessage:message + level:level + flag:flag + options:(DDLogMessageOptions)0 + timestamp:nil]; +#pragma clang diagnostic pop + + [self queueLogMessageToFile:destinationFileName withLogMessage:logMessage asynchronously:asynchronous]; +} + ++ (void)log:(BOOL)asynchronous targetFile:(NSString *)targetFile logMessage:(DDLogMessage *)logMessage { + [self.sharedInstance log:asynchronous targetFile:targetFile logMessage:logMessage]; +} + +- (void)log:(BOOL)asynchronous targetFile:(NSString *)targetFile logMessage:(DDLogMessage *)logMessage { + [self queueLogMessageToFile:targetFile withLogMessage:logMessage asynchronously:asynchronous]; +} + ++ (void)flushLog { + [self.sharedInstance flushLog]; +} + +- (void)flushLog { + NSAssert(!dispatch_get_specific(GlobalLoggingQueueIdentityKey), + @"This method shouldn't be run on the logging thread/queue that make flush fast enough"); + + dispatch_sync(_loggingQueue, ^{ @autoreleasepool { + [self lt_flush]; + } }); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Registered Dynamic Logging +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (BOOL)isRegisteredClass:(Class)class { + SEL getterSel = @selector(ddLogLevel); + SEL setterSel = @selector(ddSetLogLevel:); + +#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR + + // Issue #6 (GoogleCode) - Crashes on iOS 4.2.1 and iPhone 4 + // + // Crash caused by class_getClassMethod(2). + // + // "It's a bug with UIAccessibilitySafeCategory__NSObject so it didn't pop up until + // users had VoiceOver enabled [...]. I was able to work around it by searching the + // result of class_copyMethodList() instead of calling class_getClassMethod()" + + BOOL result = NO; + + unsigned int methodCount, i; + Method *methodList = class_copyMethodList(object_getClass(class), &methodCount); + + if (methodList != NULL) { + BOOL getterFound = NO; + BOOL setterFound = NO; + + for (i = 0; i < methodCount; ++i) { + SEL currentSel = method_getName(methodList[i]); + + if (currentSel == getterSel) { + getterFound = YES; + } else if (currentSel == setterSel) { + setterFound = YES; + } + + if (getterFound && setterFound) { + result = YES; + break; + } + } + + free(methodList); + } + + return result; + +#else /* if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR */ + + // Issue #24 (GitHub) - Crashing in in ARC+Simulator + // + // The method +[DDLog isRegisteredClass] will crash a project when using it with ARC + Simulator. + // For running in the Simulator, it needs to execute the non-iOS code. + + Method getter = class_getClassMethod(class, getterSel); + Method setter = class_getClassMethod(class, setterSel); + + if ((getter != NULL) && (setter != NULL)) { + return YES; + } + + return NO; + +#endif /* if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR */ +} + ++ (NSArray *)registeredClasses { + + // We're going to get the list of all registered classes. + // The Objective-C runtime library automatically registers all the classes defined in your source code. + // + // To do this we use the following method (documented in the Objective-C Runtime Reference): + // + // int objc_getClassList(Class *buffer, int bufferLen) + // + // We can pass (NULL, 0) to obtain the total number of + // registered class definitions without actually retrieving any class definitions. + // This allows us to allocate the minimum amount of memory needed for the application. + + NSUInteger numClasses = 0; + Class *classes = NULL; + + while (numClasses == 0) { + + numClasses = (NSUInteger)MAX(objc_getClassList(NULL, 0), 0); + + // numClasses now tells us how many classes we have (but it might change) + // So we can allocate our buffer, and get pointers to all the class definitions. + + NSUInteger bufferSize = numClasses; + + classes = numClasses ? (Class *)calloc(bufferSize, sizeof(Class)) : NULL; + if (classes == NULL) { + return @[]; //no memory or classes? + } + + numClasses = (NSUInteger)MAX(objc_getClassList(classes, (int)bufferSize),0); + + if (numClasses > bufferSize || numClasses == 0) { + //apparently more classes added between calls (or a problem); try again + free(classes); + classes = NULL; + numClasses = 0; + } + } + + // We can now loop through the classes, and test each one to see if it is a DDLogging class. + + NSMutableArray *result = [NSMutableArray arrayWithCapacity:numClasses]; + + for (NSUInteger i = 0; i < numClasses; i++) { + Class class = classes[i]; + + if ([self isRegisteredClass:class]) { + [result addObject:class]; + } + } + + free(classes); + + return result; +} + ++ (NSArray *)registeredClassNames { + NSArray *registeredClasses = [self registeredClasses]; + NSMutableArray *result = [NSMutableArray arrayWithCapacity:[registeredClasses count]]; + + for (Class class in registeredClasses) { + [result addObject:NSStringFromClass(class)]; + } + return result; +} + ++ (DDLogLevel)levelForClass:(Class)aClass { + if ([self isRegisteredClass:aClass]) { + return [aClass ddLogLevel]; + } + return (DDLogLevel)-1; +} + ++ (DDLogLevel)levelForClassWithName:(NSString *)aClassName { + Class aClass = NSClassFromString(aClassName); + + return [self levelForClass:aClass]; +} + ++ (void)setLevel:(DDLogLevel)level forClass:(Class)aClass { + if ([self isRegisteredClass:aClass]) { + [aClass ddSetLogLevel:level]; + } +} + ++ (void)setLevel:(DDLogLevel)level forClassWithName:(NSString *)aClassName { + Class aClass = NSClassFromString(aClassName); + [self setLevel:level forClass:aClass]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Logging Thread +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)lt_addLogger:(id )logger level:(DDLogLevel)level { + // Add to loggers array. + // Need to create loggerQueue if loggerNode doesn't provide one. + + for (DDLoggerNode *node in self._loggers) { + if (node->_logger == logger + && node->_level == level) { + // Exactly same logger already added, exit + return; + } + } + + NSAssert(dispatch_get_specific(GlobalLoggingQueueIdentityKey), + @"This method should only be run on the logging thread/queue"); + + dispatch_queue_t loggerQueue = NULL; + if ([logger respondsToSelector:@selector(loggerQueue)]) { + // Logger may be providing its own queue + loggerQueue = logger.loggerQueue; + } + + if (loggerQueue == nil) { + // Automatically create queue for the logger. + // Use the logger name as the queue name if possible. + const char *loggerQueueName = NULL; + + if ([logger respondsToSelector:@selector(loggerName)]) { + loggerQueueName = logger.loggerName.UTF8String; + } + + loggerQueue = dispatch_queue_create(loggerQueueName, NULL); + } + + DDLoggerNode *loggerNode = [DDLoggerNode nodeWithLogger:logger loggerQueue:loggerQueue level:level]; + [self._loggers addObject:loggerNode]; + + if ([logger respondsToSelector:@selector(didAddLoggerInQueue:)]) { + dispatch_async(loggerNode->_loggerQueue, ^{ @autoreleasepool { + [logger didAddLoggerInQueue:loggerNode->_loggerQueue]; + } }); + } else if ([logger respondsToSelector:@selector(didAddLogger)]) { + dispatch_async(loggerNode->_loggerQueue, ^{ @autoreleasepool { + [logger didAddLogger]; + } }); + } +} + +- (void)lt_removeLogger:(id )logger { + // Find associated loggerNode in list of added loggers + + NSAssert(dispatch_get_specific(GlobalLoggingQueueIdentityKey), + @"This method should only be run on the logging thread/queue"); + + DDLoggerNode *loggerNode = nil; + + for (DDLoggerNode *node in self._loggers) { + if (node->_logger == logger) { + loggerNode = node; + break; + } + } + + if (loggerNode == nil) { + NSLogDebug(@"DDLog: Request to remove logger which wasn't added"); + return; + } + + // Notify logger + if ([logger respondsToSelector:@selector(willRemoveLogger)]) { + dispatch_async(loggerNode->_loggerQueue, ^{ @autoreleasepool { + [logger willRemoveLogger]; + } }); + } + + // Remove from loggers array + [self._loggers removeObject:loggerNode]; +} + +- (void)lt_removeAllLoggers { + NSAssert(dispatch_get_specific(GlobalLoggingQueueIdentityKey), + @"This method should only be run on the logging thread/queue"); + + // Notify all loggers + for (DDLoggerNode *loggerNode in self._loggers) { + if ([loggerNode->_logger respondsToSelector:@selector(willRemoveLogger)]) { + dispatch_async(loggerNode->_loggerQueue, ^{ @autoreleasepool { + [loggerNode->_logger willRemoveLogger]; + } }); + } + } + + // Remove all loggers from array + + [self._loggers removeAllObjects]; +} + +- (NSArray *)lt_allLoggers { + NSAssert(dispatch_get_specific(GlobalLoggingQueueIdentityKey), + @"This method should only be run on the logging thread/queue"); + + NSMutableArray *theLoggers = [NSMutableArray new]; + + for (DDLoggerNode *loggerNode in self._loggers) { + [theLoggers addObject:loggerNode->_logger]; + } + + return [theLoggers copy]; +} + +- (NSArray *)lt_allLoggersWithLevel { + NSAssert(dispatch_get_specific(GlobalLoggingQueueIdentityKey), + @"This method should only be run on the logging thread/queue"); + + NSMutableArray *theLoggersWithLevel = [NSMutableArray new]; + + for (DDLoggerNode *loggerNode in self._loggers) { + [theLoggersWithLevel addObject:[DDLoggerInformation informationWithLogger:loggerNode->_logger + andLevel:loggerNode->_level]]; + } + + return [theLoggersWithLevel copy]; +} + +- (void)lt_logMessageToFile:(NSString *)fileName withMessage:(DDLogMessage *)logMessage { + // Execute the given log message on each of our loggers. + + NSAssert(dispatch_get_specific(GlobalLoggingQueueIdentityKey), + @"This method should only be run on the logging thread/queue"); + if (_numProcessors > 1) { + // Execute each logger concurrently, each within its own queue. + // All blocks are added to same group. + // After each block has been queued, wait on group. + // + // The waiting ensures that a slow logger doesn't end up with a large queue of pending log messages. + // This would defeat the purpose of the efforts we made earlier to restrict the max queue size. + + + for (DDLoggerNode *loggerNode in self._loggers) { + // skip the loggers that shouldn't write this message based on the log level + + if (!(logMessage->_flag & loggerNode->_level)) { + continue; + } + + dispatch_group_async(_loggingGroup, loggerNode->_loggerQueue, ^{ @autoreleasepool { + + if([loggerNode->_logger.loggerName isEqualToString:DDLoggerNameTTY] || + [loggerNode->_logger.loggerName isEqualToString:[NSString stringWithFormat:@"%@/%@",DDLoggerNameFile,fileName]]){ + [loggerNode->_logger logMessage:logMessage]; + } + + } }); + } + + dispatch_group_wait(_loggingGroup, DISPATCH_TIME_FOREVER); + } else { + // Execute each logger serially, each within its own queue. + for (DDLoggerNode *loggerNode in self._loggers) { + // skip the loggers that shouldn't write this message based on the log level + + if (!(logMessage->_flag & loggerNode->_level)) { + continue; + } + +#if DD_DEBUG + // we must assure that we aren not on loggerNode->_loggerQueue. + if (loggerNode->_loggerQueue == NULL) { + // tell that we can't dispatch logger node on queue that is NULL. + NSLogDebug(@"DDLog: current node has loggerQueue == NULL"); + } + else { + dispatch_async(loggerNode->_loggerQueue, ^{ + if (dispatch_get_specific(GlobalLoggingQueueIdentityKey)) { + // tell that we somehow on logging queue? + NSLogDebug(@"DDLog: current node has loggerQueue == globalLoggingQueue"); + } + }); + } +#endif + // next, we must check that node is OK. + dispatch_sync(loggerNode->_loggerQueue, ^{ @autoreleasepool { + + if([loggerNode->_logger.loggerName isEqualToString:NSStringFromClass([loggerNode->_logger class])] || + [loggerNode->_logger.loggerName isEqualToString:[NSString stringWithFormat:@"%@/%@",NSStringFromClass([loggerNode->_logger class]),fileName]]){ + [loggerNode->_logger logMessage:logMessage]; + } + } }); + } + } +} + +- (void)lt_flush { + // All log statements issued before the flush method was invoked have now been executed. + // + // Now we need to propagate the flush request to any loggers that implement the flush method. + // This is designed for loggers that buffer IO. + + NSAssert(dispatch_get_specific(GlobalLoggingQueueIdentityKey), + @"This method should only be run on the logging thread/queue"); + + for (DDLoggerNode *loggerNode in self._loggers) { + if ([loggerNode->_logger respondsToSelector:@selector(flush)]) { + dispatch_group_async(_loggingGroup, loggerNode->_loggerQueue, ^{ @autoreleasepool { + [loggerNode->_logger flush]; + } }); + } + } + + dispatch_group_wait(_loggingGroup, DISPATCH_TIME_FOREVER); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +NSString * __nullable DDExtractFileNameWithoutExtension(const char *filePath, BOOL copy) { + if (filePath == NULL) { + return nil; + } + + char *lastSlash = NULL; + char *lastDot = NULL; + + char *p = (char *)filePath; + + while (*p != '\0') { + if (*p == '/') { + lastSlash = p; + } else if (*p == '.') { + lastDot = p; + } + + p++; + } + + char *subStr; + NSUInteger subLen; + + if (lastSlash) { + if (lastDot) { + // lastSlash -> lastDot + subStr = lastSlash + 1; + subLen = (NSUInteger)(lastDot - subStr); + } else { + // lastSlash -> endOfString + subStr = lastSlash + 1; + subLen = (NSUInteger)(p - subStr); + } + } else { + if (lastDot) { + // startOfString -> lastDot + subStr = (char *)filePath; + subLen = (NSUInteger)(lastDot - subStr); + } else { + // startOfString -> endOfString + subStr = (char *)filePath; + subLen = (NSUInteger)(p - subStr); + } + } + + if (copy) { + return [[NSString alloc] initWithBytes:subStr + length:subLen + encoding:NSUTF8StringEncoding]; + } else { + // We can take advantage of the fact that __FILE__ is a string literal. + // Specifically, we don't need to waste time copying the string. + // We can just tell NSString to point to a range within the string literal. + + return [[NSString alloc] initWithBytesNoCopy:subStr + length:subLen + encoding:NSUTF8StringEncoding + freeWhenDone:NO]; + } +} + + + +/** + * Returns the path to the default logs directory. + * If the logs directory doesn't exist, this method automatically creates it. + **/ ++ (NSString *)rootLogsDirectory { + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + NSString *baseDir = paths.firstObject; + NSString *rootDirectory = [baseDir stringByAppendingPathComponent:@"Logs"]; + return rootDirectory; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation DDLoggerNode + +- (instancetype)initWithLogger:(id )logger loggerQueue:(dispatch_queue_t)loggerQueue level:(DDLogLevel)level { + if ((self = [super init])) { + _logger = logger; + + if (loggerQueue) { + _loggerQueue = loggerQueue; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(loggerQueue); + #endif + } + + _level = level; + } + return self; +} + ++ (instancetype)nodeWithLogger:(id )logger loggerQueue:(dispatch_queue_t)loggerQueue level:(DDLogLevel)level { + return [[self alloc] initWithLogger:logger loggerQueue:loggerQueue level:level]; +} + +- (void)dealloc { + #if !OS_OBJECT_USE_OBJC + if (_loggerQueue) { + dispatch_release(_loggerQueue); + } + #endif +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation DDLogMessage + +- (instancetype)init { + self = [super init]; + return self; +} + +- (instancetype)initWithMessage:(NSString *)message + level:(DDLogLevel)level + flag:(DDLogFlag)flag + options:(DDLogMessageOptions)options + timestamp:(NSDate *)timestamp { + NSParameterAssert(message); + + if ((self = [super init])) { + BOOL copyMessage = (options & DDLogMessageDontCopyMessage) == 0; + _message = copyMessage ? [message copy] : message; + _level = level; + _flag = flag; + _options = options; + _timestamp = timestamp ?: [NSDate new]; + + __uint64_t tid; + if (pthread_threadid_np(NULL, &tid) == 0) { + _threadID = [[NSString alloc] initWithFormat:@"%llu", tid]; + } else { + _threadID = @"missing threadId"; + } + _threadName = NSThread.currentThread.name; + + + // Try to get the current queue's label + _queueLabel = [[NSString alloc] initWithFormat:@"%s", dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)]; + _qos = (NSUInteger) qos_class_self(); + } + return self; +} + +NS_INLINE BOOL _nullable_strings_equal(NSString* _Nullable lhs, NSString* _Nullable rhs) +{ + if (lhs == nil) { + if (rhs == nil) + return YES; + } else if (rhs != nil && [lhs isEqualToString:(NSString* _Nonnull)rhs]) + return YES; + return NO; +} + +- (BOOL)isEqual:(id)other { + // Subclasses of NSObject should not call [super isEqual:] here. + // See https://stackoverflow.com/questions/36593038/confused-about-the-default-isequal-and-hash-implements + if (other == self) { + return YES; + } else if (!other || ![other isKindOfClass:[DDLogMessage class]]) { + return NO; + } else { + __auto_type otherMsg = (DDLogMessage *)other; + return [otherMsg->_message isEqualToString:_message] + && otherMsg->_level == _level + && otherMsg->_flag == _flag + && [otherMsg->_timestamp isEqualToDate:_timestamp] + && [otherMsg->_threadID isEqualToString:_threadID] // If the thread ID is the same, the name will likely be the same as well. + && [otherMsg->_queueLabel isEqualToString:_queueLabel] + && otherMsg->_qos == _qos; + } +} + +- (NSUInteger)hash { + // Subclasses of NSObject should not call [super hash] here. + // See https://stackoverflow.com/questions/36593038/confused-about-the-default-isequal-and-hash-implements + return _message.hash + ^ _level + ^ _flag + ^ _timestamp.hash + ^ _threadID.hash + ^ _queueLabel.hash + ^ _qos; +} + +- (id)copyWithZone:(NSZone * __attribute__((unused)))zone { + DDLogMessage *newMessage = [DDLogMessage new]; + + newMessage->_message = _message; + newMessage->_level = _level; + newMessage->_flag = _flag; + newMessage->_options = _options; + newMessage->_timestamp = _timestamp; + newMessage->_threadID = _threadID; + newMessage->_threadName = _threadName; + newMessage->_queueLabel = _queueLabel; + newMessage->_qos = _qos; + + return newMessage; +} + + +@end + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation DDAbstractLogger + +- (instancetype)initWithTag:(NSString *)tagName { + if ((self = [super init])) { + const char *loggerQueueName = NULL; + + _loggerTag = tagName; + if ([self respondsToSelector:@selector(loggerName)]) { + loggerQueueName = self.loggerName.UTF8String; + } + + _loggerQueue = dispatch_queue_create(loggerQueueName, NULL); + + // We're going to use dispatch_queue_set_specific() to "mark" our loggerQueue. + // Later we can use dispatch_get_specific() to determine if we're executing on our loggerQueue. + // The documentation states: + // + // > Keys are only compared as pointers and are never dereferenced. + // > Thus, you can use a pointer to a static variable for a specific subsystem or + // > any other value that allows you to identify the value uniquely. + // > Specifying a pointer to a string constant is not recommended. + // + // So we're going to use the very convenient key of "self", + // which also works when multiple logger classes extend this class, as each will have a different "self" key. + // + // This is used primarily for thread-safety assertions (via the isOnInternalLoggerQueue method below). + + void *key = (__bridge void *)self; + void *nonNullValue = (__bridge void *)self; + + dispatch_queue_set_specific(_loggerQueue, key, nonNullValue, NULL); + } + + return self; +} + +- (void)dealloc { + #if !OS_OBJECT_USE_OBJC + + if (_loggerQueue) { + dispatch_release(_loggerQueue); + } + + #endif +} + +- (void)logMessage:(DDLogMessage * __attribute__((unused)))logMessage { + // Override me +} + +- (id )logFormatter { + // This method must be thread safe and intuitive. + // Therefore if somebody executes the following code: + // + // [logger setLogFormatter:myFormatter]; + // formatter = [logger logFormatter]; + // + // They would expect formatter to equal myFormatter. + // This functionality must be ensured by the getter and setter method. + // + // The thread safety must not come at a cost to the performance of the logMessage method. + // This method is likely called sporadically, while the logMessage method is called repeatedly. + // This means, the implementation of this method: + // - Must NOT require the logMessage method to acquire a lock. + // - Must NOT require the logMessage method to access an atomic property (also a lock of sorts). + // + // Thread safety is ensured by executing access to the formatter variable on the loggerQueue. + // This is the same queue that the logMessage method operates on. + // + // Note: The last time I benchmarked the performance of direct access vs atomic property access, + // direct access was over twice as fast on the desktop and over 6 times as fast on the iPhone. + // + // Furthermore, consider the following code: + // + // DDLogVerbose(@"log msg 1"); + // DDLogVerbose(@"log msg 2"); + // [logger setFormatter:myFormatter]; + // DDLogVerbose(@"log msg 3"); + // + // Our intuitive requirement means that the new formatter will only apply to the 3rd log message. + // This must remain true even when using asynchronous logging. + // We must keep in mind the various queue's that are in play here: + // + // loggerQueue : Our own private internal queue that the logMessage method runs on. + // Operations are added to this queue from the global loggingQueue. + // + // globalLoggingQueue : The queue that all log messages go through before they arrive in our loggerQueue. + // + // All log statements go through the serial globalLoggingQueue before they arrive at our loggerQueue. + // Thus this method also goes through the serial globalLoggingQueue to ensure intuitive operation. + + // IMPORTANT NOTE: + // + // Methods within the DDLogger implementation MUST access the formatter ivar directly. + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block id result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(self->_loggerQueue, ^{ + result = self->_logFormatter; + }); + }); + + return result; +} + +- (void)setLogFormatter:(id )logFormatter { + // The design of this method is documented extensively in the logFormatter message (above in code). + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_block_t block = ^{ + @autoreleasepool { + if (self->_logFormatter != logFormatter) { + if ([self->_logFormatter respondsToSelector:@selector(willRemoveFromLogger:)]) { + [self->_logFormatter willRemoveFromLogger:self]; + } + + self->_logFormatter = logFormatter; + + if ([self->_logFormatter respondsToSelector:@selector(didAddToLogger:inQueue:)]) { + [self->_logFormatter didAddToLogger:self inQueue:self->_loggerQueue]; + } else if ([self->_logFormatter respondsToSelector:@selector(didAddToLogger:)]) { + [self->_logFormatter didAddToLogger:self]; + } + } + } + }; + + dispatch_async(DDLog.loggingQueue, ^{ + dispatch_async(self->_loggerQueue, block); + }); +} + +- (dispatch_queue_t)loggerQueue { + return _loggerQueue; +} + +- (NSString *)loggerName { + if(self.loggerTag.length > 0){ + return [NSString stringWithFormat:@"%@/%@",NSStringFromClass([self class]),self.loggerTag]; + } + return NSStringFromClass([self class]); +} + +- (BOOL)isOnGlobalLoggingQueue { + return (dispatch_get_specific(GlobalLoggingQueueIdentityKey) != NULL); +} + +- (BOOL)isOnInternalLoggerQueue { + void *key = (__bridge void *)self; + + return (dispatch_get_specific(key) != NULL); +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface DDLoggerInformation() +{ + // Direct accessors to be used only for performance + @public + id _logger; + DDLogLevel _level; +} + +@end + +@implementation DDLoggerInformation + +- (instancetype)initWithLogger:(id )logger andLevel:(DDLogLevel)level { + if ((self = [super init])) { + _logger = logger; + _level = level; + } + return self; +} + ++ (instancetype)informationWithLogger:(id )logger andLevel:(DDLogLevel)level { + return [[self alloc] initWithLogger:logger andLevel:level]; +} + + + +@end diff --git a/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLogFileFormatterDefault.h b/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLogFileFormatterDefault.h new file mode 100644 index 0000000..6c528fd --- /dev/null +++ b/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLogFileFormatterDefault.h @@ -0,0 +1,42 @@ +// +// DDLogFileFormatterDefault.h +// NVLogManagerDemo +// +// Created by cxb on 2023/5/10. +// Copyright © 2023 com.zhouxi. All rights reserved. +// + +#import +#import "DDLog.h" + +NS_ASSUME_NONNULL_BEGIN +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Most users will want file log messages to be prepended with the date and time. + * Rather than forcing the majority of users to write their own formatter, + * we will supply a logical default formatter. + * Users can easily replace this formatter with their own by invoking the `setLogFormatter:` method. + * It can also be removed by calling `setLogFormatter:`, and passing a nil parameter. + * + * In addition to the convenience of having a logical default formatter, + * it will also provide a template that makes it easy for developers to copy and change. + **/ +@interface DDLogFileFormatterDefault : NSObject + +/** + * Default initializer + */ +- (instancetype)init; + +/** + * Designated initializer, requires a date formatter + */ +- (instancetype)initWithDateFormatter:(nullable NSDateFormatter *)dateFormatter NS_DESIGNATED_INITIALIZER; + +@end + + +NS_ASSUME_NONNULL_END diff --git a/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLogFileFormatterDefault.m b/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLogFileFormatterDefault.m new file mode 100644 index 0000000..a5a24be --- /dev/null +++ b/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLogFileFormatterDefault.m @@ -0,0 +1,54 @@ +// +// DDLogFileFormatterDefault.m +// NVLogManagerDemo +// +// Created by cxb on 2023/5/10. +// Copyright © 2023 com.zhouxi. All rights reserved. +// + +#import "DDLogFileFormatterDefault.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface DDLogFileFormatterDefault () { + NSDateFormatter *_dateFormatter; +} + +@end + + +@implementation DDLogFileFormatterDefault + +- (instancetype)init { + return [self initWithDateFormatter:nil]; +} + +- (instancetype)initWithDateFormatter:(nullable NSDateFormatter *)aDateFormatter { + if ((self = [super init])) { + if (aDateFormatter) { + _dateFormatter = aDateFormatter; + } else { + _dateFormatter = [[NSDateFormatter alloc] init]; +// [_dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; // 10.4+ style +// [_dateFormatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]]; +// [_dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + [_dateFormatter setDateFormat:@"yyyy/MM/dd HH:mm:ss"]; + + } + } + + return self; +} + +- (NSString *)formatLogMessage:(DDLogMessage *)logMessage { + NSString *dateAndTime = [_dateFormatter stringFromDate:logMessage->_timestamp]; +// +// NSTimeInterval time = [logMessage->_timestamp timeIntervalSince1970]*1000; +// NSString *timeString = [NSString stringWithFormat:@"%.0f",time]; + return [NSString stringWithFormat:@"%@ %@", dateAndTime, logMessage->_message]; +} + +@end + diff --git a/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLogFileInfo.h b/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLogFileInfo.h new file mode 100644 index 0000000..bef2cde --- /dev/null +++ b/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLogFileInfo.h @@ -0,0 +1,70 @@ +// +// DDLogFileInfo.h +// NVLogManagerDemo +// +// Created by cxb on 2023/5/10. +// Copyright © 2023 com.zhouxi. All rights reserved. +// + +#import +#import "DDFileLogger.h" +#import "DDLogFileInfo.h" +#import "DDLog.h" +#import +NS_ASSUME_NONNULL_BEGIN + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * `DDLogFileInfo` is a simple class that provides access to various file attributes. + * It provides good performance as it only fetches the information if requested, + * and it caches the information to prevent duplicate fetches. + * + * It was designed to provide quick snapshots of the current state of log files, + * and to help sort log files in an array. + * + * This class does not monitor the files, or update it's cached attribute values if the file changes on disk. + * This is not what the class was designed for. + * + * If you absolutely must get updated values, + * you can invoke the reset method which will clear the cache. + **/ +@interface DDLogFileInfo : NSObject + +@property (strong, nonatomic, readonly) NSString *filePath; +@property (strong, nonatomic, readonly) NSString *fileName; + +@property (strong, nonatomic, readonly) NSDictionary *fileAttributes; + +@property (strong, nonatomic, nullable, readonly) NSDate *creationDate; +@property (strong, nonatomic, nullable, readonly) NSDate *modificationDate; + +@property (nonatomic, readonly) unsigned long long fileSize; + +@property (nonatomic, readonly) NSTimeInterval age; + +@property (nonatomic, readonly) BOOL isSymlink; + +@property (nonatomic, readwrite) BOOL isArchived; + ++ (nullable instancetype)logFileWithPath:(nullable NSString *)filePath NS_SWIFT_UNAVAILABLE("Use init(filePath:)"); + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithFilePath:(NSString *)filePath NS_DESIGNATED_INITIALIZER; + +- (void)reset; +- (void)renameFile:(NSString *)newFileName NS_SWIFT_NAME(renameFile(to:)); + +- (BOOL)hasExtendedAttributeWithName:(NSString *)attrName; + +- (void)addExtendedAttributeWithName:(NSString *)attrName; +- (void)removeExtendedAttributeWithName:(NSString *)attrName; + +- (NSComparisonResult)reverseCompareByCreationDate:(DDLogFileInfo *)another; +- (NSComparisonResult)reverseCompareByModificationDate:(DDLogFileInfo *)another; + +@end + +NS_ASSUME_NONNULL_END diff --git a/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLogFileInfo.m b/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLogFileInfo.m new file mode 100644 index 0000000..a29a2f0 --- /dev/null +++ b/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLogFileInfo.m @@ -0,0 +1,401 @@ +// +// DDLogFileInfo.m +// NVLogManagerDemo +// +// Created by cxb on 2023/5/10. +// Copyright © 2023 com.zhouxi. All rights reserved. +// + +#import "DDLogFileInfo.h" +static NSString * const kDDXAttrArchivedName = @"guru.log.archived"; + +@interface DDLogFileInfo () { + __strong NSString *_filePath; + __strong NSString *_fileName; + + __strong NSDictionary *_fileAttributes; + + __strong NSDate *_creationDate; + __strong NSDate *_modificationDate; + + unsigned long long _fileSize; +} + +#if TARGET_IPHONE_SIMULATOR + +// Old implementation of extended attributes on the simulator. + +- (BOOL)_hasExtensionAttributeWithName:(NSString *)attrName; +- (void)_removeExtensionAttributeWithName:(NSString *)attrName; + +#endif + +@end +@implementation DDLogFileInfo + +@synthesize filePath; + +@dynamic fileName; +@dynamic fileAttributes; +@dynamic creationDate; +@dynamic modificationDate; +@dynamic fileSize; +@dynamic age; + +@dynamic isArchived; + +#pragma mark Lifecycle + ++ (instancetype)logFileWithPath:(NSString *)aFilePath { + if (!aFilePath) return nil; + return [[self alloc] initWithFilePath:aFilePath]; +} + +- (instancetype)initWithFilePath:(NSString *)aFilePath { + NSParameterAssert(aFilePath); + if ((self = [super init])) { + filePath = [aFilePath copy]; + } + + return self; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Standard Info +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSDictionary *)fileAttributes { + if (_fileAttributes == nil && filePath != nil) { + __autoreleasing NSError *error = nil; + _fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error]; + if (!_fileAttributes) { + NSLogError(@"DDLogFileInfo: Failed to read file attributes: %@", error); + } + } + + return _fileAttributes ?: @{}; +} + +- (NSString *)fileName { + if (_fileName == nil) { + _fileName = [filePath lastPathComponent]; + } + + return _fileName; +} + +- (NSDate *)modificationDate { + if (_modificationDate == nil) { + _modificationDate = self.fileAttributes[NSFileModificationDate]; + } + + return _modificationDate; +} + +- (NSDate *)creationDate { + if (_creationDate == nil) { + _creationDate = self.fileAttributes[NSFileCreationDate]; + } + + return _creationDate; +} + +- (unsigned long long)fileSize { + if (_fileSize == 0) { + _fileSize = [self.fileAttributes[NSFileSize] unsignedLongLongValue]; + } + + return _fileSize; +} + +- (NSTimeInterval)age { + return -[[self creationDate] timeIntervalSinceNow]; +} + +- (BOOL)isSymlink { + return self.fileAttributes[NSFileType] == NSFileTypeSymbolicLink; +} + +- (NSString *)description { + return [@{ @"filePath": self.filePath ? : @"", + @"fileName": self.fileName ? : @"", + @"fileAttributes": self.fileAttributes ? : @"", + @"creationDate": self.creationDate ? : @"", + @"modificationDate": self.modificationDate ? : @"", + @"fileSize": @(self.fileSize), + @"age": @(self.age), + @"isArchived": @(self.isArchived) } description]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Archiving +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)isArchived { + return [self hasExtendedAttributeWithName:kDDXAttrArchivedName]; +} + +- (void)setIsArchived:(BOOL)flag { + if (flag) { + [self addExtendedAttributeWithName:kDDXAttrArchivedName]; + } else { + [self removeExtendedAttributeWithName:kDDXAttrArchivedName]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Changes +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)reset { + _fileName = nil; + _fileAttributes = nil; + _creationDate = nil; + _modificationDate = nil; +} + +- (void)renameFile:(NSString *)newFileName { + // This method is only used on the iPhone simulator, where normal extended attributes are broken. + // See full explanation in the header file. + + if (![newFileName isEqualToString:[self fileName]]) { + NSFileManager* fileManager = [NSFileManager defaultManager]; + NSString *fileDir = [filePath stringByDeletingLastPathComponent]; + NSString *newFilePath = [fileDir stringByAppendingPathComponent:newFileName]; + + // We only want to assert when we're not using the simulator, as we're "archiving" a log file with this method in the sim + // (in which case the file might not exist anymore and neither does it parent folder). +#if defined(DEBUG) && (!defined(TARGET_IPHONE_SIMULATOR) || !TARGET_IPHONE_SIMULATOR) + BOOL directory = NO; + [fileManager fileExistsAtPath:fileDir isDirectory:&directory]; + NSAssert(directory, @"Containing directory must exist."); +#endif + + __autoreleasing NSError *error = nil; + BOOL success = [fileManager removeItemAtPath:newFilePath error:&error]; + if (!success && error.code != NSFileNoSuchFileError) { + NSLogError(@"DDLogFileInfo: Error deleting archive (%@): %@", self.fileName, error); + } + + success = [fileManager moveItemAtPath:filePath toPath:newFilePath error:&error]; + + // When a log file is deleted, moved or renamed on the simulator, we attempt to rename it as a + // result of "archiving" it, but since the file doesn't exist anymore, needless error logs are printed + // We therefore ignore this error, and assert that the directory we are copying into exists (which + // is the only other case where this error code can come up). +#if TARGET_IPHONE_SIMULATOR + if (!success && error.code != NSFileNoSuchFileError) +#else + if (!success) +#endif + { + NSLogError(@"DDLogFileInfo: Error renaming file (%@): %@", self.fileName, error); + } + + filePath = newFilePath; + [self reset]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Attribute Management +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if TARGET_IPHONE_SIMULATOR + +// Old implementation of extended attributes on the simulator. + +// Extended attributes were not working properly on the simulator +// due to misuse of setxattr() function. +// Now that this is fixed in the new implementation, we want to keep +// backward compatibility with previous simulator installations. + +static NSString * const kDDExtensionSeparator = @"."; + +static NSString *_xattrToExtensionName(NSString *attrName) { + static NSDictionary* _xattrToExtensionNameMap; + static dispatch_once_t _token; + dispatch_once(&_token, ^{ + _xattrToExtensionNameMap = @{ kDDXAttrArchivedName: @"archived" }; + }); + return [_xattrToExtensionNameMap objectForKey:attrName]; +} + +- (BOOL)_hasExtensionAttributeWithName:(NSString *)attrName { + // This method is only used on the iPhone simulator for backward compatibility reason. + + // Split the file name into components. File name may have various format, but generally + // structure is same: + // + // . and .archived. + // or + // and .archived + // + // So we want to search for the attrName in the components (ignoring the first array index). + + NSArray *components = [[self fileName] componentsSeparatedByString:kDDExtensionSeparator]; + + // Watch out for file names without an extension + + for (NSUInteger i = 1; i < components.count; i++) { + NSString *attr = components[i]; + + if ([attrName isEqualToString:attr]) { + return YES; + } + } + + return NO; +} + +- (void)_removeExtensionAttributeWithName:(NSString *)attrName { + // This method is only used on the iPhone simulator for backward compatibility reason. + + if ([attrName length] == 0) { + return; + } + + // Example: + // attrName = "archived" + // + // "mylog.archived.txt" -> "mylog.txt" + // "mylog.archived" -> "mylog" + + NSArray *components = [[self fileName] componentsSeparatedByString:kDDExtensionSeparator]; + + NSUInteger count = [components count]; + + NSUInteger estimatedNewLength = [[self fileName] length]; + NSMutableString *newFileName = [NSMutableString stringWithCapacity:estimatedNewLength]; + + if (count > 0) { + [newFileName appendString:components[0]]; + } + + BOOL found = NO; + + NSUInteger i; + + for (i = 1; i < count; i++) { + NSString *attr = components[i]; + + if ([attrName isEqualToString:attr]) { + found = YES; + } else { + [newFileName appendString:kDDExtensionSeparator]; + [newFileName appendString:attr]; + } + } + + if (found) { + [self renameFile:newFileName]; + } +} + +#endif /* if TARGET_IPHONE_SIMULATOR */ + +- (BOOL)hasExtendedAttributeWithName:(NSString *)attrName { + const char *path = [filePath fileSystemRepresentation]; + const char *name = [attrName UTF8String]; + BOOL hasExtendedAttribute = NO; + char buffer[1]; + + ssize_t result = getxattr(path, name, buffer, 1, 0, 0); + + // Fast path + if (result > 0 && buffer[0] == '\1') { + hasExtendedAttribute = YES; + } + // Maintain backward compatibility, but fix it for future checks + else if (result >= 0) { + hasExtendedAttribute = YES; + + [self addExtendedAttributeWithName:attrName]; + } +#if TARGET_IPHONE_SIMULATOR + else if ([self _hasExtensionAttributeWithName:_xattrToExtensionName(attrName)]) { + hasExtendedAttribute = YES; + + [self addExtendedAttributeWithName:attrName]; + } +#endif + + return hasExtendedAttribute; +} + +- (void)addExtendedAttributeWithName:(NSString *)attrName { + const char *path = [filePath fileSystemRepresentation]; + const char *name = [attrName UTF8String]; + + int result = setxattr(path, name, "\1", 1, 0, 0); + + if (result < 0) { + if (errno != ENOENT) { + NSLogError(@"DDLogFileInfo: setxattr(%@, %@): error = %s", + attrName, + filePath, + strerror(errno)); + } else { + NSLogDebug(@"DDLogFileInfo: File does not exist in setxattr(%@, %@): error = %s", + attrName, + filePath, + strerror(errno)); + } + } +#if TARGET_IPHONE_SIMULATOR + else { + [self _removeExtensionAttributeWithName:_xattrToExtensionName(attrName)]; + } +#endif +} + +- (void)removeExtendedAttributeWithName:(NSString *)attrName { + const char *path = [filePath fileSystemRepresentation]; + const char *name = [attrName UTF8String]; + + int result = removexattr(path, name, 0); + + if (result < 0 && errno != ENOATTR) { + NSLogError(@"DDLogFileInfo: removexattr(%@, %@): error = %s", + attrName, + self.fileName, + strerror(errno)); + } + +#if TARGET_IPHONE_SIMULATOR + [self _removeExtensionAttributeWithName:_xattrToExtensionName(attrName)]; +#endif +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Comparisons +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)isEqual:(id)object { + if ([object isKindOfClass:[self class]]) { + DDLogFileInfo *another = (DDLogFileInfo *)object; + + return [filePath isEqualToString:[another filePath]]; + } + + return NO; +} + +- (NSUInteger)hash { + return [filePath hash]; +} + +- (NSComparisonResult)reverseCompareByCreationDate:(DDLogFileInfo *)another { + __auto_type us = [self creationDate]; + __auto_type them = [another creationDate]; + return [them compare:us]; +} + +- (NSComparisonResult)reverseCompareByModificationDate:(DDLogFileInfo *)another { + __auto_type us = [self modificationDate]; + __auto_type them = [another modificationDate]; + return [them compare:us]; +} + +@end + diff --git a/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLogFileManager.h b/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLogFileManager.h new file mode 100644 index 0000000..10843de --- /dev/null +++ b/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLogFileManager.h @@ -0,0 +1,149 @@ +// +// DDLogFileManager.h +// NVLogManagerDemo +// +// Created by cxb on 2023/5/10. +// Copyright © 2023 com.zhouxi. All rights reserved. +// +/** + * The LogFileManager protocol is designed to allow you to control all aspects of your log files. + * + * The primary purpose of this is to allow you to do something with the log files after they have been rolled. + * Perhaps you want to compress them to save disk space. + * Perhaps you want to upload them to an FTP server. + * Perhaps you want to run some analytics on the file. + * + * A default LogFileManager is, of course, provided. + * The default LogFileManager simply deletes old log files according to the maximumNumberOfLogFiles property. + * + * This protocol provides various methods to fetch the list of log files. + * + * There are two variants: sorted and unsorted. + * If sorting is not necessary, the unsorted variant is obviously faster. + * The sorted variant will return an array sorted by when the log files were created, + * with the most recently created log file at index 0, and the oldest log file at the end of the array. + * + * You can fetch only the log file paths (full path including name), log file names (name only), + * or an array of `DDLogFileInfo` objects. + * The `DDLogFileInfo` class is documented below, and provides a handy wrapper that + * gives you easy access to various file attributes such as the creation date or the file size. + */ + + +#import + +NS_ASSUME_NONNULL_BEGIN +@class DDLogFileInfo; +@protocol DDLogFileManager + +@required + +// Public properties + +/** + * The maximum number of archived log files to keep on disk. + * For example, if this property is set to 3, + * then the LogFileManager will only keep 3 archived log files (plus the current active log file) on disk. + * Once the active log file is rolled/archived, then the oldest of the existing 3 rolled/archived log files is deleted. + * + * You may optionally disable this option by setting it to zero. + **/ +@property (readwrite, assign, atomic) NSUInteger maximumNumberOfLogFiles; + +/** + * The maximum space that logs can take. On rolling logfile all old log files that exceed logFilesDiskQuota will + * be deleted. + * + * You may optionally disable this option by setting it to zero. + **/ +@property (readwrite, assign, atomic) unsigned long long logFilesDiskQuota; + +// Public methods + +/** + * Returns the logs directory (path) + */ +@property (nonatomic, readonly, copy) NSString *logsDirectory;//全路径 + +@property (nonatomic, readonly, copy) NSString *directoryName;//文件夹名称 + +/** + * Returns an array of `NSString` objects, + * each of which is the filePath to an existing log file on disk. + **/ +@property (nonatomic, readonly, strong) NSArray *unsortedLogFilePaths; + +/** + * Returns an array of `NSString` objects, + * each of which is the fileName of an existing log file on disk. + **/ +@property (nonatomic, readonly, strong) NSArray *unsortedLogFileNames; + +/** + * Returns an array of `DDLogFileInfo` objects, + * each representing an existing log file on disk, + * and containing important information about the log file such as it's modification date and size. + **/ +@property (nonatomic, readonly, strong) NSArray *unsortedLogFileInfos; + +/** + * Just like the `unsortedLogFilePaths` method, but sorts the array. + * The items in the array are sorted by creation date. + * The first item in the array will be the most recently created log file. + **/ +@property (nonatomic, readonly, strong) NSArray *sortedLogFilePaths; + +/** + * Just like the `unsortedLogFileNames` method, but sorts the array. + * The items in the array are sorted by creation date. + * The first item in the array will be the most recently created log file. + **/ +@property (nonatomic, readonly, strong) NSArray *sortedLogFileNames; + +/** + * Just like the `unsortedLogFileInfos` method, but sorts the array. + * The items in the array are sorted by creation date. + * The first item in the array will be the most recently created log file. + **/ +@property (nonatomic, readonly, strong) NSArray *sortedLogFileInfos; + +// Private methods (only to be used by DDFileLogger) + +/** + * Generates a new unique log file path, and creates the corresponding log file. + * This method is executed directly on the file logger's internal queue. + * The file has to exist by the time the method returns. + **/ +- (nullable NSString *)createNewLogFileWithError:(NSError **)error; + +@optional + +// Private methods (only to be used by DDFileLogger) +/** + * Creates a new log file ignoring any errors. Deprecated in favor of `-createNewLogFileWithError:`. + * Will only be called if `-createNewLogFileWithError:` is not implemented. + **/ +- (nullable NSString *)createNewLogFile __attribute__((deprecated("Use -createNewLogFileWithError:"))) NS_SWIFT_UNAVAILABLE("Use -createNewLogFileWithError:"); + +// Notifications from DDFileLogger + +/// Called when a log file was archived. Executed on global queue with default priority. +/// @param logFilePath The path to the log file that was archived. +/// @param wasRolled Whether or not the archiving happend after rolling the log file. +- (void)didArchiveLogFile:(NSString *)logFilePath wasRolled:(BOOL)wasRolled NS_SWIFT_NAME(didArchiveLogFile(atPath:wasRolled:)); + +// Deprecated APIs +/** + * Called when a log file was archived. Executed on global queue with default priority. + */ +- (void)didArchiveLogFile:(NSString *)logFilePath NS_SWIFT_NAME(didArchiveLogFile(atPath:)) __attribute__((deprecated("Use -didArchiveLogFile:wasRolled:"))); + +/** + * Called when the roll action was executed and the log was archived. + * Executed on global queue with default priority. + */ +- (void)didRollAndArchiveLogFile:(NSString *)logFilePath NS_SWIFT_NAME(didRollAndArchiveLogFile(atPath:)) __attribute__((deprecated("Use -didArchiveLogFile:wasRolled:"))); + +@end + +NS_ASSUME_NONNULL_END diff --git a/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLogFileManagerDefault.h b/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLogFileManagerDefault.h new file mode 100644 index 0000000..5b589f4 --- /dev/null +++ b/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLogFileManagerDefault.h @@ -0,0 +1,122 @@ +// +// DDFileLoggerManager.h +// NVLogManagerDemo +// +// Created by cxb on 2023/5/10. +// Copyright © 2023 com.zhouxi. All rights reserved. +// + +#import +#import "DDFileLogger.h" +#import "DDLog.h" +#import "DDLogFileInfo.h" +#import "DDLogFileManager.h" +NS_ASSUME_NONNULL_BEGIN + +/** + * Default log file manager. + * + * All log files are placed inside the logsDirectory. + * If a specific logsDirectory isn't specified, the default directory is used. + * On Mac, this is in `~/Library/Logs/`. + * On iPhone, this is in `~/Library/Caches/Logs`. + * + * Log files are named `"