@_nazmiforreal/flutter-ota 0.1.26 → 0.1.28
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/README.md +112 -18
- package/bin/flutter-ota-linux-x64 +0 -0
- package/bin/flutter-ota.js +12 -11
- package/dart-src/packages/cli-tools/README.md +146 -2
- 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/init.dart +36 -8
- 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 +334 -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 +1186 -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 +1 -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/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
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
package com.flutter_patcher.flutter_patcher
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import io.flutter.app.FlutterApplication
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 宿主 App 需在 AndroidManifest.xml 中使用这个 Application(或在自己的 Application
|
|
9
|
+
* 里调 [attachPatcher])。
|
|
10
|
+
*
|
|
11
|
+
* 启动流程:
|
|
12
|
+
* 1. attachBaseContext
|
|
13
|
+
* → CrashGuard.shouldLoadPatch() 熔断检查(API 30+ 走 ApplicationExitInfo
|
|
14
|
+
* 精确识别上次进程是真崩溃还是被用户/系统结束;API < 30 走朴素策略:
|
|
15
|
+
* patch_loading=true 即视为一次崩溃)
|
|
16
|
+
* → PatchManager.getValidPatchPath() 磁盘补丁验签
|
|
17
|
+
* → LoaderHook.install() 反射替换 FlutterLoader
|
|
18
|
+
* → CrashGuard.markBooting() 标记「启动中」并记录 pid(API 30+ 用于 ExitInfo 查询)
|
|
19
|
+
* 2. Dart 首帧 → MethodChannel("flutter_ota_kit#reportBootSuccess")
|
|
20
|
+
* → CrashGuard.markBootSuccess() 重置计数
|
|
21
|
+
*
|
|
22
|
+
* API 30+ 上,用户主动从最近任务划掉 / OOM kill / 强停等「非崩溃退出」不计入
|
|
23
|
+
* crash_count,只有真崩溃(CRASH / CRASH_NATIVE / ANR / INITIALIZATION_FAILURE)
|
|
24
|
+
* 才会触发熔断。API < 30 没有这层分类,对长尾设备做 fail-fast 取舍。
|
|
25
|
+
*
|
|
26
|
+
* 用户如果已经有自己的 Application 基类,直接把 [attachPatcher] 搬过去即可,
|
|
27
|
+
* 不需要继承本类。
|
|
28
|
+
*/
|
|
29
|
+
open class FlutterPatcherApplication : FlutterApplication() {
|
|
30
|
+
|
|
31
|
+
companion object {
|
|
32
|
+
private const val TAG = "FlutterPatcher/App"
|
|
33
|
+
|
|
34
|
+
@Volatile
|
|
35
|
+
private var attachAttempted = false
|
|
36
|
+
|
|
37
|
+
@Volatile
|
|
38
|
+
private var attachResult = false
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 在 Application.attachBaseContext(base) 中、super 调用之后调用。
|
|
42
|
+
* 返回补丁是否成功注入。
|
|
43
|
+
*/
|
|
44
|
+
@JvmStatic
|
|
45
|
+
@Synchronized
|
|
46
|
+
fun attachPatcher(context: Context): Boolean {
|
|
47
|
+
if (attachAttempted) {
|
|
48
|
+
Log.d(TAG, "attachPatcher already ran in this process, result=$attachResult")
|
|
49
|
+
return attachResult
|
|
50
|
+
}
|
|
51
|
+
attachAttempted = true
|
|
52
|
+
attachResult = doAttachPatcher(context)
|
|
53
|
+
return attachResult
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private fun doAttachPatcher(context: Context): Boolean {
|
|
57
|
+
// 本次启动是否已往 BootDiagnosticStore 写过精准 status。
|
|
58
|
+
// 用于在收尾阶段判断要不要兜底成 NO_PATCH(避免覆盖更精准的 reason)。
|
|
59
|
+
var diagRecorded = false
|
|
60
|
+
val appVc = PatcherConfig.currentVersionCode(context)
|
|
61
|
+
|
|
62
|
+
return try {
|
|
63
|
+
// 在熔断/校验丢弃前预读 (version, downloadMd5),等会儿入黑名单要用。
|
|
64
|
+
// 即便文件后续被删,这里捕获的字符串仍有效。
|
|
65
|
+
val patchManager = PatchManager(context)
|
|
66
|
+
val crashedMeta = patchManager.currentMeta()
|
|
67
|
+
|
|
68
|
+
val guard = CrashGuard(context)
|
|
69
|
+
val canLoad = guard.shouldLoadPatch { crashCount ->
|
|
70
|
+
// 熔断触发 ⇒ 这个补丁连续没活到 verified,加入黑名单防止重试。
|
|
71
|
+
if (crashedMeta != null) {
|
|
72
|
+
BlacklistStore.add(
|
|
73
|
+
context,
|
|
74
|
+
crashedMeta.first,
|
|
75
|
+
crashedMeta.second,
|
|
76
|
+
BlacklistStore.REASON_BOOT_CRASH,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
BootDiagnosticStore.record(
|
|
80
|
+
context = context,
|
|
81
|
+
status = BootDiagnosticStore.DROPPED_CIRCUIT_BREAKER,
|
|
82
|
+
patchVersion = crashedMeta?.first,
|
|
83
|
+
appVersionCode = appVc,
|
|
84
|
+
crashCount = crashCount,
|
|
85
|
+
message = if (crashedMeta != null)
|
|
86
|
+
"$crashCount boot failure(s); blacklisted (version=${crashedMeta.first})"
|
|
87
|
+
else
|
|
88
|
+
"$crashCount boot failure(s)",
|
|
89
|
+
)
|
|
90
|
+
diagRecorded = true
|
|
91
|
+
}
|
|
92
|
+
if (!canLoad) {
|
|
93
|
+
Log.w(TAG, "circuit breaker tripped, skip patch")
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
val patch = patchManager.getValidPatch { status, version, extras ->
|
|
98
|
+
// md5 / signature 失败 = 强烈"补丁有问题"信号,连带入黑名单。
|
|
99
|
+
// meta_corrupted / version_code_mismatch 不入黑名单(前者 key 不全,后者
|
|
100
|
+
// 属于正常 APK 升级而非补丁本身有问题)。
|
|
101
|
+
val blacklistMd5 = extras["blacklistMd5"] as? String
|
|
102
|
+
val blacklistReason = when (status) {
|
|
103
|
+
BootDiagnosticStore.DROPPED_MD5_MISMATCH ->
|
|
104
|
+
BlacklistStore.REASON_MD5_MISMATCH
|
|
105
|
+
BootDiagnosticStore.DROPPED_SIGNATURE_INVALID ->
|
|
106
|
+
BlacklistStore.REASON_SIGNATURE_INVALID
|
|
107
|
+
else -> null
|
|
108
|
+
}
|
|
109
|
+
if (blacklistReason != null && version != null && !blacklistMd5.isNullOrEmpty()) {
|
|
110
|
+
BlacklistStore.add(context, version, blacklistMd5, blacklistReason)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
BootDiagnosticStore.record(
|
|
114
|
+
context = context,
|
|
115
|
+
status = status,
|
|
116
|
+
patchVersion = version,
|
|
117
|
+
patchTargetVersionCode = extras["patchTargetVersionCode"] as? Long,
|
|
118
|
+
appVersionCode = (extras["appVersionCode"] as? Long) ?: appVc,
|
|
119
|
+
message = extras["message"] as? String,
|
|
120
|
+
)
|
|
121
|
+
diagRecorded = true
|
|
122
|
+
}
|
|
123
|
+
if (patch == null) {
|
|
124
|
+
Log.d(TAG, "no usable patch, boot with built-in libapp.so")
|
|
125
|
+
if (!diagRecorded) {
|
|
126
|
+
// DROPPED_* 是用户最关心的"上次为啥被丢弃"信息,不应被这次
|
|
127
|
+
// 无信息量的 NO_PATCH 覆盖。典型场景:上一次冷启动里 Dart 错误
|
|
128
|
+
// 钩子把补丁判崩 + record(DROPPED_CIRCUIT_BREAKER) + 删补丁,
|
|
129
|
+
// 但进程没死、UI 没建出来;用户划掉重开后这一次进入本分支,
|
|
130
|
+
// 写 NO_PATCH 会让 DiagCard 看不到 trip 信息。
|
|
131
|
+
// 后续装上新补丁时,line 127 会写 PATCHED 自然覆盖,sticky 自动解除。
|
|
132
|
+
val existing = BootDiagnosticStore.read(context)
|
|
133
|
+
val existingStatus = existing?.get("status") as? String
|
|
134
|
+
val shouldPreserve = existingStatus != null &&
|
|
135
|
+
existingStatus.startsWith("DROPPED_")
|
|
136
|
+
if (!shouldPreserve) {
|
|
137
|
+
BootDiagnosticStore.record(
|
|
138
|
+
context = context,
|
|
139
|
+
status = BootDiagnosticStore.NO_PATCH,
|
|
140
|
+
appVersionCode = appVc,
|
|
141
|
+
)
|
|
142
|
+
} else {
|
|
143
|
+
Log.d(TAG, "preserved existing $existingStatus over NO_PATCH")
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return false
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 走到这里说明补丁已通过校验。预先取版本号,方便后面 record。
|
|
150
|
+
val patchVersion = patchManager.currentVersion().ifEmpty { null }
|
|
151
|
+
|
|
152
|
+
// 标记「启动中」——必须 commit 同步写入,确保进程崩溃前状态已持久化
|
|
153
|
+
guard.markBooting()
|
|
154
|
+
|
|
155
|
+
val attemptedFields = mutableListOf<String>()
|
|
156
|
+
val ok = LoaderHook.install(
|
|
157
|
+
context,
|
|
158
|
+
patch.soPath,
|
|
159
|
+
patch.assetsBundlePath,
|
|
160
|
+
patch.assetsArchivePath,
|
|
161
|
+
attemptedFields
|
|
162
|
+
)
|
|
163
|
+
if (ok) {
|
|
164
|
+
BootDiagnosticStore.record(
|
|
165
|
+
context = context,
|
|
166
|
+
status = BootDiagnosticStore.PATCHED,
|
|
167
|
+
patchVersion = patchVersion,
|
|
168
|
+
appVersionCode = appVc,
|
|
169
|
+
)
|
|
170
|
+
} else {
|
|
171
|
+
// 注入失败:本次启动根本没换 libapp.so,不视为一次崩溃
|
|
172
|
+
guard.reset()
|
|
173
|
+
BootDiagnosticStore.record(
|
|
174
|
+
context = context,
|
|
175
|
+
status = BootDiagnosticStore.HOOK_INSTALL_FAILED,
|
|
176
|
+
patchVersion = patchVersion,
|
|
177
|
+
appVersionCode = appVc,
|
|
178
|
+
attemptedLoaderFields = attemptedFields.toList(),
|
|
179
|
+
message = "FlutterLoader reflection failed; patch retained",
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
ok
|
|
183
|
+
} catch (e: Exception) {
|
|
184
|
+
Log.e(TAG, "attachPatcher failed, fallback to built-in", e)
|
|
185
|
+
CrashGuard(context).reset()
|
|
186
|
+
BootDiagnosticStore.record(
|
|
187
|
+
context = context,
|
|
188
|
+
status = BootDiagnosticStore.UNKNOWN,
|
|
189
|
+
appVersionCode = appVc,
|
|
190
|
+
message = e.message ?: e.javaClass.simpleName,
|
|
191
|
+
)
|
|
192
|
+
false
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
override fun attachBaseContext(base: Context) {
|
|
198
|
+
super.attachBaseContext(base)
|
|
199
|
+
attachPatcher(this)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
package com.flutter_patcher.flutter_patcher
|
|
2
|
+
|
|
3
|
+
import android.content.ContentProvider
|
|
4
|
+
import android.content.ContentValues
|
|
5
|
+
import android.database.Cursor
|
|
6
|
+
import android.net.Uri
|
|
7
|
+
import android.util.Log
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 自动初始化 ContentProvider —— 宿主无需改 Application 即可挂上补丁加载。
|
|
11
|
+
*
|
|
12
|
+
* # 生命周期
|
|
13
|
+
* Android `ActivityThread.handleBindApplication` 的固定顺序:
|
|
14
|
+
*
|
|
15
|
+
* ```
|
|
16
|
+
* Application.attachBaseContext()
|
|
17
|
+
* ↓
|
|
18
|
+
* installContentProviders() ← 本 provider.onCreate 在这里触发
|
|
19
|
+
* ↓
|
|
20
|
+
* Application.onCreate()
|
|
21
|
+
* ↓
|
|
22
|
+
* Activity lifecycle(FlutterActivity.onCreate → 首次 new FlutterEngine)
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* 插件反射替换 `FlutterInjector.flutterLoader` 只要求"早于第一次
|
|
26
|
+
* `FlutterInjector.instance()` 真正被使用",ContentProvider 这个时机足够。
|
|
27
|
+
* 这是 Firebase / WorkManager / androidx.startup 都在用的自动初始化模式。
|
|
28
|
+
*
|
|
29
|
+
* # 边界:什么时候走不通
|
|
30
|
+
* 宿主在自己 `Application.attachBaseContext` 里 **预热 FlutterEngine**(罕见,
|
|
31
|
+
* 主要出现在大厂混合工程的冷启动优化)。此时 provider 还没轮到、Engine 已起来,
|
|
32
|
+
* 反射来不及。这类工程应在宿主 Manifest 里关掉自动初始化,改为显式调用:
|
|
33
|
+
*
|
|
34
|
+
* ```xml
|
|
35
|
+
* <provider
|
|
36
|
+
* android:name="com.flutter_patcher.flutter_patcher.FlutterPatcherAutoInitProvider"
|
|
37
|
+
* android:authorities="${applicationId}.flutter_patcher.autoinit"
|
|
38
|
+
* tools:node="remove" />
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* 然后在自己的 Application 里:
|
|
42
|
+
*
|
|
43
|
+
* ```kotlin
|
|
44
|
+
* override fun attachBaseContext(base: Context) {
|
|
45
|
+
* super.attachBaseContext(base)
|
|
46
|
+
* FlutterPatcherApplication.attachPatcher(base)
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
class FlutterPatcherAutoInitProvider : ContentProvider() {
|
|
51
|
+
|
|
52
|
+
companion object {
|
|
53
|
+
private const val TAG = "FlutterPatcher/AutoInit"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
override fun onCreate(): Boolean {
|
|
57
|
+
val ctx = context ?: run {
|
|
58
|
+
Log.w(TAG, "context is null, skip auto init")
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
Log.d(TAG, "auto init via ContentProvider.onCreate()")
|
|
62
|
+
FlutterPatcherApplication.attachPatcher(ctx)
|
|
63
|
+
return true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ==================== no-op data surface ====================
|
|
67
|
+
// 本 provider 不对外暴露任何数据;所有 CRUD 都返回空 / 0 即可。
|
|
68
|
+
|
|
69
|
+
override fun query(
|
|
70
|
+
uri: Uri,
|
|
71
|
+
projection: Array<out String>?,
|
|
72
|
+
selection: String?,
|
|
73
|
+
selectionArgs: Array<out String>?,
|
|
74
|
+
sortOrder: String?
|
|
75
|
+
): Cursor? = null
|
|
76
|
+
|
|
77
|
+
override fun getType(uri: Uri): String? = null
|
|
78
|
+
|
|
79
|
+
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
|
|
80
|
+
|
|
81
|
+
override fun delete(
|
|
82
|
+
uri: Uri,
|
|
83
|
+
selection: String?,
|
|
84
|
+
selectionArgs: Array<out String>?
|
|
85
|
+
): Int = 0
|
|
86
|
+
|
|
87
|
+
override fun update(
|
|
88
|
+
uri: Uri,
|
|
89
|
+
values: ContentValues?,
|
|
90
|
+
selection: String?,
|
|
91
|
+
selectionArgs: Array<out String>?
|
|
92
|
+
): Int = 0
|
|
93
|
+
}
|