diff --git a/android-tv/README.md b/android-tv/README.md
new file mode 100644
index 0000000..847f5ba
--- /dev/null
+++ b/android-tv/README.md
@@ -0,0 +1,101 @@
+# Android TV WebView 壳
+
+为 Android TV 设备(包括智能电视、电视盒子等)提供的 WebView 套壳应用。
+
+## 特性
+
+- ✅ **Leanback 支持** - 专为 TV 优化的主题和启动器
+- ✅ **D-Pad 导航** - 遥控器方向键支持,自动注入 JS 导航
+- ✅ **全屏沉浸** - 无状态栏/导航栏,纯全屏体验
+- ✅ **横屏锁定** - 强制横屏显示
+- ✅ **按键处理** - 支持返回、菜单、信息键等遥控器按键
+
+## 目录结构
+
+```
+android-tv/
+├── app/src/main/
+│ ├── java/com/iptv/tv/
+│ │ └── MainActivity.java # TV 主活动(含 D-Pad 支持)
+│ ├── res/
+│ │ ├── layout/activity_main.xml
+│ │ ├── values/
+│ │ │ ├── strings.xml
+│ │ │ ├── styles.xml # Leanback 主题
+│ │ │ └── colors.xml
+│ │ └── drawable/
+│ │ └── ic_banner.xml # TV 启动器横幅
+│ ├── assets/
+│ │ └── www/ # 打包的 Web 资源
+│ └── AndroidManifest.xml # TV 特定配置
+├── build.sh # 一键构建脚本
+└── ...
+```
+
+## 构建步骤
+
+```bash
+# 1. 构建 Web UI
+cd ../ui
+npm install
+npm run build
+
+# 2. 构建 Android TV APK
+cd ../android-tv
+./build.sh
+```
+
+APK 输出: `app/build/outputs/apk/debug/app-debug.apk`
+
+## TV 特定配置
+
+### AndroidManifest.xml
+
+```xml
+
+
+
+
+
+
+
+
+```
+
+### D-Pad 导航支持
+
+应用自动注入 JavaScript 代码,使 Web 应用支持遥控器方向键导航:
+
+- **方向键** - 在可聚焦元素间移动
+- **确认键** - 点击当前聚焦元素
+- **返回键** - 页面后退/退出应用
+- **菜单键** - 刷新页面
+- **信息键** - 显示版本信息
+
+## 遥控器按键映射
+
+| 按键 | 功能 |
+|------|------|
+| 上/下/左/右 | 导航焦点 |
+| 确认/OK | 点击 |
+| 返回 | 页面后退 |
+| 菜单 | 刷新页面 |
+| 信息/INFO | 显示版本 |
+
+## 开发与调试
+
+1. **ADB 连接 TV**
+ ```bash
+ adb connect :5555
+ adb install app-debug.apk
+ ```
+
+2. **查看日志**
+ ```bash
+ adb logcat -s IPTV:D
+ ```
+
+3. **远程调试**
+ ```bash
+ adb shell am start -a android.intent.action.VIEW -d "http://:5173"
+ ```
diff --git a/android-tv/app/build.gradle b/android-tv/app/build.gradle
new file mode 100644
index 0000000..c585851
--- /dev/null
+++ b/android-tv/app/build.gradle
@@ -0,0 +1,33 @@
+plugins {
+ id 'com.android.application'
+}
+
+android {
+ namespace 'com.iptv.tv'
+ compileSdk 34
+
+ defaultConfig {
+ applicationId "com.iptv.tv"
+ minSdk 21
+ targetSdk 34
+ versionCode 1
+ versionName "1.0"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ implementation 'androidx.leanback:leanback:1.0.0'
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
+}
diff --git a/android-tv/app/proguard-rules.pro b/android-tv/app/proguard-rules.pro
new file mode 100644
index 0000000..f23d2ad
--- /dev/null
+++ b/android-tv/app/proguard-rules.pro
@@ -0,0 +1 @@
+# ProGuard rules
diff --git a/android-tv/app/src/main/AndroidManifest.xml b/android-tv/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..4b666d1
--- /dev/null
+++ b/android-tv/app/src/main/AndroidManifest.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-tv/app/src/main/java/com/iptv/tv/AssetReader.java b/android-tv/app/src/main/java/com/iptv/tv/AssetReader.java
new file mode 100644
index 0000000..85f73d4
--- /dev/null
+++ b/android-tv/app/src/main/java/com/iptv/tv/AssetReader.java
@@ -0,0 +1,48 @@
+package com.iptv.tv;
+
+import android.content.Context;
+import android.webkit.JavascriptInterface;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+public class AssetReader {
+ private Context context;
+
+ public AssetReader(Context context) {
+ this.context = context;
+ }
+
+ @JavascriptInterface
+ public String readFile(String path) {
+ try {
+ InputStream is = context.getAssets().open(path);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(is));
+ StringBuilder sb = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ sb.append(line).append("\n");
+ }
+ reader.close();
+ return sb.toString();
+ } catch (IOException e) {
+ return "ERROR: " + e.getMessage();
+ }
+ }
+
+ @JavascriptInterface
+ public String readChannelData() {
+ return readFile("www/api/result.txt");
+ }
+
+ @JavascriptInterface
+ public boolean fileExists(String path) {
+ try {
+ context.getAssets().open(path).close();
+ return true;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+}
diff --git a/android-tv/app/src/main/java/com/iptv/tv/MainActivity.java b/android-tv/app/src/main/java/com/iptv/tv/MainActivity.java
new file mode 100644
index 0000000..e09e575
--- /dev/null
+++ b/android-tv/app/src/main/java/com/iptv/tv/MainActivity.java
@@ -0,0 +1,148 @@
+package com.iptv.tv;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.graphics.Color;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.WindowManager;
+import android.webkit.WebChromeClient;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+public class MainActivity extends Activity {
+
+ private static final String TAG = "IPTV_TV";
+ private WebView webView;
+
+ @SuppressLint("SetJavaScriptEnabled")
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Log.d(TAG, "onCreate started");
+
+ // 全屏设置
+ getWindow().setFlags(
+ WindowManager.LayoutParams.FLAG_FULLSCREEN,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN
+ );
+
+ // 设置内容视图
+ webView = new WebView(this);
+ webView.setBackgroundColor(Color.BLACK);
+ setContentView(webView);
+
+ // 添加 JavaScript 接口
+ webView.addJavascriptInterface(new AssetReader(this), "AndroidAsset");
+
+ // WebView 设置
+ WebSettings settings = webView.getSettings();
+ settings.setJavaScriptEnabled(true);
+ settings.setDomStorageEnabled(true);
+ settings.setDatabaseEnabled(true);
+ settings.setCacheMode(WebSettings.LOAD_DEFAULT);
+ settings.setMediaPlaybackRequiresUserGesture(false);
+ settings.setAllowFileAccess(true);
+ settings.setAllowContentAccess(true);
+ settings.setAllowFileAccessFromFileURLs(true);
+ settings.setAllowUniversalAccessFromFileURLs(true);
+
+ // TV 不需要缩放
+ settings.setSupportZoom(false);
+ settings.setBuiltInZoomControls(false);
+
+ // WebViewClient
+ webView.setWebViewClient(new WebViewClient() {
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
+ return false;
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ Log.d(TAG, "onPageFinished: " + url);
+ injectDPadSupport();
+ }
+
+ @Override
+ public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
+ Log.e(TAG, "onReceivedError: " + errorCode + " - " + description);
+ }
+ });
+
+ // WebChromeClient
+ webView.setWebChromeClient(new WebChromeClient() {
+ @Override
+ public boolean onConsoleMessage(android.webkit.ConsoleMessage consoleMessage) {
+ Log.d(TAG, "Console: " + consoleMessage.message());
+ return true;
+ }
+ });
+
+ // 延迟加载
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ webView.loadUrl("file:///android_asset/www/index.html");
+ }, 100);
+
+ Log.d(TAG, "onCreate finished");
+ }
+
+ private void injectDPadSupport() {
+ String js = "javascript:(function() {" +
+ "document.addEventListener('keydown', function(e) {" +
+ " if(e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'ArrowLeft' || e.key === 'ArrowRight') {" +
+ " e.preventDefault();" +
+ " var focusable = document.querySelectorAll('button, a, input, [tabindex]:not([tabindex=\"-1\"])');" +
+ " var current = document.activeElement;" +
+ " var index = Array.prototype.indexOf.call(focusable, current);" +
+ " if(e.key === 'ArrowRight' || e.key === 'ArrowDown') {" +
+ " var next = focusable[index + 1] || focusable[0];" +
+ " next.focus();" +
+ " } else {" +
+ " var prev = focusable[index - 1] || focusable[focusable.length - 1];" +
+ " prev.focus();" +
+ " }" +
+ " }" +
+ " if(e.key === 'Enter') {" +
+ " document.activeElement.click();" +
+ " }" +
+ "});" +
+ "})()";
+ webView.evaluateJavascript(js, null);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK && webView.canGoBack()) {
+ webView.goBack();
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (webView != null) webView.onPause();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (webView != null) webView.onResume();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (webView != null) webView.destroy();
+ }
+}
diff --git a/android-tv/app/src/main/res/drawable/ic_banner.xml b/android-tv/app/src/main/res/drawable/ic_banner.xml
new file mode 100644
index 0000000..2a5088c
--- /dev/null
+++ b/android-tv/app/src/main/res/drawable/ic_banner.xml
@@ -0,0 +1,33 @@
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/android-tv/app/src/main/res/layout/activity_main.xml b/android-tv/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..e66bb2f
--- /dev/null
+++ b/android-tv/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/android-tv/app/src/main/res/values/colors.xml b/android-tv/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..768b058
--- /dev/null
+++ b/android-tv/app/src/main/res/values/colors.xml
@@ -0,0 +1,5 @@
+
+
+ #FF000000
+ #FFFFFFFF
+
diff --git a/android-tv/app/src/main/res/values/strings.xml b/android-tv/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..a89f24e
--- /dev/null
+++ b/android-tv/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ IPTV TV
+
diff --git a/android-tv/app/src/main/res/values/styles.xml b/android-tv/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..24947da
--- /dev/null
+++ b/android-tv/app/src/main/res/values/styles.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/android-tv/build.gradle b/android-tv/build.gradle
new file mode 100644
index 0000000..ef972fe
--- /dev/null
+++ b/android-tv/build.gradle
@@ -0,0 +1,4 @@
+// Top-level build file
+plugins {
+ id 'com.android.application' version '8.1.0' apply false
+}
diff --git a/android-tv/build.sh b/android-tv/build.sh
new file mode 100755
index 0000000..41c8819
--- /dev/null
+++ b/android-tv/build.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+# Android TV WebView 壳打包脚本
+
+set -e
+
+echo "=== IPTV Android TV 构建 ==="
+
+# 检查 UI 构建产物
+if [ ! -d "../ui/dist-web" ]; then
+ echo "错误: 未找到 ../ui/dist-web 目录"
+ echo "请先构建 Web UI: cd ../ui && npm run build"
+ exit 1
+fi
+
+# 复制 Web 资源到 Android assets
+echo "复制 Web 资源..."
+mkdir -p app/src/main/assets/www
+cp -r ../ui/dist-web/* app/src/main/assets/www/
+
+# 统计文件
+echo "已复制文件数量:"
+find app/src/main/assets/www -type f | wc -l
+
+# 构建 Debug APK
+echo "构建 Debug APK..."
+./gradlew assembleDebug
+
+echo ""
+echo "=== 构建完成 ==="
+echo "APK 位置: app/build/outputs/apk/debug/app-debug.apk"
diff --git a/android-tv/gradle.properties b/android-tv/gradle.properties
new file mode 100644
index 0000000..1869e4d
--- /dev/null
+++ b/android-tv/gradle.properties
@@ -0,0 +1,5 @@
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+android.useAndroidX=true
+kotlin.code.style=official
+android.nonTransitiveRClass=true
+android.suppressUnsupportedCompileSdk=34
diff --git a/android-tv/gradle/wrapper/gradle-wrapper.jar b/android-tv/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..033e24c
Binary files /dev/null and b/android-tv/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/android-tv/gradle/wrapper/gradle-wrapper.properties b/android-tv/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..62f495d
--- /dev/null
+++ b/android-tv/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/android-tv/gradlew b/android-tv/gradlew
new file mode 100755
index 0000000..8636023
--- /dev/null
+++ b/android-tv/gradlew
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+# Gradle Wrapper Startup Script
+
+# Find the script's directory
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+WRAPPER_JAR="$SCRIPT_DIR/gradle/wrapper/gradle-wrapper.jar"
+
+# Find Java
+if [ -n "$JAVA_HOME" ] ; then
+ JAVA_CMD="$JAVA_HOME/bin/java"
+else
+ JAVA_CMD="java"
+fi
+
+# Run Gradle wrapper
+exec "$JAVA_CMD" -cp "$WRAPPER_JAR" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/android-tv/settings.gradle b/android-tv/settings.gradle
new file mode 100644
index 0000000..f23c83f
--- /dev/null
+++ b/android-tv/settings.gradle
@@ -0,0 +1,16 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+rootProject.name = "IPTV TV"
+include ':app'