diff --git a/android/README.md b/android/README.md
new file mode 100644
index 0000000..ca2b8f2
--- /dev/null
+++ b/android/README.md
@@ -0,0 +1,71 @@
+# Android WebView 壳
+
+## 目录结构
+
+```
+android/
+├── app/src/main/
+│ ├── java/com/iptv/app/
+│ │ └── MainActivity.java # WebView 主活动
+│ ├── res/
+│ │ ├── layout/activity_main.xml
+│ │ ├── values/
+│ │ │ ├── strings.xml
+│ │ │ ├── styles.xml
+│ │ │ └── colors.xml
+│ │ └── mipmap-*/ # 图标资源
+│ ├── assets/
+│ │ └── www/ # 打包的 Web 资源 (从 ui/dist-web 复制)
+│ └── AndroidManifest.xml
+├── build.gradle # 项目级构建配置
+├── settings.gradle
+└── gradle.properties
+```
+
+## 构建步骤
+
+### 1. 构建 Web UI
+
+```bash
+cd ../ui
+npm install
+npm run build:web
+```
+
+### 2. 复制资源到 Android
+
+```bash
+# 将构建好的 web 资源复制到 Android assets
+cp -r ../ui/dist-web/* app/src/main/assets/www/
+```
+
+### 3. 构建 APK
+
+```bash
+./gradlew assembleDebug
+```
+
+APK 输出位置: `app/build/outputs/apk/debug/app-debug.apk`
+
+## 开发模式
+
+如需连接开发服务器测试,修改 `MainActivity.java`:
+
+```java
+private static final String LOAD_MODE = "remote";
+private static final String REMOTE_URL = "http://你的IP:5173";
+```
+
+## 功能特性
+
+- ✅ WebView 加载 Web UI
+- ✅ 全屏无标题栏
+- ✅ 下拉刷新
+- ✅ 返回键支持页面后退
+- ✅ 视频全屏自动横屏
+- ✅ 暗色主题
+
+## 权限
+
+- `INTERNET` - 网络访问
+- `ACCESS_NETWORK_STATE` - 网络状态检测
diff --git a/android/app/build.gradle b/android/app/build.gradle
new file mode 100644
index 0000000..5219d16
--- /dev/null
+++ b/android/app/build.gradle
@@ -0,0 +1,33 @@
+plugins {
+ id 'com.android.application'
+}
+
+android {
+ namespace 'com.iptv.app'
+ compileSdk 34
+
+ defaultConfig {
+ applicationId "com.iptv.app"
+ 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.appcompat:appcompat:1.6.1'
+ implementation 'com.google.android.material:material:1.11.0'
+ implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
+}
diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
new file mode 100644
index 0000000..f23d2ad
--- /dev/null
+++ b/android/app/proguard-rules.pro
@@ -0,0 +1 @@
+# ProGuard rules
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..d8d1841
--- /dev/null
+++ b/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/assets/error.html b/android/app/src/main/assets/error.html
new file mode 100644
index 0000000..c4b836c
--- /dev/null
+++ b/android/app/src/main/assets/error.html
@@ -0,0 +1,74 @@
+
+
+
+
+
+ 加载错误
+
+
+
+ ⚠️
+ 页面加载失败
+ 无法加载应用资源,可能是以下原因:
+
+
+
可能的解决方案:
+
+ - 检查网络连接是否正常
+ - 清除应用数据后重试
+ - 重新安装应用
+ - 如果是开发模式,请检查远程服务器地址
+
+
+
+
diff --git a/android/app/src/main/java/com/iptv/app/AssetReader.java b/android/app/src/main/java/com/iptv/app/AssetReader.java
new file mode 100644
index 0000000..3a01b97
--- /dev/null
+++ b/android/app/src/main/java/com/iptv/app/AssetReader.java
@@ -0,0 +1,49 @@
+package com.iptv.app;
+
+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 {
+ // path 如: "www/api/result.txt"
+ 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/app/src/main/java/com/iptv/app/MainActivity.java b/android/app/src/main/java/com/iptv/app/MainActivity.java
new file mode 100644
index 0000000..c7ea244
--- /dev/null
+++ b/android/app/src/main/java/com/iptv/app/MainActivity.java
@@ -0,0 +1,231 @@
+package com.iptv.app;
+
+import android.annotation.SuppressLint;
+import android.content.pm.ActivityInfo;
+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.View;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowInsetsController;
+import android.view.WindowManager;
+import android.webkit.WebChromeClient;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+
+public class MainActivity extends AppCompatActivity {
+
+ private static final String TAG = "IPTV";
+ private WebView webView;
+ private SwipeRefreshLayout swipeRefresh;
+
+ // 加载模式:"local" 使用本地打包的web资源,"remote" 使用远程服务器,"test" 使用测试页面
+ private static final String LOAD_MODE = "local";
+
+ // 远程服务器地址(开发测试用)
+ private static final String REMOTE_URL = "http://192.168.1.100:5173";
+
+ @SuppressLint("SetJavaScriptEnabled")
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Log.d(TAG, "onCreate started");
+
+ // 必须在 setContentView 之前调用
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+
+ setContentView(R.layout.activity_main);
+
+ webView = findViewById(R.id.webview);
+ swipeRefresh = findViewById(R.id.swipe_refresh);
+
+ if (webView == null) {
+ Log.e(TAG, "WebView is null!");
+ return;
+ }
+
+ // 全屏设置(在 setContentView 之后)
+ setFullscreen();
+
+ // 添加 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);
+
+ // 支持缩放
+ settings.setSupportZoom(true);
+ settings.setBuiltInZoomControls(true);
+ settings.setDisplayZoomControls(false);
+
+ // 启用硬件加速
+ webView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+
+ // WebViewClient 处理页面跳转
+ webView.setWebViewClient(new WebViewClient() {
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
+ Log.d(TAG, "shouldOverrideUrlLoading: " + request.getUrl());
+ return false; // 在 WebView 内打开链接
+ }
+
+ @Override
+ public void onPageStarted(WebView view, String url, android.graphics.Bitmap favicon) {
+ Log.d(TAG, "onPageStarted: " + url);
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ Log.d(TAG, "onPageFinished: " + url);
+ if (swipeRefresh != null) {
+ swipeRefresh.setRefreshing(false);
+ }
+ }
+
+ @Override
+ public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
+ Log.e(TAG, "onReceivedError: " + errorCode + " - " + description + " - " + failingUrl);
+ }
+ });
+
+ // WebChromeClient 处理全屏视频和 JS 控制台
+ webView.setWebChromeClient(new WebChromeClient() {
+ private View customView;
+ private CustomViewCallback customViewCallback;
+
+ @Override
+ public boolean onConsoleMessage(android.webkit.ConsoleMessage consoleMessage) {
+ Log.d(TAG, "WebView Console: " + consoleMessage.message() + " at " + consoleMessage.sourceId() + ":" + consoleMessage.lineNumber());
+ return true;
+ }
+
+ @Override
+ public void onProgressChanged(WebView view, int newProgress) {
+ Log.d(TAG, "Loading progress: " + newProgress + "%");
+ }
+
+ @Override
+ public void onShowCustomView(View view, CustomViewCallback callback) {
+ customView = view;
+ customViewCallback = callback;
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+ }
+
+ @Override
+ public void onHideCustomView() {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ if (customViewCallback != null) {
+ customViewCallback.onCustomViewHidden();
+ }
+ customView = null;
+ }
+ });
+
+ // 下拉刷新
+ if (swipeRefresh != null) {
+ swipeRefresh.setOnRefreshListener(() -> webView.reload());
+ swipeRefresh.setColorSchemeResources(android.R.color.holo_blue_bright);
+ }
+
+ // 延迟加载页面,确保 WebView 完全初始化
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ loadContent();
+ }, 100);
+
+ Log.d(TAG, "onCreate finished");
+ }
+
+ private void loadContent() {
+ Log.d(TAG, "loadContent, mode: " + LOAD_MODE);
+ String url;
+ if ("test".equals(LOAD_MODE)) {
+ // 加载测试页面(红色背景)
+ url = "file:///android_asset/test.html";
+ } else if ("local".equals(LOAD_MODE)) {
+ // 加载本地打包的web资源
+ url = "file:///android_asset/www/index.html";
+ } else {
+ // 加载远程服务器(开发测试)
+ url = REMOTE_URL;
+ }
+ Log.d(TAG, "Loading URL: " + url);
+ webView.loadUrl(url);
+ }
+
+ private void setFullscreen() {
+ // 设置状态栏和导航栏颜色
+ getWindow().setStatusBarColor(Color.BLACK);
+ getWindow().setNavigationBarColor(Color.BLACK);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ getWindow().setDecorFitsSystemWindows(false);
+ WindowInsetsController controller = getWindow().getInsetsController();
+ if (controller != null) {
+ controller.hide(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars());
+ controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
+ }
+ } else {
+ getWindow().setFlags(
+ WindowManager.LayoutParams.FLAG_FULLSCREEN,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN
+ );
+ getWindow().getDecorView().setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_FULLSCREEN
+ );
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (webView != null && webView.canGoBack()) {
+ webView.goBack();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (webView != null) {
+ webView.onResume();
+ }
+ setFullscreen();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (webView != null) {
+ webView.onPause();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (webView != null) {
+ webView.destroy();
+ }
+ }
+}
diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..16c3960
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..3da829c
--- /dev/null
+++ b/android/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..5ed0a2d
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..768b058
--- /dev/null
+++ b/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,5 @@
+
+
+ #FF000000
+ #FFFFFFFF
+
diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..b9f789b
--- /dev/null
+++ b/android/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #0a0a0a
+
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..abc6c64
--- /dev/null
+++ b/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ IPTV
+
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..ce07cf2
--- /dev/null
+++ b/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android/build.gradle b/android/build.gradle
new file mode 100644
index 0000000..ef972fe
--- /dev/null
+++ b/android/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/build.sh b/android/build.sh
new file mode 100755
index 0000000..31d60aa
--- /dev/null
+++ b/android/build.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+# Android WebView 壳打包脚本
+
+set -e
+
+echo "=== IPTV Android 构建 ==="
+
+# 检查 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/gradle.properties b/android/gradle.properties
new file mode 100644
index 0000000..f0a2e55
--- /dev/null
+++ b/android/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+android.useAndroidX=true
+kotlin.code.style=official
+android.nonTransitiveRClass=true
diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..033e24c
Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..62f495d
--- /dev/null
+++ b/android/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/gradlew b/android/gradlew
new file mode 100755
index 0000000..8636023
--- /dev/null
+++ b/android/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/settings.gradle b/android/settings.gradle
new file mode 100644
index 0000000..8876fa8
--- /dev/null
+++ b/android/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 App"
+include ':app'