#!/usr/bin/env python3 # ============================================================ # 闲言APP — Android配置一致性检查脚本 # 创建时间: 2026-06-01 # 更新时间: 2026-06-01 # 名称: check_android_config.py # 作用: 验证Android原生配置与Flutter插件的一致性 # 上次更新: 初始创建,检查shortcuts/manifest/gradle配置 # ============================================================ import argparse import json import os import re import sys import xml.etree.ElementTree as ET ANDROID_NS = "http://schemas.android.com/apk/res/android" TOOLS_NS = "http://schemas.android.com/tools" PASS = "pass" WARN = "warn" FAIL = "fail" SCORE_WEIGHTS = {PASS: 10, WARN: 5, FAIL: 0} def ns(attr): return f"{{{ANDROID_NS}}}{attr}" def find_project_root(): candidates = [os.getcwd()] script_dir = os.path.dirname(os.path.abspath(__file__)) candidates.append(script_dir) for d in candidates: if os.path.isfile(os.path.join(d, "pubspec.yaml")): return d parent = os.path.dirname(d) if os.path.isfile(os.path.join(parent, "pubspec.yaml")): return parent return os.getcwd() def read_file(path): if not os.path.isfile(path): return None with open(path, "r", encoding="utf-8", errors="replace") as f: return f.read() def parse_xml(path): if not os.path.isfile(path): return None try: tree = ET.parse(path) return tree.getroot() except ET.ParseError: return None class CheckResult: def __init__(self, category, name, status, message, detail=None): self.category = category self.name = name self.status = status self.message = message self.detail = detail or [] def to_dict(self): return { "category": self.category, "name": self.name, "status": self.status, "message": self.message, "detail": self.detail, } class AndroidConfigChecker: def __init__(self, project_root, verbose=False): self.project_root = project_root self.verbose = verbose self.results = [] self.manifest_path = os.path.join( project_root, "android", "app", "src", "main", "AndroidManifest.xml" ) self.shortcuts_path = os.path.join( project_root, "android", "app", "src", "main", "res", "xml", "shortcuts.xml" ) self.app_gradle_path = os.path.join( project_root, "android", "app", "build.gradle.kts" ) self.root_gradle_path = os.path.join( project_root, "android", "build.gradle.kts" ) self.gradle_props_path = os.path.join( project_root, "android", "gradle.properties" ) self.pubspec_lock_path = os.path.join(project_root, "pubspec.lock") self.manifest_root = parse_xml(self.manifest_path) self.shortcuts_root = parse_xml(self.shortcuts_path) self.app_gradle_content = read_file(self.app_gradle_path) self.root_gradle_content = read_file(self.root_gradle_path) self.gradle_props_content = read_file(self.gradle_props_path) self.pubspec_lock_content = read_file(self.pubspec_lock_path) def add(self, category, name, status, message, detail=None): self.results.append(CheckResult(category, name, status, message, detail or [])) def check_manifest_exists(self): if self.manifest_root is not None: self.add("Manifest", "文件存在", PASS, "AndroidManifest.xml 存在且可解析") else: self.add("Manifest", "文件存在", FAIL, "AndroidManifest.xml 不存在或无法解析") def check_permissions(self): if self.manifest_root is None: self.add("Manifest", "权限检查", FAIL, "无法解析 Manifest,跳过权限检查") return permissions = [] for elem in self.manifest_root.iter(): if elem.tag == "uses-permission": name = elem.get(ns("name"), "") permissions.append(name) required = { "android.permission.INTERNET": "网络访问(dio/cached_network_image)", "android.permission.ACCESS_NETWORK_STATE": "网络状态检测", } for perm, desc in required.items(): if perm in permissions: self.add("Manifest", f"权限: {perm}", PASS, f"已声明 — {desc}") else: self.add("Manifest", f"权限: {perm}", FAIL, f"缺少必要权限 — {desc}") optional = { "android.permission.VIBRATE": "震动反馈", } for perm, desc in optional.items(): if perm in permissions: self.add("Manifest", f"权限: {perm}", PASS, f"已声明 — {desc}") else: self.add("Manifest", f"权限: {perm}", WARN, f"未声明 — {desc}(如不需要可忽略)") if self.verbose: self.add( "Manifest", "全部权限列表", PASS, f"共声明 {len(permissions)} 项权限", permissions, ) def check_activity_config(self): if self.manifest_root is None: self.add("Manifest", "Activity配置", FAIL, "无法解析 Manifest") return app = self.manifest_root.find("application") if app is None: self.add("Manifest", "Activity配置", FAIL, "未找到 标签") return activity = None for act in app.findall("activity"): name = act.get(ns("name"), "") if "MainActivity" in name: activity = act break if activity is None: self.add("Manifest", "Activity配置", FAIL, "未找到 MainActivity") return exported = activity.get(ns("exported"), "") if exported == "true": self.add("Manifest", "Activity exported", PASS, "MainActivity 已设置 exported=true") else: self.add("Manifest", "Activity exported", WARN, "MainActivity 未设置 exported=true,可能影响启动") launch_mode = activity.get(ns("launchMode"), "") if launch_mode == "singleTop": self.add("Manifest", "Activity launchMode", PASS, "launchMode=singleTop,防止重复实例") else: self.add("Manifest", "Activity launchMode", WARN, f"launchMode={launch_mode or '未设置'},建议设为 singleTop") soft_input = activity.get(ns("windowSoftInputMode"), "") if soft_input == "adjustResize": self.add("Manifest", "Activity softInputMode", PASS, "windowSoftInputMode=adjustResize") else: self.add("Manifest", "Activity softInputMode", WARN, f"windowSoftInputMode={soft_input or '未设置'},建议设为 adjustResize") hardware_accel = activity.get(ns("hardwareAccelerated"), "") if hardware_accel == "true": self.add("Manifest", "Activity hardwareAccelerated", PASS, "hardwareAccelerated=true") else: self.add("Manifest", "Activity hardwareAccelerated", WARN, "hardwareAccelerated 未启用,可能影响渲染性能") def check_intent_filters(self): if self.manifest_root is None: self.add("Manifest", "IntentFilter", FAIL, "无法解析 Manifest") return app = self.manifest_root.find("application") if app is None: return activity = None for act in app.findall("activity"): if "MainActivity" in act.get(ns("name"), ""): activity = act break if activity is None: return filters = activity.findall("intent-filter") has_main = False has_launcher = False share_filters = [] for f in filters: actions = [a.get(ns("name"), "") for a in f.findall("action")] categories = [c.get(ns("name"), "") for c in f.findall("category")] data_elems = f.findall("data") mime_types = [d.get(ns("mimeType"), "") for d in data_elems] if "android.intent.action.MAIN" in actions: has_main = True if "android.intent.category.LAUNCHER" in categories: has_launcher = True if "android.intent.action.SEND" in actions or "android.intent.action.SEND_MULTIPLE" in actions: share_filters.append( {"actions": actions, "categories": categories, "mimeTypes": mime_types} ) if has_main and has_launcher: self.add("Manifest", "启动IntentFilter", PASS, "MAIN+LAUNCHER 配置正确") else: self.add( "Manifest", "启动IntentFilter", FAIL, f"MAIN={has_main}, LAUNCHER={has_launcher},应用可能无法启动", ) if share_filters: self.add( "Manifest", "分享IntentFilter", PASS, f"已配置 {len(share_filters)} 个分享 IntentFilter", [f"actions={s['actions']}, mimeTypes={s['mimeTypes']}" for s in share_filters] if self.verbose else [], ) else: self.add("Manifest", "分享IntentFilter", WARN, "未配置分享 IntentFilter") def check_enable_on_back_invoked(self): if self.manifest_root is None: return app = self.manifest_root.find("application") if app is None: return app_flag = app.get(ns("enableOnBackInvokedCallback"), "") if app_flag == "true": self.add("Manifest", "enableOnBackInvokedCallback(app)", PASS, "Application 级已启用预测性返回手势") else: self.add("Manifest", "enableOnBackInvokedCallback(app)", WARN, "Application 级未启用预测性返回手势(Android 13+推荐)") activity = None for act in app.findall("activity"): if "MainActivity" in act.get(ns("name"), ""): activity = act break if activity is not None: act_flag = activity.get(ns("enableOnBackInvokedCallback"), "") if act_flag == "true": self.add("Manifest", "enableOnBackInvokedCallback(activity)", PASS, "Activity 级已启用预测性返回手势") else: self.add("Manifest", "enableOnBackInvokedCallback(activity)", WARN, "Activity 级未启用预测性返回手势") def check_shortcuts_xml(self): if self.shortcuts_root is None: self.add("Shortcuts", "shortcuts.xml", WARN, "res/xml/shortcuts.xml 不存在或无法解析,跳过快捷方式检查") return shortcuts = self.shortcuts_root.findall("shortcut") if not shortcuts: self.add("Shortcuts", "快捷方式数量", WARN, "shortcuts.xml 中无快捷方式定义") return self.add("Shortcuts", "快捷方式数量", PASS, f"共定义 {len(shortcuts)} 个快捷方式") shortcut_ids = [] for s in shortcuts: sid = s.get(ns("shortcutId"), "") enabled = s.get(ns("enabled"), "true") shortcut_ids.append(sid) if enabled == "true": self.add("Shortcuts", f"快捷方式: {sid}", PASS, f"已启用 (id={sid})") else: self.add("Shortcuts", f"快捷方式: {sid}", WARN, f"已禁用 (id={sid})") intent = s.find("intent") if intent is None: self.add("Shortcuts", f"{sid} intent", FAIL, f"快捷方式 {sid} 缺少 ") continue action = intent.get(ns("action"), "") target_class = intent.get(ns("targetClass"), "") extras = intent.findall("extra") if action == "android.intent.action.RUN": self.add("Shortcuts", f"{sid} action", PASS, f"action=RUN,与 quick_actions_android 插件一致") else: self.add( "Shortcuts", f"{sid} action", FAIL, f"action={action},应为 android.intent.action.RUN(quick_actions_android 插件要求)", ) if "MainActivity" in target_class: self.add("Shortcuts", f"{sid} targetClass", PASS, f"targetClass 指向 MainActivity") else: self.add("Shortcuts", f"{sid} targetClass", WARN, f"targetClass={target_class},请确认是否正确") extra_keys = [] extra_values = [] for extra in extras: key = extra.get(ns("name"), "") val = extra.get(ns("value"), "") extra_keys.append(key) extra_values.append(val) if "some unique action key" in extra_keys: self.add("Shortcuts", f"{sid} extra key", PASS, f'extra key="some unique action key",与 quick_actions_android 插件一致') else: self.add( "Shortcuts", f"{sid} extra key", FAIL, f'extra key={extra_keys},应为 "some unique action key"(quick_actions_android 插件内部常量)', ) if self.verbose and extra_values: self.add( "Shortcuts", f"{sid} extra values", PASS, f"extra values: {extra_values}", extra_values, ) def check_shortcuts_flutter_consistency(self): if self.shortcuts_root is None: return if self.pubspec_lock_content is None: self.add("Shortcuts", "Flutter插件一致性", WARN, "无法读取 pubspec.lock,跳过插件一致性检查") return plugin_version = None for line in self.pubspec_lock_content.splitlines(): stripped = line.strip() if stripped.startswith("version:"): parent_indent = len(line) - len(line.lstrip()) pass if "quick_actions_android" in stripped and "name:" in stripped: pass version_match = re.search( r"quick_actions_android:.*?version:\s*[\"']?([^\"'\s]+)", self.pubspec_lock_content, re.DOTALL, ) if version_match: plugin_version = version_match.group(1) self.add("Shortcuts", "quick_actions_android版本", PASS, f"插件版本: {plugin_version}") else: self.add("Shortcuts", "quick_actions_android版本", WARN, "无法从 pubspec.lock 解析 quick_actions_android 版本") pub_cache = self._find_pub_cache() plugin_source = None if pub_cache: plugin_dir = os.path.join(pub_cache, "quick_actions_android") if os.path.isdir(plugin_dir): plugin_source = plugin_dir if plugin_source is None: self.add( "Shortcuts", "插件源码检查", WARN, "未找到 quick_actions_android 插件源码,无法深度验证常量一致性", [f"搜索路径: {pub_cache}"] if pub_cache else [], ) self._check_shortcuts_dart_consistency() return quick_actions_file = os.path.join(plugin_source, "android", "src", "main", "kotlin", "io", "flutter", "plugins", "quickactions", "QuickActionsPlugin.kt") if not os.path.isfile(quick_actions_file): alt_paths = [ os.path.join(plugin_source, "lib", "src", "main", "kotlin", "io", "flutter", "plugins", "quickactions", "QuickActionsPlugin.kt"), os.path.join(plugin_source, "android", "src", "main", "kotlin", "io", "flutter", "plugins", "quickactions", "MethodCallHandlerImpl.kt"), ] for alt in alt_paths: if os.path.isfile(alt): quick_actions_file = alt break if not os.path.isfile(quick_actions_file): self.add("Shortcuts", "插件源码检查", WARN, "未找到 QuickActionsPlugin.kt,无法验证常量") self._check_shortcuts_dart_consistency() return plugin_content = read_file(quick_actions_file) expected_action = "android.intent.action.RUN" expected_key = "some unique action key" action_found = expected_action in plugin_content if plugin_content else False key_found = expected_key in plugin_content if plugin_content else False if action_found: self.add("Shortcuts", "插件action常量", PASS, f"插件源码中包含 action={expected_action}") else: self.add("Shortcuts", "插件action常量", WARN, f"插件源码中未找到 action={expected_action},可能版本已变更") if key_found: self.add("Shortcuts", "插件extra key常量", PASS, f'插件源码中包含 extra key="{expected_key}"') else: self.add("Shortcuts", "插件extra key常量", WARN, f'插件源码中未找到 extra key="{expected_key}",可能版本已变更') if self.shortcuts_root is not None: for s in self.shortcuts_root.findall("shortcut"): sid = s.get(ns("shortcutId"), "") intent = s.find("intent") if intent is None: continue xml_action = intent.get(ns("action"), "") extras = intent.findall("extra") xml_key = extras[0].get(ns("name"), "") if extras else "" action_match = xml_action == expected_action if action_found else True key_match = xml_key == expected_key if key_found else True if action_match and key_match: self.add("Shortcuts", f"{sid} 一致性", PASS, f"shortcuts.xml 与 quick_actions_android 插件常量完全一致") else: mismatches = [] if not action_match: mismatches.append(f"action: xml={xml_action}, plugin={expected_action}") if not key_match: mismatches.append(f"extra key: xml={xml_key}, plugin={expected_key}") self.add( "Shortcuts", f"{sid} 一致性", FAIL, f"shortcuts.xml 与插件常量不匹配!快捷方式将失效", mismatches, ) def _check_shortcuts_dart_consistency(self): dart_service_path = os.path.join( self.project_root, "lib", "core", "services", "device", "quick_actions_service.dart" ) content = read_file(dart_service_path) if content is None: self.add("Shortcuts", "Dart快捷操作一致性", WARN, "未找到 quick_actions_service.dart") return dart_types = re.findall(r"type:\s*'([^']+)'", content) if not dart_types: self.add("Shortcuts", "Dart快捷操作类型", WARN, "未从 Dart 代码中提取到 ShortcutItem type") return self.add("Shortcuts", "Dart快捷操作类型", PASS, f"Dart 中定义了 {len(dart_types)} 个快捷操作: {dart_types}") if self.shortcuts_root is None: return xml_ids = [s.get(ns("shortcutId"), "") for s in self.shortcuts_root.findall("shortcut")] for dart_type in dart_types: if dart_type in xml_ids: self.add("Shortcuts", f"Dart↔XML: {dart_type}", PASS, f"Dart type 与 XML shortcutId 一致") else: self.add( "Shortcuts", f"Dart↔XML: {dart_type}", FAIL, f"Dart type='{dart_type}' 在 XML shortcutId 中不存在 ({xml_ids})", ) for xml_id in xml_ids: if xml_id not in dart_types: self.add( "Shortcuts", f"XML↔Dart: {xml_id}", WARN, f"XML shortcutId='{xml_id}' 在 Dart ShortcutItem 中未定义", ) def _find_pub_cache(self): env_path = os.environ.get("PUB_CACHE") if env_path and os.path.isdir(env_path): return os.path.join(env_path, "hosted", "pub.flutter-io.cn") if os.path.isdir( os.path.join(env_path, "hosted", "pub.flutter-io.cn") ) else os.path.join(env_path, "hosted", "pub.dev") if os.path.isdir( os.path.join(env_path, "hosted", "pub.dev") ) else env_path home = os.path.expanduser("~") candidates = [ os.path.join(home, "AppData", "Local", "Pub", "Cache"), os.path.join(home, ".pub-cache"), os.path.join(home, ".pub_cache"), ] for c in candidates: hosted = os.path.join(c, "hosted") if os.path.isdir(hosted): for sub in os.listdir(hosted): sub_path = os.path.join(hosted, sub) if os.path.isdir(sub_path) and os.path.isdir( os.path.join(sub_path, "quick_actions_android") ): return sub_path return hosted return None def check_16kb_page_support(self): if self.app_gradle_content is None: self.add("Gradle", "16KB页面支持", WARN, "无法读取 app/build.gradle.kts") return if "useLegacyPackaging" in self.app_gradle_content: if "useLegacyPackaging = false" in self.app_gradle_content or "useLegacyPackaging=false" in self.app_gradle_content: self.add("Gradle", "16KB页面支持", PASS, "useLegacyPackaging=false,已支持 Android 15+ 16KB 页面大小") else: self.add("Gradle", "16KB页面支持", FAIL, "useLegacyPackaging=true,不支持 Android 15+ 16KB 页面大小设备") else: self.add("Gradle", "16KB页面支持", WARN, "未设置 useLegacyPackaging,建议显式设为 false") def check_sdk_versions(self): if self.app_gradle_content is None: self.add("Gradle", "SDK版本", WARN, "无法读取 app/build.gradle.kts") return min_sdk_match = re.search(r"minSdk\s*=\s*(\d+)", self.app_gradle_content) target_sdk_match = re.search(r"targetSdk\s*=\s*(\S+)", self.app_gradle_content) compile_sdk_match = re.search(r"compileSdk\s*=\s*(\S+)", self.app_gradle_content) min_sdk = int(min_sdk_match.group(1)) if min_sdk_match else None target_sdk = target_sdk_match.group(1) if target_sdk_match else None compile_sdk = compile_sdk_match.group(1) if compile_sdk_match else None if min_sdk is not None: if min_sdk >= 28: self.add("Gradle", f"minSdk={min_sdk}", PASS, f"最低SDK版本 {min_sdk},满足基本要求") else: self.add("Gradle", f"minSdk={min_sdk}", WARN, f"最低SDK版本 {min_sdk},建议 >= 28") else: self.add("Gradle", "minSdk", FAIL, "未找到 minSdk 配置") if target_sdk is not None: if target_sdk.startswith("flutter."): self.add("Gradle", f"targetSdk={target_sdk}", PASS, f"使用 Flutter 默认 targetSdk ({target_sdk})") else: try: tv = int(target_sdk) if tv >= 34: self.add("Gradle", f"targetSdk={tv}", PASS, f"目标SDK版本 {tv},满足 Google Play 要求") else: self.add("Gradle", f"targetSdk={tv}", WARN, f"目标SDK版本 {tv},Google Play 要求 >= 34") except ValueError: self.add("Gradle", f"targetSdk={target_sdk}", WARN, f"无法解析 targetSdk 值: {target_sdk}") else: self.add("Gradle", "targetSdk", WARN, "未找到 targetSdk 配置") if compile_sdk is not None: self.add("Gradle", f"compileSdk={compile_sdk}", PASS, f"编译SDK版本: {compile_sdk}") else: self.add("Gradle", "compileSdk", WARN, "未找到 compileSdk 配置") def check_ndk_config(self): if self.app_gradle_content is None: self.add("Gradle", "NDK配置", WARN, "无法读取 app/build.gradle.kts") return ndk_matches = re.findall(r"abiFilters\.add\([\"']([^\"']+)[\"']\)", self.app_gradle_content) if ndk_matches: if "arm64-v8a" in ndk_matches: self.add("Gradle", "NDK abiFilters", PASS, f"已配置 ABI 过滤: {ndk_matches}") else: self.add("Gradle", "NDK abiFilters", WARN, f"ABI 过滤中缺少 arm64-v8a: {ndk_matches}") else: self.add("Gradle", "NDK abiFilters", WARN, "未配置 abiFilters,将包含所有架构") ndk_version_match = re.search(r"ndkVersion\s*=\s*(\S+)", self.app_gradle_content) if ndk_version_match: self.add("Gradle", "NDK版本", PASS, f"ndkVersion={ndk_version_match.group(1)}") else: self.add("Gradle", "NDK版本", PASS, "使用 Flutter 默认 NDK 版本") def check_signing_config(self): if self.app_gradle_content is None: self.add("Gradle", "签名配置", WARN, "无法读取 app/build.gradle.kts") return if "signingConfig" in self.app_gradle_content: if "signingConfigs.getByName(\"debug\")" in self.app_gradle_content: self.add("Gradle", "签名配置", WARN, "Release 使用 debug 签名,正式发布前需配置 release 签名") else: self.add("Gradle", "签名配置", PASS, "已配置自定义签名") else: self.add("Gradle", "签名配置", WARN, "未找到签名配置") def check_gradle_properties(self): if self.gradle_props_content is None: self.add("Gradle", "gradle.properties", WARN, "无法读取 gradle.properties") return if "android.useAndroidX=true" in self.gradle_props_content: self.add("Gradle", "AndroidX", PASS, "已启用 AndroidX") else: self.add("Gradle", "AndroidX", WARN, "未启用 AndroidX") jvm_args_match = re.search(r"org\.gradle\.jvmargs=(.+)", self.gradle_props_content) if jvm_args_match: args = jvm_args_match.group(1).strip() if "-Xmx" in args: self.add("Gradle", "JVM内存", PASS, f"Gradle JVM 参数: {args}") else: self.add("Gradle", "JVM内存", WARN, f"JVM 参数中未设置 -Xmx: {args}") else: self.add("Gradle", "JVM内存", WARN, "未设置 org.gradle.jvmargs") def check_shortcuts_meta_data(self): if self.manifest_root is None: return app = self.manifest_root.find("application") if app is None: return activity = None for act in app.findall("activity"): if "MainActivity" in act.get(ns("name"), ""): activity = act break if activity is None: return has_shortcuts_meta = False for meta in activity.findall("meta-data"): name = meta.get(ns("name"), "") if name == "android.app.shortcuts": has_shortcuts_meta = True resource = meta.get(ns("resource"), "") if resource == "@xml/shortcuts": self.add("Manifest", "shortcuts meta-data", PASS, "android.app.shortcuts 指向 @xml/shortcuts") else: self.add("Manifest", "shortcuts meta-data", WARN, f"android.app.shortcuts 指向 {resource},请确认是否正确") break if not has_shortcuts_meta: if self.shortcuts_root is not None: self.add("Manifest", "shortcuts meta-data", FAIL, "shortcuts.xml 存在但 Activity 中缺少 android.app.shortcuts meta-data") else: self.add("Manifest", "shortcuts meta-data", PASS, "无 shortcuts 配置,无需 meta-data") def check_manage_space_activity(self): if self.manifest_root is None: return app = self.manifest_root.find("application") if app is None: return manage_space = app.get(ns("manageSpaceActivity"), "") if manage_space: self.add("Manifest", "manageSpaceActivity", PASS, f"已配置 manageSpaceActivity={manage_space}") else: self.add("Manifest", "manageSpaceActivity", WARN, "未配置 manageSpaceActivity,用户无法通过系统设置清理应用数据") def check_cleartext_traffic(self): if self.manifest_root is None: return app = self.manifest_root.find("application") if app is None: return cleartext = app.get(ns("usesCleartextTraffic"), "") if cleartext == "true": self.add("Manifest", "usesCleartextTraffic", WARN, "已启用明文流量(HTTP),生产环境建议关闭") else: self.add("Manifest", "usesCleartextTraffic", PASS, "未启用明文流量,安全性良好") def check_queries(self): if self.manifest_root is None: return queries = self.manifest_root.find("queries") if queries is not None: intents = queries.findall("intent") self.add("Manifest", "queries配置", PASS, f"已配置 ,包含 {len(intents)} 个 intent(包可见性适配)") else: self.add("Manifest", "queries配置", WARN, "未配置 ,Android 11+ 可能无法查询其他应用") def check_work_manager_receiver(self): if self.manifest_root is None: return app = self.manifest_root.find("application") if app is None: return for receiver in app.findall("receiver"): name = receiver.get(ns("name"), "") if "RescheduleReceiver" in name: tools_node_val = receiver.get(f"{{{TOOLS_NS}}}node", "") if tools_node_val == "remove": self.add("Manifest", "WorkManager Receiver", PASS, "已移除 WorkManager 自启动 Receiver,防止开机自启") else: self.add("Manifest", "WorkManager Receiver", WARN, "WorkManager RescheduleReceiver 未移除,可能导致开机自启") return self.add("Manifest", "WorkManager Receiver", PASS, "未发现 WorkManager RescheduleReceiver(已移除或不存在)") def run_all_checks(self): self.check_manifest_exists() self.check_permissions() self.check_activity_config() self.check_intent_filters() self.check_enable_on_back_invoked() self.check_shortcuts_meta_data() self.check_shortcuts_xml() self.check_shortcuts_flutter_consistency() self.check_16kb_page_support() self.check_sdk_versions() self.check_ndk_config() self.check_signing_config() self.check_gradle_properties() self.check_manage_space_activity() self.check_cleartext_traffic() self.check_queries() self.check_work_manager_receiver() return self.results def calculate_score(self): if not self.results: return 0 total = sum(SCORE_WEIGHTS[r.status] for r in self.results) max_total = len(self.results) * SCORE_WEIGHTS[PASS] return round(total / max_total * 100) if max_total > 0 else 0 def print_report(self): status_icon = {PASS: "✅", WARN: "⚠️", FAIL: "❌"} categories = {} for r in self.results: categories.setdefault(r.category, []).append(r) print("\n" + "=" * 60) print(" 闲言APP — Android 配置一致性检查报告") print("=" * 60) for cat, items in categories.items(): print(f"\n📦 {cat}") print("-" * 40) for item in items: icon = status_icon.get(item.status, "❓") print(f" {icon} {item.name}: {item.message}") if self.verbose and item.detail: for d in item.detail: print(f" → {d}") score = self.calculate_score() pass_count = sum(1 for r in self.results if r.status == PASS) warn_count = sum(1 for r in self.results if r.status == WARN) fail_count = sum(1 for r in self.results if r.status == FAIL) print("\n" + "=" * 60) print(f" 📊 总计: {len(self.results)} 项检查") print(f" ✅ 通过: {pass_count}") print(f" ⚠️ 警告: {warn_count}") print(f" ❌ 错误: {fail_count}") print(f" 🏆 评分: {score}/100") print("=" * 60) if fail_count > 0: print("\n🔴 需要立即修复的错误:") for r in self.results: if r.status == FAIL: print(f" • {r.category} → {r.name}: {r.message}") if warn_count > 0: print(f"\n🟡 建议关注的警告 ({warn_count} 项):") for r in self.results: if r.status == WARN: print(f" • {r.category} → {r.name}: {r.message}") print() return score def json_report(self): score = self.calculate_score() pass_count = sum(1 for r in self.results if r.status == PASS) warn_count = sum(1 for r in self.results if r.status == WARN) fail_count = sum(1 for r in self.results if r.status == FAIL) report = { "project": os.path.basename(self.project_root), "score": score, "total": len(self.results), "pass": pass_count, "warn": warn_count, "fail": fail_count, "checks": [r.to_dict() for r in self.results], } print(json.dumps(report, ensure_ascii=False, indent=2)) return score def main(): parser = argparse.ArgumentParser(description="闲言APP Android配置一致性检查") parser.add_argument("--verbose", "-v", action="store_true", help="输出详细信息") parser.add_argument("--json", action="store_true", help="输出JSON格式报告") parser.add_argument("--project", "-p", help="项目根目录路径(默认自动检测)") args = parser.parse_args() project_root = args.project or find_project_root() if not os.path.isfile(os.path.join(project_root, "pubspec.yaml")): print(f"❌ 未找到 Flutter 项目: {project_root}") sys.exit(1) checker = AndroidConfigChecker(project_root, verbose=args.verbose) checker.run_all_checks() if args.json: score = checker.json_report() else: score = checker.print_report() sys.exit(0 if score >= 60 else 1) if __name__ == "__main__": main()