feat(android-tv): 创建 Android TV WebView 壳应用
- 添加 TV 专用的 Leanback 主题和配置 - 支持遥控器 D-Pad 导航 - 添加 AssetReader JS 接口 - 强制横屏显示 - 处理遥控器按键(返回、菜单、信息)
This commit is contained in:
parent
327f03c562
commit
2a565fb8da
101
android-tv/README.md
Normal file
101
android-tv/README.md
Normal file
@ -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
|
||||||
|
<!-- TV 必需特性 -->
|
||||||
|
<uses-feature android:name="android.software.leanback" android:required="true" />
|
||||||
|
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||||
|
|
||||||
|
<!-- TV 启动器 -->
|
||||||
|
<intent-filter>
|
||||||
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
```
|
||||||
|
|
||||||
|
### D-Pad 导航支持
|
||||||
|
|
||||||
|
应用自动注入 JavaScript 代码,使 Web 应用支持遥控器方向键导航:
|
||||||
|
|
||||||
|
- **方向键** - 在可聚焦元素间移动
|
||||||
|
- **确认键** - 点击当前聚焦元素
|
||||||
|
- **返回键** - 页面后退/退出应用
|
||||||
|
- **菜单键** - 刷新页面
|
||||||
|
- **信息键** - 显示版本信息
|
||||||
|
|
||||||
|
## 遥控器按键映射
|
||||||
|
|
||||||
|
| 按键 | 功能 |
|
||||||
|
|------|------|
|
||||||
|
| 上/下/左/右 | 导航焦点 |
|
||||||
|
| 确认/OK | 点击 |
|
||||||
|
| 返回 | 页面后退 |
|
||||||
|
| 菜单 | 刷新页面 |
|
||||||
|
| 信息/INFO | 显示版本 |
|
||||||
|
|
||||||
|
## 开发与调试
|
||||||
|
|
||||||
|
1. **ADB 连接 TV**
|
||||||
|
```bash
|
||||||
|
adb connect <TV_IP>: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://<dev_server>:5173"
|
||||||
|
```
|
||||||
33
android-tv/app/build.gradle
Normal file
33
android-tv/app/build.gradle
Normal file
@ -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'
|
||||||
|
}
|
||||||
1
android-tv/app/proguard-rules.pro
vendored
Normal file
1
android-tv/app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
# ProGuard rules
|
||||||
41
android-tv/app/src/main/AndroidManifest.xml
Normal file
41
android-tv/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.iptv.tv">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
|
<!-- TV 特定配置 -->
|
||||||
|
<uses-feature android:name="android.software.leanback" android:required="true" />
|
||||||
|
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@drawable/ic_banner"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/AppTheme"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
|
android:banner="@drawable/ic_banner">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:configChanges="orientation|screenSize|keyboardHidden|smallestScreenSize"
|
||||||
|
android:screenOrientation="landscape"
|
||||||
|
android:theme="@style/AppTheme.NoActionBar">
|
||||||
|
|
||||||
|
<!-- TV Launcher -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- 普通 Launcher(用于调试) -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
48
android-tv/app/src/main/java/com/iptv/tv/AssetReader.java
Normal file
48
android-tv/app/src/main/java/com/iptv/tv/AssetReader.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
148
android-tv/app/src/main/java/com/iptv/tv/MainActivity.java
Normal file
148
android-tv/app/src/main/java/com/iptv/tv/MainActivity.java
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
33
android-tv/app/src/main/res/drawable/ic_banner.xml
Normal file
33
android-tv/app/src/main/res/drawable/ic_banner.xml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#0a0a0a" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item
|
||||||
|
android:left="48dp"
|
||||||
|
android:right="48dp"
|
||||||
|
android:top="20dp"
|
||||||
|
android:bottom="20dp"
|
||||||
|
android:gravity="center">
|
||||||
|
<vector
|
||||||
|
android:width="160dp"
|
||||||
|
android:height="60dp"
|
||||||
|
android:viewportWidth="160"
|
||||||
|
android:viewportHeight="60">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M15,10 L15,50 L23,50 L23,34 L40,50 L52,50 L52,10 L40,10 L23,26 L23,10 Z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M65,20 L65,25 L70,25 L70,35 L65,35 L65,40 L85,40 L85,35 L90,35 L90,25 L85,25 L85,20 Z M75,30 L80,30 L80,32 L75,32 Z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M95,20 L95,40 L105,40 L105,35 L115,35 L115,40 L125,40 L125,20 L115,20 L115,25 L105,25 L105,20 Z M105,30 L115,30 L115,32 L105,32 Z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M130,20 L130,40 L150,40 L150,32 L140,32 L140,20 Z"/>
|
||||||
|
</vector>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
17
android-tv/app/src/main/res/layout/activity_main.xml
Normal file
17
android-tv/app/src/main/res/layout/activity_main.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/swipe_refresh"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#0a0a0a">
|
||||||
|
|
||||||
|
<WebView
|
||||||
|
android:id="@+id/webview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#0a0a0a"
|
||||||
|
android:focusable="true"
|
||||||
|
android:focusableInTouchMode="true" />
|
||||||
|
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
5
android-tv/app/src/main/res/values/colors.xml
Normal file
5
android-tv/app/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
</resources>
|
||||||
3
android-tv/app/src/main/res/values/strings.xml
Normal file
3
android-tv/app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">IPTV TV</string>
|
||||||
|
</resources>
|
||||||
12
android-tv/app/src/main/res/values/styles.xml
Normal file
12
android-tv/app/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<resources>
|
||||||
|
<style name="AppTheme" parent="Theme.Leanback">
|
||||||
|
<item name="android:windowNoTitle">true</item>
|
||||||
|
<item name="android:windowFullscreen">true</item>
|
||||||
|
<item name="android:windowBackground">@android:color/black</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="AppTheme.NoActionBar" parent="AppTheme">
|
||||||
|
<item name="android:windowActionBar">false</item>
|
||||||
|
<item name="android:windowNoTitle">true</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
4
android-tv/build.gradle
Normal file
4
android-tv/build.gradle
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// Top-level build file
|
||||||
|
plugins {
|
||||||
|
id 'com.android.application' version '8.1.0' apply false
|
||||||
|
}
|
||||||
31
android-tv/build.sh
Executable file
31
android-tv/build.sh
Executable file
@ -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"
|
||||||
5
android-tv/gradle.properties
Normal file
5
android-tv/gradle.properties
Normal file
@ -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
|
||||||
BIN
android-tv/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
android-tv/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
android-tv/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
android-tv/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -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
|
||||||
17
android-tv/gradlew
vendored
Executable file
17
android-tv/gradlew
vendored
Executable file
@ -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 "$@"
|
||||||
16
android-tv/settings.gradle
Normal file
16
android-tv/settings.gradle
Normal file
@ -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'
|
||||||
Loading…
x
Reference in New Issue
Block a user