Files
xianyan/Scripts/check_android_config.py
Developer 88a3f6d65f feat: 新增仪表盘页面与macOS多项优化
1. 新增TDashboard翻译类型与多语言文案
2. 完善macOS权限管理与Impeller渲染适配
3. 更新服务器部署配置与协议文件上传脚本
4. 修复翻译导入服务与根类型编译问题
2026-06-26 06:34:05 +08:00

866 lines
35 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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, "未找到 <application> 标签")
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} 缺少 <intent>")
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.RUNquick_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"已配置 <queries>,包含 {len(intents)} 个 intent包可见性适配")
else:
self.add("Manifest", "queries配置", WARN, "未配置 <queries>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()