1. 新增TDashboard翻译类型与多语言文案 2. 完善macOS权限管理与Impeller渲染适配 3. 更新服务器部署配置与协议文件上传脚本 4. 修复翻译导入服务与根类型编译问题
866 lines
35 KiB
Python
866 lines
35 KiB
Python
#!/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.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"已配置 <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()
|