diff --git a/.github/workflows/android-tv.yml b/.github/workflows/android-tv.yml index 0b3f429..532fc89 100644 --- a/.github/workflows/android-tv.yml +++ b/.github/workflows/android-tv.yml @@ -40,12 +40,26 @@ jobs: fail-fast: false matrix: include: - - variant: standard + - variant: standard-webview + flavor: webview + flavor_task: Webview min_sdk: '23' - label: android6plus - - variant: compat + label: webview-android6plus + - variant: compat-webview + flavor: webview + flavor_task: Webview min_sdk: '21' - label: android5plus + label: webview-android5plus + - variant: standard-gecko + flavor: gecko + flavor_task: Gecko + min_sdk: '23' + label: geckoview-android6plus + - variant: compat-gecko + flavor: gecko + flavor_task: Gecko + min_sdk: '21' + label: geckoview-android5plus steps: - name: Checkout source code @@ -74,13 +88,25 @@ jobs: cp public/logo.png apps/android-tv/app/src/main/res/drawable/logo.png - name: Decode Android signing keystore + id: signing env: ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} run: | - if [ -n "$ANDROID_KEYSTORE_BASE64" ]; then + if [ -n "$ANDROID_KEYSTORE_BASE64" ] && [ -n "$ANDROID_KEYSTORE_PASSWORD" ] && [ -n "$ANDROID_KEY_ALIAS" ] && [ -n "$ANDROID_KEY_PASSWORD" ]; then echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > /tmp/android-tv-release.keystore + echo "build_type=release" >> "$GITHUB_OUTPUT" + echo "build_type_task=Release" >> "$GITHUB_OUTPUT" + echo "apk_suffix=release" >> "$GITHUB_OUTPUT" + echo "artifact_suffix=release-signed" >> "$GITHUB_OUTPUT" else - echo "ANDROID_KEYSTORE_BASE64 is not set, release APK will be unsigned." + echo "Android signing secrets are not fully set, building debug APK only." + echo "build_type=debug" >> "$GITHUB_OUTPUT" + echo "build_type_task=Debug" >> "$GITHUB_OUTPUT" + echo "apk_suffix=debug" >> "$GITHUB_OUTPUT" + echo "artifact_suffix=debug" >> "$GITHUB_OUTPUT" fi - name: Build APK @@ -96,7 +122,7 @@ jobs: ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} run: | - gradle assembleDebug assembleRelease \ + gradle assemble${{ matrix.flavor_task }}${{ steps.signing.outputs.build_type_task }} \ -PBASE_URL="$BASE_URL" \ -PAPP_NAME="$APP_NAME" \ -PVERSION_NAME="$VERSION_NAME" \ @@ -106,13 +132,9 @@ jobs: - name: Collect APK artifacts run: | mkdir -p /tmp/android-tv-apks - cp apps/android-tv/app/build/outputs/apk/debug/app-debug.apk "/tmp/android-tv-apks/moontvplus-tv-${{ inputs.version_name }}-${{ matrix.label }}-debug.apk" - if [ -f apps/android-tv/app/build/outputs/apk/release/app-release.apk ]; then - cp apps/android-tv/app/build/outputs/apk/release/app-release.apk "/tmp/android-tv-apks/moontvplus-tv-${{ inputs.version_name }}-${{ matrix.label }}-release-signed.apk" - fi - if [ -f apps/android-tv/app/build/outputs/apk/release/app-release-unsigned.apk ]; then - cp apps/android-tv/app/build/outputs/apk/release/app-release-unsigned.apk "/tmp/android-tv-apks/moontvplus-tv-${{ inputs.version_name }}-${{ matrix.label }}-release-unsigned.apk" - fi + cp \ + "apps/android-tv/app/build/outputs/apk/${{ matrix.flavor }}/${{ steps.signing.outputs.build_type }}/app-${{ matrix.flavor }}-${{ steps.signing.outputs.apk_suffix }}.apk" \ + "/tmp/android-tv-apks/moontvplus-tv-${{ inputs.version_name }}-${{ matrix.label }}-${{ steps.signing.outputs.artifact_suffix }}.apk" ls -lh /tmp/android-tv-apks - name: Upload APK artifacts diff --git a/apps/android-tv/README.md b/apps/android-tv/README.md index 80b8c09..f5116b5 100644 --- a/apps/android-tv/README.md +++ b/apps/android-tv/README.md @@ -9,6 +9,7 @@ - `VERSION_NAME`: APK 版本名 - `VERSION_CODE`: APK 版本号,整数 - `MIN_SDK`: 最低 Android API,标准版为 `23`(Android 6+),兼容版为 `21`(Android 5+) +- `GECKOVIEW_VERSION`: GeckoView 依赖版本,仅 GeckoView 版本使用,默认 `126.0.20240526221752` App 启动时会自动打开: @@ -18,10 +19,20 @@ BASE_URL 去掉末尾 / 后 + /tv ## 特性 -- GitHub Actions 默认构建两个版本:`android6plus` 标准版和 `android5plus` 兼容版 +- GitHub Actions 默认构建四个版本:`webview-android6plus`、`webview-android5plus`、`geckoview-android6plus`、`geckoview-android5plus` +- GitHub Actions 未配置签名 secrets 时只构建 debug APK;配置完整签名 secrets 后只构建 release APK +- `webview` 版本使用系统 Android WebView,体积小但依赖设备内置 WebView 版本 +- `gecko` 版本自带 GeckoView 浏览器内核,用于旧系统 WebView 无法兼容 Next.js 页面时测试 - 锁定横屏 - 支持 Android TV Launcher - 允许 HTTP 明文访问 - 允许 HTTPS 页面加载 HTTP 视频/图片等混合内容 - 使用 `public/logo.png` 作为图标来源 -- User-Agent 追加 `MoonTVPlusAndroidTV` +- `webview` 版本 User-Agent 追加 `MoonTVPlusAndroidTV WebView` + +## 本地构建 + +```bash +gradle assembleWebviewDebug -PBASE_URL="http://192.168.1.10:3000" +gradle assembleGeckoDebug -PBASE_URL="http://192.168.1.10:3000" +``` diff --git a/apps/android-tv/app/build.gradle.kts b/apps/android-tv/app/build.gradle.kts index f2cb9b8..05a100d 100644 --- a/apps/android-tv/app/build.gradle.kts +++ b/apps/android-tv/app/build.gradle.kts @@ -13,6 +13,7 @@ val appDisplayName = propOrEnv("APP_NAME", "APP_NAME", "MoonTVPlus TV") val versionNameValue = propOrEnv("VERSION_NAME", "VERSION_NAME", "1.0.0") val versionCodeValue = propOrEnv("VERSION_CODE", "VERSION_CODE", "1").toIntOrNull() ?: 1 val minSdkValue = propOrEnv("MIN_SDK", "MIN_SDK", "23").toIntOrNull() ?: 23 +val geckoViewVersion = propOrEnv("GECKOVIEW_VERSION", "GECKOVIEW_VERSION", "126.0.20240526221752") fun escapeJavaString(value: String): String = value .replace("\\", "\\\\") @@ -44,6 +45,27 @@ android { buildConfig = true } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + flavorDimensions += "engine" + + productFlavors { + create("webview") { + dimension = "engine" + buildConfigField("String", "ENGINE_NAME", "\"WebView\"") + resValue("string", "engine_name", "WebView") + } + create("gecko") { + dimension = "engine" + applicationIdSuffix = ".gecko" + buildConfigField("String", "ENGINE_NAME", "\"GeckoView\"") + resValue("string", "engine_name", "GeckoView") + } + } + signingConfigs { create("release") { val storeFilePath = System.getenv("ANDROID_KEYSTORE_PATH") @@ -69,3 +91,7 @@ android { } } } + +dependencies { + add("geckoImplementation", "org.mozilla.geckoview:geckoview:$geckoViewVersion") +} diff --git a/apps/android-tv/app/src/gecko/java/com/moontvplus/tv/MainActivity.java b/apps/android-tv/app/src/gecko/java/com/moontvplus/tv/MainActivity.java new file mode 100644 index 0000000..4ccd6b3 --- /dev/null +++ b/apps/android-tv/app/src/gecko/java/com/moontvplus/tv/MainActivity.java @@ -0,0 +1,95 @@ +package com.moontvplus.tv; + +import android.app.Activity; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import org.mozilla.geckoview.GeckoRuntime; +import org.mozilla.geckoview.GeckoSession; +import org.mozilla.geckoview.GeckoView; + +public class MainActivity extends Activity { + private static GeckoRuntime runtime; + + private GeckoSession session; + private GeckoView geckoView; + private boolean canGoBack = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); + getWindow().getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + ); + + geckoView = new GeckoView(this); + geckoView.setFocusable(true); + geckoView.setFocusableInTouchMode(true); + geckoView.requestFocus(); + setContentView(geckoView, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + )); + + if (runtime == null) { + runtime = GeckoRuntime.create(this); + } + + session = new GeckoSession(); + session.setNavigationDelegate(new GeckoSession.NavigationDelegate() { + @Override + public void onCanGoBack(GeckoSession session, boolean canGoBackValue) { + canGoBack = canGoBackValue; + } + }); + session.open(runtime); + geckoView.setSession(session); + session.loadUri(buildTvUrl(BuildConfig.BASE_URL)); + } + + private static String buildTvUrl(String baseUrl) { + String url = baseUrl == null ? "" : baseUrl.trim(); + if (url.isEmpty()) { + url = "http://192.168.1.10:3000"; + } + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = "http://" + url; + } + while (url.endsWith("/")) { + url = url.substring(0, url.length() - 1); + } + if (url.endsWith("/tv")) { + return url; + } + return url + "/tv"; + } + + @Override + public void onBackPressed() { + if (session != null && canGoBack) { + session.goBack(); + return; + } + super.onBackPressed(); + } + + @Override + protected void onDestroy() { + if (session != null) { + session.close(); + session = null; + } + super.onDestroy(); + } +} diff --git a/apps/android-tv/app/src/main/java/com/moontvplus/tv/MainActivity.java b/apps/android-tv/app/src/webview/java/com/moontvplus/tv/MainActivity.java similarity index 96% rename from apps/android-tv/app/src/main/java/com/moontvplus/tv/MainActivity.java rename to apps/android-tv/app/src/webview/java/com/moontvplus/tv/MainActivity.java index 97b5435..3ef46da 100644 --- a/apps/android-tv/app/src/main/java/com/moontvplus/tv/MainActivity.java +++ b/apps/android-tv/app/src/webview/java/com/moontvplus/tv/MainActivity.java @@ -70,7 +70,7 @@ public class MainActivity extends Activity { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); } - settings.setUserAgentString(settings.getUserAgentString() + " MoonTVPlusAndroidTV"); + settings.setUserAgentString(settings.getUserAgentString() + " MoonTVPlusAndroidTV WebView"); webView.setWebViewClient(new WebViewClient() { @Override @@ -90,8 +90,6 @@ public class MainActivity extends Activity { @Override public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { - // Do not bypass TLS certificate errors. HTTP is allowed by network security config; - // invalid HTTPS certificates should still be blocked for safety. handler.cancel(); } }); diff --git a/apps/android-tv/gradle.properties b/apps/android-tv/gradle.properties index ed2d02a..2e11322 100644 --- a/apps/android-tv/gradle.properties +++ b/apps/android-tv/gradle.properties @@ -1,3 +1,3 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -android.useAndroidX=false +android.useAndroidX=true android.nonTransitiveRClass=true diff --git a/apps/android-tv/settings.gradle.kts b/apps/android-tv/settings.gradle.kts index 66db2fd..8322724 100644 --- a/apps/android-tv/settings.gradle.kts +++ b/apps/android-tv/settings.gradle.kts @@ -11,6 +11,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven("https://maven.mozilla.org/maven2/") } }