@_nazmiforreal/flutter-ota 0.1.25 → 0.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/flutter-ota-linux-x64 +0 -0
- package/bin/flutter-ota.js +12 -11
- package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +46 -13
- package/dart-src/packages/cli-tools/lib/src/backend.dart +6 -6
- package/dart-src/packages/cli-tools/lib/src/commands/build.dart +2 -0
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +7 -7
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +4 -1
- package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/config.dart +5 -5
- package/dart-src/packages/cli-tools/lib/src/operations.dart +2 -2
- package/dart-src/packages/cli-tools/pubspec.yaml +8 -21
- package/dart-src/packages/cli-tools/test/backends_test.dart +3 -3
- package/dart-src/packages/cli-tools/test/cli_test.dart +1 -1
- package/dart-src/packages/cli-tools/test/mocks/mock_aws_s3_client.dart +2 -2
- package/dart-src/packages/cli-tools/test/mocks/mock_d1_client.dart +2 -2
- package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +4 -1
- package/dart-src/packages/cli-tools/test/mocks/mock_postgres_client.dart +2 -2
- package/dart-src/packages/cli-tools/test/pocketbase_smoke_test.dart +1 -1
- package/dart-src/root/README.md +394 -0
- package/dart-src/root/analysis_options.yaml +21 -0
- package/dart-src/root/android/build.gradle +116 -0
- package/dart-src/root/android/consumer-rules.pro +23 -0
- package/dart-src/root/android/settings.gradle +1 -0
- package/dart-src/root/android/src/main/AndroidManifest.xml +21 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/ApplyResult.kt +52 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/BlacklistStore.kt +154 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/BootDiagnosticStore.kt +150 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/CrashGuard.kt +271 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/FlutterPatcherApplication.kt +201 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/FlutterPatcherAutoInitProvider.kt +93 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/FlutterPatcherPlugin.kt +245 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/LoaderHook.kt +298 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/PatchManager.kt +1579 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/PatcherConfig.kt +173 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/SignatureVerifier.kt +177 -0
- package/dart-src/root/android/src/test/kotlin/com/flutter_patcher/flutter_patcher/CrashGuardTest.kt +61 -0
- package/dart-src/root/android/src/test/kotlin/com/flutter_patcher/flutter_patcher/LoaderHookTest.kt +25 -0
- package/dart-src/root/android/src/test/kotlin/com/flutter_patcher/flutter_patcher/PatchManagerTest.kt +401 -0
- package/dart-src/root/android/src/test/kotlin/com/flutter_patcher/flutter_patcher/PatcherConfigTest.kt +30 -0
- package/dart-src/root/bin/pack.dart +182 -0
- package/dart-src/root/lib/flutter_ota_kit.dart +1170 -0
- package/dart-src/root/lib/src/app_version_resolver.dart +36 -0
- package/dart-src/root/lib/src/aws_update_source.dart +97 -0
- package/dart-src/root/lib/src/blacklist.dart +54 -0
- package/dart-src/root/lib/src/boot_diagnostic.dart +138 -0
- package/dart-src/root/lib/src/cloudflare_update_source.dart +97 -0
- package/dart-src/root/lib/src/flutter_ota_app.dart +69 -0
- package/dart-src/root/lib/src/io.dart +48 -0
- package/dart-src/root/lib/src/io_stub.dart +19 -0
- package/dart-src/root/lib/src/ota_progress_overlay.dart +987 -0
- package/dart-src/root/lib/src/pack/pack.dart +434 -0
- package/dart-src/root/lib/src/patch_info.dart +12 -0
- package/dart-src/root/lib/src/patcher_channel.dart +160 -0
- package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_database.dart +1 -1
- package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_s3_client.dart +1 -1
- package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_storage.dart +1 -1
- package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_storage_profile.dart +1 -1
- package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/with_cloudfront_signed_url.dart +1 -1
- package/dart-src/root/lib/src/pkg/client/flutter_ota_kit_client.dart +1 -0
- package/dart-src/root/lib/src/pkg/client/src/server_update_source.dart +46 -0
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/cloudflare_worker_database.dart +1 -1
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_build_where.dart +1 -1
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_bundle_mapper.dart +3 -3
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_database.dart +1 -1
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_database_plugin.dart +2 -2
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/r2_s3_client.dart +1 -1
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/r2_storage.dart +1 -1
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/r2_storage_profile.dart +1 -1
- package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/status.dart +1 -1
- package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/flutter_ota_kit_plugin_core.dart +1 -1
- package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/create_blob_database_plugin.dart +1 -1
- package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/filter_compatible_app_versions.dart +1 -1
- package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/get_update_info.dart +1 -1
- package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/request_update_bundle_state.dart +1 -1
- package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/resolve_update_info_from_bundles.dart +1 -1
- package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/types.dart +2 -2
- package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/src/pocketbase_bundle_mapper.dart +2 -2
- package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/src/pocketbase_database.dart +26 -15
- package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/src/pocketbase_storage.dart +1 -1
- package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_bundle_mapper.dart +2 -2
- package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_database.dart +2 -2
- package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_get_update_info.dart +2 -2
- package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_storage.dart +1 -1
- package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_types.dart +1 -1
- package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_bundle_mapper.dart +2 -2
- package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_database.dart +2 -2
- package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_edge_function_database.dart +1 -1
- package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_edge_function_storage.dart +1 -1
- package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_storage.dart +1 -1
- package/dart-src/root/lib/src/pocketbase_update_source.dart +107 -0
- package/dart-src/root/lib/src/postgres_update_source.dart +95 -0
- package/dart-src/root/lib/src/shared_update_check.dart +131 -0
- package/dart-src/root/lib/src/supabase_update_source.dart +93 -0
- package/dart-src/root/pubspec.yaml +54 -0
- package/package.json +2 -1
- package/scripts/postinstall.js +17 -13
- package/scripts/vendor.sh +22 -7
- package/dart-src/packages/core/CHANGELOG.md +0 -21
- package/dart-src/packages/core/README.md +0 -23
- package/dart-src/packages/core/example/lib/main.dart +0 -69
- package/dart-src/packages/core/pubspec.yaml +0 -13
- package/dart-src/packages/core/test/rollout_test.dart +0 -135
- package/dart-src/packages/core/test/semver_test.dart +0 -55
- package/dart-src/packages/core/test/semver_vectors.json +0 -1
- package/dart-src/packages/core/test/types_test.dart +0 -131
- package/dart-src/packages/core/test/uuid_test.dart +0 -40
- package/dart-src/packages/core/tool/dbg.dart +0 -7
- package/dart-src/plugins/aws/.gitkeep +0 -0
- package/dart-src/plugins/aws/CHANGELOG.md +0 -23
- package/dart-src/plugins/aws/LICENSE +0 -21
- package/dart-src/plugins/aws/README.md +0 -28
- package/dart-src/plugins/aws/example/lib/main.dart +0 -60
- package/dart-src/plugins/aws/pubspec.yaml +0 -30
- package/dart-src/plugins/aws/test/aws_database_test.dart +0 -161
- package/dart-src/plugins/aws/test/aws_storage_test.dart +0 -73
- package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +0 -78
- package/dart-src/plugins/cloudflare/.gitkeep +0 -0
- package/dart-src/plugins/cloudflare/CHANGELOG.md +0 -23
- package/dart-src/plugins/cloudflare/LICENSE +0 -21
- package/dart-src/plugins/cloudflare/README.md +0 -25
- package/dart-src/plugins/cloudflare/example/lib/main.dart +0 -62
- package/dart-src/plugins/cloudflare/migrations/0001_hot-updater_init.sql +0 -16
- package/dart-src/plugins/cloudflare/migrations/0002_hot-updater_0.13.0.sql +0 -8
- package/dart-src/plugins/cloudflare/migrations/0003_hot-updater_0.18.0.sql +0 -38
- package/dart-src/plugins/cloudflare/migrations/0004_hot-updater_0.29.0.sql +0 -11
- package/dart-src/plugins/cloudflare/migrations/0005_hot-updater_0.31.0.sql +0 -24
- package/dart-src/plugins/cloudflare/pubspec.yaml +0 -28
- package/dart-src/plugins/cloudflare/sql/bundles.sql +0 -42
- package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +0 -126
- package/dart-src/plugins/cloudflare/test/d1_bundle_mapper_test.dart +0 -106
- package/dart-src/plugins/cloudflare/test/d1_database_test.dart +0 -200
- package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +0 -261
- package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +0 -78
- package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +0 -74
- package/dart-src/plugins/plugin-core/CHANGELOG.md +0 -20
- package/dart-src/plugins/plugin-core/LICENSE +0 -21
- package/dart-src/plugins/plugin-core/README.md +0 -21
- package/dart-src/plugins/plugin-core/example/lib/main.dart +0 -61
- package/dart-src/plugins/plugin-core/pubspec.yaml +0 -23
- package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +0 -170
- package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_store_test.dart +0 -74
- package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +0 -345
- package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +0 -843
- package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +0 -298
- package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +0 -195
- package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +0 -56
- package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +0 -1589
- package/dart-src/plugins/plugin-core/test/helpers_test.dart +0 -283
- package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +0 -202
- package/dart-src/plugins/plugin-core/test/request_update_bundle_state_test.dart +0 -86
- package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +0 -88
- package/dart-src/plugins/pocketbase/CHANGELOG.md +0 -23
- package/dart-src/plugins/pocketbase/LICENSE +0 -21
- package/dart-src/plugins/pocketbase/README.md +0 -54
- package/dart-src/plugins/pocketbase/example/lib/main.dart +0 -62
- package/dart-src/plugins/pocketbase/pubspec.yaml +0 -26
- package/dart-src/plugins/pocketbase/test/pb_uri_test.dart +0 -12
- package/dart-src/plugins/postgres/.gitkeep +0 -0
- package/dart-src/plugins/postgres/CHANGELOG.md +0 -23
- package/dart-src/plugins/postgres/LICENSE +0 -21
- package/dart-src/plugins/postgres/README.md +0 -29
- package/dart-src/plugins/postgres/example/lib/main.dart +0 -67
- package/dart-src/plugins/postgres/pubspec.yaml +0 -26
- package/dart-src/plugins/postgres/sql/bundles.sql +0 -45
- package/dart-src/plugins/postgres/sql/get_target_app_version_list.sql +0 -21
- package/dart-src/plugins/postgres/sql/get_update_info.spec.ts +0 -171
- package/dart-src/plugins/postgres/sql/get_update_info_by_app_version.sql +0 -126
- package/dart-src/plugins/postgres/sql/get_update_info_by_fingerprint_hash.sql +0 -125
- package/dart-src/plugins/postgres/sql/hash_user_id.sql +0 -30
- package/dart-src/plugins/postgres/sql/is_device_eligible.sql +0 -237
- package/dart-src/plugins/postgres/sql/prepareSql.ts +0 -11
- package/dart-src/plugins/postgres/test/mock_postgres_client.dart +0 -320
- package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +0 -139
- package/dart-src/plugins/postgres/test/postgres_database_test.dart +0 -210
- package/dart-src/plugins/postgres/test/postgres_storage_runtime_test.dart +0 -60
- package/dart-src/plugins/supabase/CHANGELOG.md +0 -24
- package/dart-src/plugins/supabase/LICENSE +0 -21
- package/dart-src/plugins/supabase/README.md +0 -53
- package/dart-src/plugins/supabase/example/lib/main.dart +0 -63
- package/dart-src/plugins/supabase/pubspec.yaml +0 -27
- package/dart-src/plugins/supabase/supabase/edge-functions/index.ts +0 -44
- package/dart-src/plugins/supabase/supabase/edge-functions/runtime.docker.integration.spec.ts +0 -968
- package/dart-src/plugins/supabase/supabase/migrations/20250103114225_init.sql +0 -232
- package/dart-src/plugins/supabase/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +0 -137
- package/dart-src/plugins/supabase/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +0 -206
- package/dart-src/plugins/supabase/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +0 -186
- package/dart-src/plugins/supabase/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +0 -505
- package/dart-src/plugins/supabase/supabase/migrations/20260414000000_hot-updater_0.30.0.sql +0 -50
- package/dart-src/plugins/supabase/supabase/migrations/20260422000000_hot-updater_0.31.0.sql +0 -20
- package/dart-src/plugins/supabase/supabase/migrations/20260520014100_hot-updater_rls.sql +0 -75
- package/dart-src/plugins/supabase/test/mock_supabase_client.dart +0 -654
- package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +0 -115
- package/dart-src/plugins/supabase/test/supabase_database_test.dart +0 -198
- package/dart-src/plugins/supabase/test/supabase_storage_test.dart +0 -168
- /package/dart-src/{packages/core → root}/LICENSE +0 -0
- /package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/flutter_ota_kit_aws.dart +0 -0
- /package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_cloudfront_client.dart +0 -0
- /package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_cloudfront_signer.dart +0 -0
- /package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_config.dart +0 -0
- /package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_ssm_client.dart +0 -0
- /package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/flutter_ota_kit_cloudflare.dart +0 -0
- /package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_client.dart +0 -0
- /package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_config.dart +0 -0
- /package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/r2_config.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/flutter_ota_kit_core.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/app_update_info.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/bundle.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/bundle_artifacts.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/bundle_patch_artifact.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/changed_asset.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/get_bundles_args.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/metadata.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/patch_info.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/platform.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/rollout.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/semver.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/semver_range.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/semver_version.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/strategy.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/update_bundle_params.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/uuid.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/asset_storage_layout.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/bundle_helpers.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/bundle_storage_layout.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/bundle_unit_of_work.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/bundle_unit_of_work_store.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/calculate_pagination.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/compression_format.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/content_addressed_assets.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/create_database_plugin.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/create_storage_key_builder.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/create_storage_plugin.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/generate_min_bundle_id.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/is_object.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/legacy_asset_storage_layout.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/paginate_bundles.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/parse_storage_uri.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/query_bundles.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/storage_helpers.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/storage_profile.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/uuidv7.dart +0 -0
- /package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/flutter_ota_kit_pocketbase.dart +0 -0
- /package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/src/pocketbase_client.dart +0 -0
- /package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/src/pocketbase_config.dart +0 -0
- /package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/flutter_ota_kit_postgres.dart +0 -0
- /package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_client.dart +0 -0
- /package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_config.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/edge.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/flutter_ota_kit_supabase.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/error_message.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_client_adapter.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_client_http.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_config.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_signed_url_batcher.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/types.dart +0 -0
package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/BlacklistStore.kt
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
package com.flutter_patcher.flutter_patcher
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import org.json.JSONArray
|
|
6
|
+
import org.json.JSONObject
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 已知"装上就出事"的补丁本地黑名单。
|
|
10
|
+
*
|
|
11
|
+
* # 为什么需要
|
|
12
|
+
* 没有黑名单时,若服务端持续下发同一个有问题的补丁:
|
|
13
|
+
* 客户端下载 → 校验通过 → 装上 → 启动崩溃 → 自动回滚 → 下次又收到 → 死循环。
|
|
14
|
+
* 黑名单把"已经验证过的坏补丁"记下来,再次下发直接拒,连流量都不浪费。
|
|
15
|
+
*
|
|
16
|
+
* # 双键设计 (version, md5)
|
|
17
|
+
* - 只用 `version`:开发者修了 bug 后用同样 version 重发会被无脑拒绝
|
|
18
|
+
* - 只用 `md5`:可读性差,debug 时不知道是哪个补丁
|
|
19
|
+
* - 用 (version, md5) 同时作为复合键:不同 md5 视作"修了再试",允许下载
|
|
20
|
+
*
|
|
21
|
+
* # 持久性
|
|
22
|
+
* **不**因 APK 版本升级清空。否则线上事故时若服务端配置忘下架,升 APK 后又
|
|
23
|
+
* 能下到坏补丁。除非用户主动调 [clear],黑名单条目永久保留。
|
|
24
|
+
*
|
|
25
|
+
* # 容量
|
|
26
|
+
* FIFO 上限 [MAX_ENTRIES]=50,超出按时间淘汰最老条目。极端情况下避免无限增长。
|
|
27
|
+
*/
|
|
28
|
+
internal object BlacklistStore {
|
|
29
|
+
|
|
30
|
+
private const val TAG = "FlutterPatcher/Blacklist"
|
|
31
|
+
|
|
32
|
+
/** SharedPreferences key(沿用 PatcherConfig.PREFS_NAME 文件,与现有键无冲突)。*/
|
|
33
|
+
private const val KEY_BLACKLIST = "patch_blacklist"
|
|
34
|
+
|
|
35
|
+
/** 黑名单条目数上限,达到后 FIFO 淘汰最早入黑的条目。*/
|
|
36
|
+
const val MAX_ENTRIES = 50
|
|
37
|
+
|
|
38
|
+
// ---- 入黑原因常量(与 BootDiagnosticStore 命名风格保持一致)----
|
|
39
|
+
|
|
40
|
+
const val REASON_BOOT_CRASH = "BOOT_CRASH"
|
|
41
|
+
const val REASON_MD5_MISMATCH = "MD5_MISMATCH"
|
|
42
|
+
const val REASON_SIGNATURE_INVALID = "SIGNATURE_INVALID"
|
|
43
|
+
const val REASON_META_CORRUPTED = "META_CORRUPTED"
|
|
44
|
+
const val REASON_APPLY_FAILED = "APPLY_FAILED"
|
|
45
|
+
|
|
46
|
+
// ---- 字段 key ----
|
|
47
|
+
|
|
48
|
+
private const val FIELD_VERSION = "version"
|
|
49
|
+
private const val FIELD_MD5 = "md5"
|
|
50
|
+
private const val FIELD_REASON = "reason"
|
|
51
|
+
private const val FIELD_BLACKLISTED_AT = "blacklistedAt"
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 把 (version, md5) 加入黑名单。重复 add 会更新 reason + 时间戳,不会出现重复条目。
|
|
55
|
+
* version 或 md5 为空字符串则忽略(防御性,不进黑名单)。
|
|
56
|
+
*/
|
|
57
|
+
@Synchronized
|
|
58
|
+
fun add(context: Context, version: String, md5: String, reason: String) {
|
|
59
|
+
if (version.isEmpty() || md5.isEmpty()) {
|
|
60
|
+
Log.w(TAG, "skip blacklist entry: version='$version' md5='$md5'")
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
val arr = readArray(context)
|
|
64
|
+
// 找到旧条目并删除(按双键),再追加到末尾,达到"重复 add 时刷新位置 + 元数据"的语义
|
|
65
|
+
val md5Lower = md5.lowercase()
|
|
66
|
+
val filtered = JSONArray()
|
|
67
|
+
for (i in 0 until arr.length()) {
|
|
68
|
+
val obj = arr.optJSONObject(i) ?: continue
|
|
69
|
+
val v = obj.optString(FIELD_VERSION)
|
|
70
|
+
val m = obj.optString(FIELD_MD5)
|
|
71
|
+
if (v == version && m == md5Lower) continue
|
|
72
|
+
filtered.put(obj)
|
|
73
|
+
}
|
|
74
|
+
filtered.put(JSONObject().apply {
|
|
75
|
+
put(FIELD_VERSION, version)
|
|
76
|
+
put(FIELD_MD5, md5Lower)
|
|
77
|
+
put(FIELD_REASON, reason)
|
|
78
|
+
put(FIELD_BLACKLISTED_AT, System.currentTimeMillis())
|
|
79
|
+
})
|
|
80
|
+
// FIFO 淘汰:超过上限就丢最老的
|
|
81
|
+
val capped = if (filtered.length() > MAX_ENTRIES) {
|
|
82
|
+
val drop = filtered.length() - MAX_ENTRIES
|
|
83
|
+
val sliced = JSONArray()
|
|
84
|
+
for (i in drop until filtered.length()) sliced.put(filtered.opt(i))
|
|
85
|
+
sliced
|
|
86
|
+
} else {
|
|
87
|
+
filtered
|
|
88
|
+
}
|
|
89
|
+
PatcherConfig.prefs(context).edit()
|
|
90
|
+
.putString(KEY_BLACKLIST, capped.toString())
|
|
91
|
+
.commit()
|
|
92
|
+
Log.w(TAG, "blacklisted version=$version md5=$md5Lower reason=$reason (${capped.length()}/$MAX_ENTRIES)")
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 命中 (version, md5) 双键则返回 true。md5 大小写不敏感比较。*/
|
|
96
|
+
fun contains(context: Context, version: String, md5: String): Boolean {
|
|
97
|
+
if (version.isEmpty() || md5.isEmpty()) return false
|
|
98
|
+
val arr = readArray(context)
|
|
99
|
+
val md5Lower = md5.lowercase()
|
|
100
|
+
for (i in 0 until arr.length()) {
|
|
101
|
+
val obj = arr.optJSONObject(i) ?: continue
|
|
102
|
+
if (obj.optString(FIELD_VERSION) == version &&
|
|
103
|
+
obj.optString(FIELD_MD5).equals(md5Lower, ignoreCase = true)
|
|
104
|
+
) {
|
|
105
|
+
return true
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 仅以 version 维度命中黑名单。供"调用方未下发 md5"场景的下载前置检查。
|
|
113
|
+
* 同 version 任意 md5 条目都视为命中。
|
|
114
|
+
*/
|
|
115
|
+
fun containsByVersion(context: Context, version: String): Boolean {
|
|
116
|
+
if (version.isEmpty()) return false
|
|
117
|
+
val arr = readArray(context)
|
|
118
|
+
for (i in 0 until arr.length()) {
|
|
119
|
+
val obj = arr.optJSONObject(i) ?: continue
|
|
120
|
+
if (obj.optString(FIELD_VERSION) == version) return true
|
|
121
|
+
}
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 全部黑名单条目(旧→新顺序)。供 Dart 业务侧查询展示用。*/
|
|
126
|
+
fun entries(context: Context): List<Map<String, Any?>> {
|
|
127
|
+
val arr = readArray(context)
|
|
128
|
+
return List(arr.length()) { i ->
|
|
129
|
+
val obj = arr.optJSONObject(i)
|
|
130
|
+
mapOf(
|
|
131
|
+
FIELD_VERSION to obj?.optString(FIELD_VERSION),
|
|
132
|
+
FIELD_MD5 to obj?.optString(FIELD_MD5),
|
|
133
|
+
FIELD_REASON to obj?.optString(FIELD_REASON),
|
|
134
|
+
FIELD_BLACKLISTED_AT to (obj?.optLong(FIELD_BLACKLISTED_AT) ?: 0L),
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 清空整个黑名单。慎用:通常只在调试时调用。*/
|
|
140
|
+
fun clear(context: Context) {
|
|
141
|
+
PatcherConfig.prefs(context).edit().remove(KEY_BLACKLIST).commit()
|
|
142
|
+
Log.w(TAG, "blacklist cleared")
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private fun readArray(context: Context): JSONArray {
|
|
146
|
+
val raw = PatcherConfig.prefs(context).getString(KEY_BLACKLIST, null) ?: return JSONArray()
|
|
147
|
+
return try {
|
|
148
|
+
JSONArray(raw)
|
|
149
|
+
} catch (e: Exception) {
|
|
150
|
+
Log.w(TAG, "blacklist json corrupt, resetting", e)
|
|
151
|
+
JSONArray()
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
package com.flutter_patcher.flutter_patcher
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import org.json.JSONArray
|
|
6
|
+
import org.json.JSONObject
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 上次冷启动时补丁加载结果的持久化存储。
|
|
10
|
+
*
|
|
11
|
+
* 现状:补丁能否生效取决于 `CrashGuard` / `PatchManager.getValidPatchPath` /
|
|
12
|
+
* `LoaderHook.install` 三处决策,事件只写 logcat。线上业务只能事后通过
|
|
13
|
+
* [PatchManager.currentVersion] 间接推测"补丁是不是又没了",监控/告警接不上。
|
|
14
|
+
*
|
|
15
|
+
* 本对象在每次冷启动时把"补丁的加载结果 + 上下文"一次性覆写到
|
|
16
|
+
* SharedPreferences,Dart 侧通过 `FlutterPatcher.lastBootDiagnostic` 查询。
|
|
17
|
+
*
|
|
18
|
+
* # 写入时机
|
|
19
|
+
* - [FlutterPatcherApplication.attachPatcher] 三个出口(patched / hookFailed / noPatch / unknown)
|
|
20
|
+
* - [PatchManager.getValidPatchPath] 通过传入的 onDrop 闭包写入(versionCode/md5/sig/meta)
|
|
21
|
+
* - [CrashGuard.shouldLoadPatch] 通过传入的 onDrop 闭包写入(circuit breaker)
|
|
22
|
+
*
|
|
23
|
+
* # 跨边界约定
|
|
24
|
+
* status 用大写 SCREAMING_SNAKE 字符串而非 int,保证 Kotlin / Dart 两侧可读性 +
|
|
25
|
+
* 未来扩展不破坏序列化兼容。Dart 侧用 [PatchBootStatus] 枚举映射。
|
|
26
|
+
*/
|
|
27
|
+
internal object BootDiagnosticStore {
|
|
28
|
+
|
|
29
|
+
private const val TAG = "FlutterPatcher/Diag"
|
|
30
|
+
|
|
31
|
+
/** SharedPreferences key(沿用 PatcherConfig.PREFS_NAME 文件,与现有键无冲突)。*/
|
|
32
|
+
private const val KEY_LAST_BOOT_DIAG = "last_boot_diagnostic"
|
|
33
|
+
|
|
34
|
+
// ---- status 常量(与 Dart 侧 PatchBootStatus 一一对应)----
|
|
35
|
+
|
|
36
|
+
/** 未安装补丁,使用 APK 内置 libapp.so。属于正常状态。*/
|
|
37
|
+
const val NO_PATCH = "NO_PATCH"
|
|
38
|
+
|
|
39
|
+
/** 补丁加载成功,本次启动按补丁运行。*/
|
|
40
|
+
const val PATCHED = "PATCHED"
|
|
41
|
+
|
|
42
|
+
/** 补丁被丢弃:targetVersionCode 与当前 APK versionCode 不匹配。*/
|
|
43
|
+
const val DROPPED_VERSION_CODE_MISMATCH = "DROPPED_VERSION_CODE_MISMATCH"
|
|
44
|
+
|
|
45
|
+
/** 补丁被丢弃:本地 .so 文件 md5 与 meta.effectiveMd5 不一致。*/
|
|
46
|
+
const val DROPPED_MD5_MISMATCH = "DROPPED_MD5_MISMATCH"
|
|
47
|
+
|
|
48
|
+
/** 补丁被丢弃:Ed25519 签名校验失败 / 严格模式下 API < 33。*/
|
|
49
|
+
const val DROPPED_SIGNATURE_INVALID = "DROPPED_SIGNATURE_INVALID"
|
|
50
|
+
|
|
51
|
+
/** 补丁被丢弃:meta.json 损坏或缺关键字段(effectiveMd5 等)。*/
|
|
52
|
+
const val DROPPED_META_CORRUPTED = "DROPPED_META_CORRUPTED"
|
|
53
|
+
|
|
54
|
+
/** 补丁被丢弃:连续启动失败累计 >= maxCrashCount,熔断器触发。*/
|
|
55
|
+
const val DROPPED_CIRCUIT_BREAKER = "DROPPED_CIRCUIT_BREAKER"
|
|
56
|
+
|
|
57
|
+
/** 补丁文件保留,但反射替换 FlutterLoader 失败,本次启动用了内置 .so。*/
|
|
58
|
+
const val HOOK_INSTALL_FAILED = "HOOK_INSTALL_FAILED"
|
|
59
|
+
|
|
60
|
+
/** attachPatcher 阶段抛出未分类异常。*/
|
|
61
|
+
const val UNKNOWN = "UNKNOWN"
|
|
62
|
+
|
|
63
|
+
// ---- 字段 key(写入 / 读取共用)----
|
|
64
|
+
|
|
65
|
+
private const val FIELD_STATUS = "status"
|
|
66
|
+
private const val FIELD_PATCH_VERSION = "patchVersion"
|
|
67
|
+
private const val FIELD_PATCH_TARGET_VC = "patchTargetVersionCode"
|
|
68
|
+
private const val FIELD_APP_VC = "appVersionCode"
|
|
69
|
+
private const val FIELD_CRASH_COUNT = "crashCount"
|
|
70
|
+
private const val FIELD_ATTEMPTED_LOADER_FIELDS = "attemptedLoaderFields"
|
|
71
|
+
private const val FIELD_MESSAGE = "message"
|
|
72
|
+
private const val FIELD_RECORDED_AT = "recordedAt"
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 覆写一条诊断记录。
|
|
76
|
+
*
|
|
77
|
+
* 使用 commit 同步写入:本方法常在 attachBaseContext 阶段调用,若进程随后
|
|
78
|
+
* 因补丁问题崩溃,commit 能保证下次启动 Dart 侧仍能查询到本次的诊断结果。
|
|
79
|
+
*/
|
|
80
|
+
fun record(
|
|
81
|
+
context: Context,
|
|
82
|
+
status: String,
|
|
83
|
+
patchVersion: String? = null,
|
|
84
|
+
patchTargetVersionCode: Long? = null,
|
|
85
|
+
appVersionCode: Long? = null,
|
|
86
|
+
crashCount: Int? = null,
|
|
87
|
+
attemptedLoaderFields: List<String>? = null,
|
|
88
|
+
message: String? = null,
|
|
89
|
+
) {
|
|
90
|
+
val json = JSONObject().apply {
|
|
91
|
+
put(FIELD_STATUS, status)
|
|
92
|
+
putOpt(FIELD_PATCH_VERSION, patchVersion)
|
|
93
|
+
putOpt(FIELD_PATCH_TARGET_VC, patchTargetVersionCode)
|
|
94
|
+
putOpt(FIELD_APP_VC, appVersionCode)
|
|
95
|
+
putOpt(FIELD_CRASH_COUNT, crashCount)
|
|
96
|
+
if (attemptedLoaderFields != null) {
|
|
97
|
+
put(FIELD_ATTEMPTED_LOADER_FIELDS, JSONArray(attemptedLoaderFields))
|
|
98
|
+
}
|
|
99
|
+
putOpt(FIELD_MESSAGE, message)
|
|
100
|
+
put(FIELD_RECORDED_AT, System.currentTimeMillis())
|
|
101
|
+
}
|
|
102
|
+
PatcherConfig.prefs(context).edit()
|
|
103
|
+
.putString(KEY_LAST_BOOT_DIAG, json.toString())
|
|
104
|
+
.commit()
|
|
105
|
+
Log.d(TAG, "record status=$status patch=$patchVersion msg=$message")
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 读取最近一次 record。
|
|
110
|
+
*
|
|
111
|
+
* @return 诊断 Map,可直接交给 MethodChannel 发到 Dart 侧;
|
|
112
|
+
* null 表示从未 record 过(首次安装或 pm clear 后第一次启动且尚未到首帧)。
|
|
113
|
+
*/
|
|
114
|
+
fun read(context: Context): Map<String, Any?>? {
|
|
115
|
+
val raw = PatcherConfig.prefs(context).getString(KEY_LAST_BOOT_DIAG, null) ?: return null
|
|
116
|
+
return try {
|
|
117
|
+
val json = JSONObject(raw)
|
|
118
|
+
val map = mutableMapOf<String, Any?>()
|
|
119
|
+
map[FIELD_STATUS] = json.optString(FIELD_STATUS, UNKNOWN)
|
|
120
|
+
map[FIELD_PATCH_VERSION] = json.optStringOrNull(FIELD_PATCH_VERSION)
|
|
121
|
+
map[FIELD_PATCH_TARGET_VC] = json.optLongOrNull(FIELD_PATCH_TARGET_VC)
|
|
122
|
+
map[FIELD_APP_VC] = json.optLongOrNull(FIELD_APP_VC)
|
|
123
|
+
map[FIELD_CRASH_COUNT] = json.optIntOrNull(FIELD_CRASH_COUNT)
|
|
124
|
+
map[FIELD_ATTEMPTED_LOADER_FIELDS] = json.optStringListOrNull(FIELD_ATTEMPTED_LOADER_FIELDS)
|
|
125
|
+
map[FIELD_MESSAGE] = json.optStringOrNull(FIELD_MESSAGE)
|
|
126
|
+
map[FIELD_RECORDED_AT] = json.optLong(FIELD_RECORDED_AT, 0L)
|
|
127
|
+
map
|
|
128
|
+
} catch (e: Exception) {
|
|
129
|
+
Log.w(TAG, "read failed, raw=$raw", e)
|
|
130
|
+
null
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---- 内部工具:JSONObject 缺字段时返回 null 而非 "" / 0 ----
|
|
135
|
+
|
|
136
|
+
private fun JSONObject.optStringOrNull(key: String): String? =
|
|
137
|
+
if (isNull(key) || !has(key)) null else optString(key, "").ifEmpty { null }
|
|
138
|
+
|
|
139
|
+
private fun JSONObject.optLongOrNull(key: String): Long? =
|
|
140
|
+
if (isNull(key) || !has(key)) null else optLong(key)
|
|
141
|
+
|
|
142
|
+
private fun JSONObject.optIntOrNull(key: String): Int? =
|
|
143
|
+
if (isNull(key) || !has(key)) null else optInt(key)
|
|
144
|
+
|
|
145
|
+
private fun JSONObject.optStringListOrNull(key: String): List<String>? {
|
|
146
|
+
if (isNull(key) || !has(key)) return null
|
|
147
|
+
val arr = optJSONArray(key) ?: return null
|
|
148
|
+
return List(arr.length()) { arr.optString(it) }
|
|
149
|
+
}
|
|
150
|
+
}
|
package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/CrashGuard.kt
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
package com.flutter_patcher.flutter_patcher
|
|
2
|
+
|
|
3
|
+
import android.app.ActivityManager
|
|
4
|
+
import android.app.ApplicationExitInfo
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.os.Build
|
|
7
|
+
import android.os.Process
|
|
8
|
+
import android.util.Log
|
|
9
|
+
import java.io.File
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 熔断器:防止补丁导致启动崩溃时无限重启。
|
|
13
|
+
*
|
|
14
|
+
* 状态机(解耦版):
|
|
15
|
+
* 1. Application.attachBaseContext → [markBooting] 写 patch_loading=true(commit 同步)+ pid
|
|
16
|
+
* 2. Dart 首帧 → [markBootSuccess] 立即清 patch_loading=false + crash_count=0;
|
|
17
|
+
* **保留** last_booting_pid 给下次冷启动的 ExitInfo 查询
|
|
18
|
+
* 3. Dart 首帧 + verifyAfter 秒 → 卸载 Dart 错误钩子(仅 Dart 层动作,不触原生)
|
|
19
|
+
*
|
|
20
|
+
* 下次冷启动 [shouldLoadPatch] 综合判定:
|
|
21
|
+
*
|
|
22
|
+
* - **API 30+ 且有 pid 记录**:通过 [ActivityManager.getHistoricalProcessExitReasons]
|
|
23
|
+
* 拿到精确死因。覆盖首帧前后所有崩溃。只有 [ApplicationExitInfo.REASON_CRASH]
|
|
24
|
+
* / [ApplicationExitInfo.REASON_CRASH_NATIVE] / [ApplicationExitInfo.REASON_ANR]
|
|
25
|
+
* / [ApplicationExitInfo.REASON_INITIALIZATION_FAILURE] 计入 crash_count;
|
|
26
|
+
* 用户从最近任务划掉、系统 OOM 等不计入。
|
|
27
|
+
* - **API < 30 或 ExitInfo 无记录**:退回到 patch_loading 兜底——
|
|
28
|
+
* patch_loading=true(首帧前死了)→ 视为崩溃;
|
|
29
|
+
* patch_loading=false(首帧后死的)→ 不计崩溃。
|
|
30
|
+
*
|
|
31
|
+
* API < 30 因此存在已知盲区:补丁加载首帧后才发生的 native crash / ANR 不会被
|
|
32
|
+
* 检测(Dart 层异常仍由 verifyAfter 窗口内的 Dart 错误钩子捕获)。覆盖率约
|
|
33
|
+
* 5–10% 的存量长尾设备,业务侧若无法接受可显式调高 `maxCrashCount`。
|
|
34
|
+
*
|
|
35
|
+
* 当 crash_count 累计 >= [PatcherConfig.maxCrashCount] 次,自动删除补丁并拒绝加载。
|
|
36
|
+
*/
|
|
37
|
+
internal class CrashGuard(private val context: Context) {
|
|
38
|
+
|
|
39
|
+
companion object {
|
|
40
|
+
private const val TAG = "FlutterPatcher/Guard"
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Pure decision: should a real crash reason be charged against the patch?
|
|
44
|
+
*
|
|
45
|
+
* Only crashes within [PatcherConfig.BOOT_CRASH_WINDOW_MS] of boot start
|
|
46
|
+
* count as patch-boot failures. A crash long after a healthy boot is a
|
|
47
|
+
* runtime failure and must NOT trip the breaker (that was the bug causing
|
|
48
|
+
* a working OTA to silently revert hours later).
|
|
49
|
+
*
|
|
50
|
+
* When [bootStartedAt] or [crashTimestamp] is unknown (<= 0) we cannot
|
|
51
|
+
* bound it, so we conservatively charge the patch (preserves the old
|
|
52
|
+
* fail-fast behavior for that edge).
|
|
53
|
+
*/
|
|
54
|
+
@JvmStatic
|
|
55
|
+
fun isWithinBootWindow(
|
|
56
|
+
bootStartedAt: Long,
|
|
57
|
+
crashTimestamp: Long,
|
|
58
|
+
windowMs: Long = PatcherConfig.BOOT_CRASH_WINDOW_MS,
|
|
59
|
+
): Boolean {
|
|
60
|
+
if (bootStartedAt <= 0L || crashTimestamp <= 0L) return true
|
|
61
|
+
val elapsed = crashTimestamp - bootStartedAt
|
|
62
|
+
// Negative elapsed (clock skew / stale record) → treat as in-window.
|
|
63
|
+
if (elapsed < 0L) return true
|
|
64
|
+
return elapsed <= windowMs
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private val sp = PatcherConfig.prefs(context)
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 启动开始时综合判断是否加载补丁。
|
|
72
|
+
*
|
|
73
|
+
* @param onTrip 当熔断器**本次**触发并丢弃补丁时调用,参数为触发时的真实
|
|
74
|
+
* crash_count(删除前)。供 [BootDiagnosticStore] / [BlacklistStore] 上报使用。
|
|
75
|
+
*/
|
|
76
|
+
fun shouldLoadPatch(onTrip: ((crashCount: Int) -> Unit)? = null): Boolean {
|
|
77
|
+
val threshold = PatcherConfig.maxCrashCount(context)
|
|
78
|
+
val patchLoading = sp.getBoolean(PatcherConfig.KEY_PATCH_LOADING, false)
|
|
79
|
+
val lastPid = sp.getInt(PatcherConfig.KEY_LAST_BOOTING_PID, -1)
|
|
80
|
+
val bootStartedAt = sp.getLong(PatcherConfig.KEY_BOOT_STARTED_AT, 0L)
|
|
81
|
+
|
|
82
|
+
val verdict = classifyPreviousSession(patchLoading, lastPid, bootStartedAt)
|
|
83
|
+
if (verdict.isCrash) {
|
|
84
|
+
val count = recordCrashAndMaybeTrip(verdict.reasonName, onTrip)
|
|
85
|
+
if (count >= threshold) return false
|
|
86
|
+
} else if (patchLoading || lastPid > 0) {
|
|
87
|
+
// 非崩溃但状态未清:清掉以免下次冷启动重复处理同一会话
|
|
88
|
+
sp.edit()
|
|
89
|
+
.putBoolean(PatcherConfig.KEY_PATCH_LOADING, false)
|
|
90
|
+
.remove(PatcherConfig.KEY_LAST_BOOTING_PID)
|
|
91
|
+
.remove(PatcherConfig.KEY_BOOT_STARTED_AT)
|
|
92
|
+
.commit()
|
|
93
|
+
Log.i(TAG, "previous session ended without crash (${verdict.reasonName})")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return sp.getInt(PatcherConfig.KEY_CRASH_COUNT, 0) < threshold
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 由 Dart 侧通过 MethodChannel 调用:上报一次"补丁加载阶段 Dart 层未捕获异常"。
|
|
101
|
+
*
|
|
102
|
+
* 这种崩溃不算系统层 [ApplicationExitInfo.REASON_CRASH] —— PlatformDispatcher
|
|
103
|
+
* 把异常吞了,进程没死,下次冷启动 ExitInfo 看到的是 USER_REQUESTED 之类的非
|
|
104
|
+
* 崩溃原因。所以需要 Dart 主动汇报,语义等同于一次真崩溃:crash_count += 1,
|
|
105
|
+
* 达到阈值则熔断 + 删补丁 + 黑名单。
|
|
106
|
+
*
|
|
107
|
+
* @param message 异常字符串(透传给日志 / 诊断展示,不做任何业务逻辑判断)
|
|
108
|
+
* @param onTrip 与 [shouldLoadPatch] 同款:触发熔断时调用,上报黑名单 / 诊断
|
|
109
|
+
*/
|
|
110
|
+
fun reportDartBootError(message: String?, onTrip: ((crashCount: Int) -> Unit)? = null) {
|
|
111
|
+
recordCrashAndMaybeTrip(message ?: "no msg", onTrip)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 增计 crash_count、清 patch_loading、必要时触发熔断 + 删补丁。供
|
|
116
|
+
* [shouldLoadPatch] 与 [reportDartBootError] 共用,统一日志格式。
|
|
117
|
+
*
|
|
118
|
+
* @param reason 错误原因(ExitInfo reason name / Dart 异常 toString),仅用于日志
|
|
119
|
+
* @return 累加后的 crash_count(删补丁前的真实值)
|
|
120
|
+
*/
|
|
121
|
+
private fun recordCrashAndMaybeTrip(
|
|
122
|
+
reason: String,
|
|
123
|
+
onTrip: ((crashCount: Int) -> Unit)?,
|
|
124
|
+
): Int {
|
|
125
|
+
val threshold = PatcherConfig.maxCrashCount(context)
|
|
126
|
+
val count = sp.getInt(PatcherConfig.KEY_CRASH_COUNT, 0) + 1
|
|
127
|
+
sp.edit()
|
|
128
|
+
.putInt(PatcherConfig.KEY_CRASH_COUNT, count)
|
|
129
|
+
.putBoolean(PatcherConfig.KEY_PATCH_LOADING, false)
|
|
130
|
+
.commit()
|
|
131
|
+
Log.w(TAG, "patch boot failure recorded ($reason), crash_count=$count")
|
|
132
|
+
if (count >= threshold) {
|
|
133
|
+
Log.w(TAG, "circuit tripped! $count consecutive failures, dropping patch")
|
|
134
|
+
onTrip?.invoke(count)
|
|
135
|
+
deletePatchFiles()
|
|
136
|
+
}
|
|
137
|
+
return count
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 在 Application.attachBaseContext 内调用。commit 同步写入,确保进程崩溃前状态已持久化。 */
|
|
141
|
+
fun markBooting() {
|
|
142
|
+
sp.edit()
|
|
143
|
+
.putBoolean(PatcherConfig.KEY_PATCH_LOADING, true)
|
|
144
|
+
.putInt(PatcherConfig.KEY_LAST_BOOTING_PID, Process.myPid())
|
|
145
|
+
.putLong(PatcherConfig.KEY_BOOT_STARTED_AT, System.currentTimeMillis())
|
|
146
|
+
.commit()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Dart 首帧渲染完成:补丁可信,立即清 patch_loading + crash_count。
|
|
151
|
+
*
|
|
152
|
+
* 注意:故意 **不清** [PatcherConfig.KEY_LAST_BOOTING_PID] / [PatcherConfig.KEY_BOOT_STARTED_AT]。
|
|
153
|
+
* 下次冷启动 [shouldLoadPatch] 仍可用这个 pid 查 [ApplicationExitInfo],从而捕捉首帧
|
|
154
|
+
* 后短时间内([PatcherConfig.BOOT_CRASH_WINDOW_MS] 以内)才发生的 native crash / ANR —— 那一刻
|
|
155
|
+
* patch_loading 已清,不能再作为崩溃信号。pid + 时间戳在 [shouldLoadPatch]
|
|
156
|
+
* 处理完一次会话后清除。
|
|
157
|
+
*/
|
|
158
|
+
fun markBootSuccess() {
|
|
159
|
+
sp.edit()
|
|
160
|
+
.putBoolean(PatcherConfig.KEY_PATCH_LOADING, false)
|
|
161
|
+
.putInt(PatcherConfig.KEY_CRASH_COUNT, 0)
|
|
162
|
+
.commit()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** 清零所有熔断状态(配合补丁安装/回滚调用)。 */
|
|
166
|
+
fun reset() {
|
|
167
|
+
sp.edit()
|
|
168
|
+
.putBoolean(PatcherConfig.KEY_PATCH_LOADING, false)
|
|
169
|
+
.putInt(PatcherConfig.KEY_CRASH_COUNT, 0)
|
|
170
|
+
.remove(PatcherConfig.KEY_LAST_BOOTING_PID)
|
|
171
|
+
.remove(PatcherConfig.KEY_BOOT_STARTED_AT)
|
|
172
|
+
.apply()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 综合 [patchLoading] 与 [lastPid] 判定上次会话是否崩溃。
|
|
177
|
+
*
|
|
178
|
+
* - API 30+ 且有 pid 记录 → 优先用 [ApplicationExitInfo] 精确分类。**但只把发生在
|
|
179
|
+
* 启动窗口内(距 [bootStartedAt] 不超过 [PatcherConfig.BOOT_CRASH_WINDOW_MS])的
|
|
180
|
+
* 崩溃计入补丁失败**:首帧后长时间运行才发生的 crash/ANR 属于正常运行期故障,
|
|
181
|
+
* 与补丁无关,不应触发熔断回滚。用户主动关闭 / 系统 OOM 等也不计崩溃。
|
|
182
|
+
* - API < 30 或 ExitInfo 拿不到记录 → 退回到 [patchLoading] 兜底信号。
|
|
183
|
+
* patchLoading=true(首帧前死了)→ 崩溃;patchLoading=false(首帧后死的,
|
|
184
|
+
* 或本就没启动过)→ 非崩溃。
|
|
185
|
+
*/
|
|
186
|
+
private fun classifyPreviousSession(
|
|
187
|
+
patchLoading: Boolean,
|
|
188
|
+
lastPid: Int,
|
|
189
|
+
bootStartedAt: Long,
|
|
190
|
+
): ExitVerdict {
|
|
191
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && lastPid > 0) {
|
|
192
|
+
val record = queryExitInfo(lastPid)
|
|
193
|
+
if (record != null) {
|
|
194
|
+
val isCrashReason = when (record.reason) {
|
|
195
|
+
ApplicationExitInfo.REASON_CRASH,
|
|
196
|
+
ApplicationExitInfo.REASON_CRASH_NATIVE,
|
|
197
|
+
ApplicationExitInfo.REASON_ANR,
|
|
198
|
+
ApplicationExitInfo.REASON_INITIALIZATION_FAILURE -> true
|
|
199
|
+
else -> false
|
|
200
|
+
}
|
|
201
|
+
val reasonName = reasonNameApi30(record.reason)
|
|
202
|
+
if (!isCrashReason) {
|
|
203
|
+
return ExitVerdict(false, reasonName)
|
|
204
|
+
}
|
|
205
|
+
// A real crash reason — but only charge it against the patch if it
|
|
206
|
+
// happened close to boot. A crash hours into a healthy session is a
|
|
207
|
+
// runtime bug, not a bad patch; counting it would revert a working OTA.
|
|
208
|
+
if (!isWithinBootWindow(bootStartedAt, record.timestamp)) {
|
|
209
|
+
Log.i(
|
|
210
|
+
TAG,
|
|
211
|
+
"crash ($reasonName) at ts=${record.timestamp} is outside the " +
|
|
212
|
+
"${PatcherConfig.BOOT_CRASH_WINDOW_MS}ms boot window " +
|
|
213
|
+
"(bootStartedAt=$bootStartedAt); treating as runtime " +
|
|
214
|
+
"failure, not charging the patch"
|
|
215
|
+
)
|
|
216
|
+
return ExitVerdict(false, "${reasonName}_LATE")
|
|
217
|
+
}
|
|
218
|
+
return ExitVerdict(true, reasonName)
|
|
219
|
+
}
|
|
220
|
+
// ExitInfo 队列已被新进程挤出(罕见):退回到 patchLoading 兜底
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return if (patchLoading) {
|
|
224
|
+
ExitVerdict(true, "NO_FIRST_FRAME")
|
|
225
|
+
} else {
|
|
226
|
+
ExitVerdict(false, "NO_RECORD")
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private fun queryExitInfo(pid: Int): ApplicationExitInfo? {
|
|
231
|
+
val am = context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
|
|
232
|
+
return try {
|
|
233
|
+
am?.getHistoricalProcessExitReasons(context.packageName, pid, 1)?.firstOrNull()
|
|
234
|
+
} catch (e: Throwable) {
|
|
235
|
+
Log.w(TAG, "getHistoricalProcessExitReasons failed", e)
|
|
236
|
+
null
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private fun deletePatchFiles() {
|
|
241
|
+
val dir = File(context.filesDir, PatcherConfig.PATCH_DIR)
|
|
242
|
+
if (dir.exists()) dir.deleteRecursively()
|
|
243
|
+
sp.edit()
|
|
244
|
+
.putInt(PatcherConfig.KEY_CRASH_COUNT, 0)
|
|
245
|
+
.putBoolean(PatcherConfig.KEY_PATCH_LOADING, false)
|
|
246
|
+
.remove(PatcherConfig.KEY_LAST_BOOTING_PID)
|
|
247
|
+
.remove(PatcherConfig.KEY_BOOT_STARTED_AT)
|
|
248
|
+
.commit()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private data class ExitVerdict(val isCrash: Boolean, val reasonName: String)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** 把 [ApplicationExitInfo] reason 整数翻成可读名,仅在 API 30+ 调用。 */
|
|
255
|
+
private fun reasonNameApi30(reason: Int): String = when (reason) {
|
|
256
|
+
ApplicationExitInfo.REASON_UNKNOWN -> "UNKNOWN"
|
|
257
|
+
ApplicationExitInfo.REASON_EXIT_SELF -> "EXIT_SELF"
|
|
258
|
+
ApplicationExitInfo.REASON_SIGNALED -> "SIGNALED"
|
|
259
|
+
ApplicationExitInfo.REASON_LOW_MEMORY -> "LOW_MEMORY"
|
|
260
|
+
ApplicationExitInfo.REASON_CRASH -> "CRASH"
|
|
261
|
+
ApplicationExitInfo.REASON_CRASH_NATIVE -> "CRASH_NATIVE"
|
|
262
|
+
ApplicationExitInfo.REASON_ANR -> "ANR"
|
|
263
|
+
ApplicationExitInfo.REASON_INITIALIZATION_FAILURE -> "INITIALIZATION_FAILURE"
|
|
264
|
+
ApplicationExitInfo.REASON_PERMISSION_CHANGE -> "PERMISSION_CHANGE"
|
|
265
|
+
ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE -> "EXCESSIVE_RESOURCE_USAGE"
|
|
266
|
+
ApplicationExitInfo.REASON_USER_REQUESTED -> "USER_REQUESTED"
|
|
267
|
+
ApplicationExitInfo.REASON_USER_STOPPED -> "USER_STOPPED"
|
|
268
|
+
ApplicationExitInfo.REASON_DEPENDENCY_DIED -> "DEPENDENCY_DIED"
|
|
269
|
+
ApplicationExitInfo.REASON_OTHER -> "OTHER"
|
|
270
|
+
else -> "REASON_$reason"
|
|
271
|
+
}
|