Files
embyx/.github/patch_android.py
T
juneix 36b92b2b64 v1.1
2026-04-21 18:20:33 +08:00

301 lines
14 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.
import os
import re
import sys
import shutil
import argparse
# 根据命令行参数决定打包中文版还是英文版
# 用法: python patch_android.py --lang zh 或 --lang en
parser = argparse.ArgumentParser()
parser.add_argument("--lang", choices=["zh", "en"], default="zh", help="打包语言版本")
args = parser.parse_args()
LANG = args.lang
PKG_NAME = "juneix.embyx"
PKG_PATH = PKG_NAME.replace(".", "/") # juneix/embyx
APP_NAME = "EmbyX"
ICON_SRC = f"{LANG}/icon.png" # zh/icon.png 或 en/icon.png
# ── 0. 从 HTML 徽章提取版本号 ─────────────────────────────────────────────────
# HTML 中的版本徽章格式为 ">v1.1<",两个版本的徽章内容保持一致,统一读 zh/index.html
# 修改规格:如需从其他文件读取版本,修改下方 VERSION_SRC 路径即可
VERSION_SRC = "zh/index.html"
version_name = "1.0" # 默认回退值
version_code = 100 # 对应 v1.0
if os.path.exists(VERSION_SRC):
with open(VERSION_SRC, "r", encoding="utf-8") as f:
html = f.read()
# 匹配徽章文本,例如 ">v1.1<" 或 ">v2.0<"(非贪婪,只取第一个)
m = re.search(r">v(\d+)\.(\d+)(?:\.(\d+))?<", html)
if m:
major = int(m.group(1))
minor = int(m.group(2))
patch = int(m.group(3)) if m.group(3) else 0
version_name = f"{major}.{minor}" if patch == 0 else f"{major}.{minor}.{patch}"
# versionCode 规则:major×10000 + minor×100 + patch
# 示例:v1.0→10000, v1.1→10100, v1.9→10900, v2.0→20000, v1.1.2→10102
# 这样三段版本号也能正确递增,且不会与两段版本号冲突
version_code = major * 10000 + minor * 100 + patch
print(f" Detected version: v{version_name} → versionCode={version_code}")
else:
print(f" WARNING: Version badge not found in {VERSION_SRC}, using default {version_name}")
else:
print(f" WARNING: {VERSION_SRC} not found, using default version {version_name}")
print(f"Patching Android Project for lang={LANG}, pkg={PKG_NAME}, version={version_name}...")
# ── 1. 图标文件 ──────────────────────────────────────────────────────────────
# 将对应语言版本的 icon.png 复制到 Android drawable 资源目录
os.makedirs("android/app/src/main/res/drawable", exist_ok=True)
if os.path.exists(ICON_SRC):
shutil.copy(ICON_SRC, "android/app/src/main/res/drawable/icon.png")
print(f" Copied {ICON_SRC} → drawable/icon.png")
else:
print(f" WARNING: {ICON_SRC} not found, skipping icon copy")
# ── 2. AndroidManifest.xml 补丁 ──────────────────────────────────────────────
manifest_path = "android/app/src/main/AndroidManifest.xml"
with open(manifest_path, "r", encoding="utf-8") as f:
manifest = f.read()
# 替换默认图标引用为我们的 drawable/icon
manifest = manifest.replace("@mipmap/ic_launcher_round", "@drawable/icon")
manifest = manifest.replace("@mipmap/ic_launcher", "@drawable/icon")
manifest = manifest.replace("@drawable/icon_round", "@drawable/icon")
# 添加 WAKE_LOCK 权限(屏保/常亮需要)
if "android.permission.WAKE_LOCK" not in manifest:
manifest = manifest.replace(
"</manifest>",
' <uses-permission android:name="android.permission.WAKE_LOCK" />\n</manifest>'
)
# 注册 EmbyXDreamServiceAndroid 系统屏保服务)
# 修改规格:如需换 label 或 icon,修改下方 android:label / android:permission 即可
service_block = """
<service
android:name=".EmbyXDreamService"
android:exported="true"
android:label="@string/app_name"
android:permission="android.permission.BIND_DREAM_SERVICE">
<intent-filter>
<action android:name="android.service.dreams.DreamService" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="android.service.dream"
android:resource="@xml/dream_info" />
</service>
"""
if "EmbyXDreamService" not in manifest:
manifest = manifest.replace("</application>", f"{service_block}\n </application>")
with open(manifest_path, "w", encoding="utf-8") as f:
f.write(manifest)
print(" Patched AndroidManifest.xml")
# ── 3. MainActivity.java 补丁 ────────────────────────────────────────────────
# 添加全屏沉浸式模式 + FLAG_KEEP_SCREEN_ON(视频播放常亮)
# 修改规格:如需改变沉浸式行为,修改下方 SYSTEM_UI_FLAG_* 标志位组合
main_activity_path = f"android/app/src/main/java/{PKG_PATH}/MainActivity.java"
with open(main_activity_path, "r", encoding="utf-8") as f:
main_activity = f.read()
immersive_code = """
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
getWindow().getDecorView().setSystemUiVisibility(
android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| android.view.View.SYSTEM_UI_FLAG_FULLSCREEN);
}
}
"""
# 添加 FLAG_KEEP_SCREEN_ON(视频播放常亮,防止熄屏打断视频)
if "FLAG_KEEP_SCREEN_ON" not in main_activity:
main_activity = main_activity.replace(
"super.onCreate(savedInstanceState);",
"super.onCreate(savedInstanceState);\n getWindow().addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);"
)
# 添加全屏沉浸式覆盖
if "onWindowFocusChanged" not in main_activity:
main_activity = main_activity.replace("}", immersive_code + "\n}", 1)
with open(main_activity_path, "w", encoding="utf-8") as f:
f.write(main_activity)
print(" Patched MainActivity.java (fullscreen + keep screen on)")
# ── 4. EmbyXDreamService.java ───────────────────────────────────────────────
# 屏保实现:使用 Android DreamService,内嵌 WebView 加载 EmbyX 本地页面
# Capacitor 打包后的 webDir 内容位于 file:///android_asset/public/index.html
# EmbyX 会读取 localStorage 中已保存的 Emby 服务器配置,自动连接并播放
# 修改规格:如需改变屏保交互性,修改 setInteractive(true/false)
dream_service_code = f"""package {PKG_NAME};
import android.service.dreams.DreamService;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.view.WindowManager;
public class EmbyXDreamService extends DreamService {{
private WebView webView;
@Override
public void onAttachedToWindow() {{
super.onAttachedToWindow();
// 允许用户与屏保交互(点击/滑动视频)
setInteractive(true);
setFullscreen(true);
webView = new WebView(this);
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true); // localStorage 需要,用于读取 Emby token
settings.setDatabaseEnabled(true);
settings.setMediaPlaybackRequiresUserGesture(false); // 屏保自动播放视频无需手势
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); // 允许 HTTP 视频流
webView.setWebViewClient(new WebViewClient());
// 加载 Capacitor 打包的本地 index.htmlEmbyX 会自动读取已保存的 Emby 配置)
webView.loadUrl("file:///android_asset/public/index.html");
setContentView(webView);
}}
@Override
public void onDreamingStarted() {{
super.onDreamingStarted();
// 屏保激活时保持屏幕常亮(播放视频需要)
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}}
@Override
public void onDreamingStopped() {{
super.onDreamingStopped();
if (webView != null) {{
webView.loadUrl("about:blank");
webView.destroy();
webView = null;
}}
}}
}}
"""
dream_service_path = f"android/app/src/main/java/{PKG_PATH}/EmbyXDreamService.java"
with open(dream_service_path, "w", encoding="utf-8") as f:
f.write(dream_service_code)
print(f" Created EmbyXDreamService.java at {dream_service_path}")
# ── 5. dream_info.xml ────────────────────────────────────────────────────────
# Android 需要此 XML 文件来注册屏保,previewImage 显示在系统屏保选择列表中
os.makedirs("android/app/src/main/res/xml", exist_ok=True)
xml_code = f"""<?xml version="1.0" encoding="utf-8"?>
<dream android:settingsActivity="{PKG_NAME}.MainActivity"
android:previewImage="@drawable/icon"
xmlns:android="http://schemas.android.com/apk/res/android" />
"""
with open("android/app/src/main/res/xml/dream_info.xml", "w", encoding="utf-8") as f:
f.write(xml_code)
print(" Created dream_info.xml")
# ── 6. 注入 APK 版本号到 build.gradle ───────────────────────────────────────
# Android 覆盖安装依赖 versionCode(整数)必须递增,versionName 是人类可读标签
# Capacitor 生成的默认值是 versionCode=1, versionName="1.0"
# 这里用从 HTML 读取的版本替换,保证每次发布 APK 能覆盖旧版本
# 修改规格:versionCode 算法在脚本顶部第 0 步,修改 major/minor/patch 的乘数即可
gradle_path = "android/app/build.gradle"
if os.path.exists(gradle_path):
with open(gradle_path, "r", encoding="utf-8") as f:
gradle = f.read()
# 替换 versionCode(形如 "versionCode 1" 或 "versionCode 10000"
gradle = re.sub(r"versionCode\s+\d+", f"versionCode {version_code}", gradle)
# 替换 versionName(形如 versionName "1.0"
gradle = re.sub(r'versionName\s+"[^"]+"', f'versionName "{version_name}"', gradle)
with open(gradle_path, "w", encoding="utf-8") as f:
f.write(gradle)
print(f" Patched build.gradle → versionCode={version_code}, versionName=\"{version_name}\"")
else:
print(f" WARNING: {gradle_path} not found, skipping version injection")
# ── 7. 启动页 (Splash Screen) 美化 ───────────────────────────────────────────
# 创建颜色资源
os.makedirs("android/app/src/main/res/values", exist_ok=True)
colors_path = "android/app/src/main/res/values/colors.xml"
colors_xml = """<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#000000</color>
<color name="white">#FFFFFF</color>
</resources>
"""
with open(colors_path, "w", encoding="utf-8") as f:
f.write(colors_xml)
print(" Created colors.xml")
# 创建 Splash Drawable (Layer-List)
# 背景纯黑,Logo 居中
splash_xml = """<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/black" />
<item>
<bitmap
android:gravity="center"
android:src="@drawable/icon" />
</item>
</layer-list>
"""
with open("android/app/src/main/res/drawable/splash_screen.xml", "w", encoding="utf-8") as f:
f.write(splash_xml)
print(" Created splash_screen.xml")
# ── 8. 修改主题 (Themes) 强制黑底 ──────────────────────────────────────────────
# Capacitor 默认使用 AppTheme.NoActionBarLaunch 作为启动主题
# 我们将其背景从白色改为我们自定义的黑色启动图
themes_dirs = ["android/app/src/main/res/values", "android/app/src/main/res/values-night"]
for t_dir in themes_dirs:
t_path = os.path.join(t_dir, "themes.xml")
if os.path.exists(t_path):
with open(t_path, "r", encoding="utf-8") as f:
t_content = f.read()
# 替换 windowBackground
t_content = re.sub(
r'(<item name="android:background">)(.*?)(</item>)',
r'\1@drawable/splash_screen\3',
t_content
)
# 针对新版 Android 12+ Splash API
t_content = t_content.replace("@drawable/show_splash", "@drawable/splash_screen")
with open(t_path, "w", encoding="utf-8") as f:
f.write(t_content)
print(f" Patched {t_path} to use splash_screen")
# ── 9. MainActivity 深度黑化 (防止 WebView 加载瞬间闪白) ───────────────────────
with open(main_activity_path, "r", encoding="utf-8") as f:
ma = f.read()
# 在 onCreate 中设置 WebView 背景颜色
if "getWebView().setBackgroundColor" not in ma:
ma = ma.replace(
"super.onCreate(savedInstanceState);",
"super.onCreate(savedInstanceState);\n // 消除 WebView 初始化时的白屏闪烁\n this.bridge.getWebView().setBackgroundColor(android.graphics.Color.BLACK);"
)
with open(main_activity_path, "w", encoding="utf-8") as f:
f.write(ma)
print(" Injected black background to WebView in MainActivity.java")
print(f"\nPatch complete! ({LANG} version, pkg={PKG_NAME}, v{version_name})")