一些修改提交

This commit is contained in:
2026-06-17 15:35:24 +08:00
parent f3dff010fd
commit 494053be1a
23 changed files with 1150 additions and 245 deletions
+16
View File
@@ -0,0 +1,16 @@
# CodeGraph data files
# These are local to each machine and should not be committed
# Database
*.db
*.db-wal
*.db-shm
# Cache
cache/
# Logs
*.log
# Hook markers
.dirty
+6
View File
@@ -0,0 +1,6 @@
{
"pid": 38368,
"version": "0.9.7",
"socketPath": "\\\\.\\pipe\\codegraph-cb339f1c8a0989e3",
"startedAt": 1781679324951
}
+3
View File
@@ -0,0 +1,3 @@
{
"topic_20260617-065519_fbde6a25cff2683a": 1781679319697
}
@@ -0,0 +1,3 @@
{
"topic_20260617-065519_fbde6a25cff2683a": "auto"
}
+3
View File
@@ -0,0 +1,3 @@
{
"topic_20260617-065519_fbde6a25cff2683a": "我要给电视盒子做一个非常简单的APP…"
}
+10 -4
View File
@@ -27,11 +27,14 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = "17"
}
buildFeatures {
viewBinding = true
}
}
@@ -42,7 +45,10 @@ dependencies {
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.exoplayer.hls)
implementation(libs.androidx.media3.ui)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
}
+25 -3
View File
@@ -2,6 +2,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -11,16 +17,32 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.FengTv"
android:banner="@mipmap/ic_launcher"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
android:exported="true"
android:excludeFromRecents="false"
android:launchMode="singleTask"
android:screenOrientation="landscape"
android:configChanges="orientation|keyboardHidden|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<receiver
android:name=".BootReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
</manifest>
</manifest>
@@ -0,0 +1,15 @@
package cn.flyzxf.FengTv
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
launchIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(launchIntent)
}
}
}
@@ -0,0 +1,6 @@
package cn.flyzxf.FengTv
data class ChannelInfo(
val name: String,
val url: String
)
@@ -0,0 +1,101 @@
package cn.flyzxf.FengTv
import android.content.Context
import android.content.SharedPreferences
import java.io.BufferedReader
import java.io.File
import java.io.FileReader
import java.io.FileWriter
object ChannelManager {
private const val PREFS_NAME = "fengtv_prefs"
private const val KEY_SOURCE_URL = "source_url"
private const val KEY_CURRENT_INDEX = "current_index"
private const val CHANNEL_FILE = "channels.txt"
private var channels: MutableList<ChannelInfo> = mutableListOf()
var currentIndex: Int = 0
private set
fun loadChannels(context: Context): List<ChannelInfo> {
val file = File(context.filesDir, CHANNEL_FILE)
channels.clear()
if (file.exists()) {
try {
BufferedReader(FileReader(file)).use { reader ->
reader.forEachLine { line ->
val trimmed = line.trim()
if (trimmed.isNotEmpty() && !trimmed.startsWith("#")) {
val commaIdx = trimmed.indexOf(',')
if (commaIdx > 0 && commaIdx < trimmed.length - 1) {
val name = trimmed.substring(0, commaIdx).trim()
val url = trimmed.substring(commaIdx + 1).trim()
if (url.isNotEmpty()) {
channels.add(ChannelInfo(name, url))
}
}
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
val prefs = getPrefs(context)
currentIndex = prefs.getInt(KEY_CURRENT_INDEX, 0)
if (currentIndex >= channels.size) currentIndex = 0
return channels.toList()
}
fun saveChannels(context: Context, lines: List<String>) {
val file = File(context.filesDir, CHANNEL_FILE)
try {
FileWriter(file).use { writer ->
lines.forEach { line ->
writer.write(line + "\n")
}
}
} catch (e: Exception) {
e.printStackTrace()
}
loadChannels(context)
}
fun getSavedSourceUrl(context: Context): String? {
return getPrefs(context).getString(KEY_SOURCE_URL, null)
}
fun saveSourceUrl(context: Context, url: String) {
getPrefs(context).edit().putString(KEY_SOURCE_URL, url).apply()
}
fun saveCurrentIndex(context: Context, index: Int) {
currentIndex = index
getPrefs(context).edit().putInt(KEY_CURRENT_INDEX, index).apply()
}
fun getChannelCount(): Int = channels.size
fun getChannel(index: Int): ChannelInfo? {
if (index in channels.indices) return channels[index]
return null
}
fun getCurrentChannel(): ChannelInfo? = getChannel(currentIndex)
fun nextChannel(): ChannelInfo? {
if (channels.isEmpty()) return null
currentIndex = (currentIndex + 1) % channels.size
return channels[currentIndex]
}
fun prevChannel(): ChannelInfo? {
if (channels.isEmpty()) return null
currentIndex = if (currentIndex - 1 < 0) channels.size - 1 else currentIndex - 1
return channels[currentIndex]
}
private fun getPrefs(context: Context): SharedPreferences {
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
}
}
@@ -0,0 +1,106 @@
package cn.flyzxf.FengTv
import android.app.AlertDialog
import android.app.Dialog
import android.net.Uri
import android.os.Bundle
import android.text.InputType
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.DialogFragment
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.URL
class ConfigDialogFragment : DialogFragment() {
private val filePickerLauncher = registerForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri: Uri? ->
uri?.let { handleFileUri(it) }
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val items = arrayOf("输入 M3U8 源链接", "选择本地 TXT 文件", "取消")
return AlertDialog.Builder(requireActivity())
.setTitle("设置电视源")
.setItems(items) { _, which ->
when (which) {
0 -> showUrlInputDialog()
1 -> filePickerLauncher.launch(arrayOf("text/plain"))
}
}
.create()
}
private fun showUrlInputDialog() {
// 提前捕获 activity 引用,防止回调时 Fragment 已被销毁导致 IllegalStateException
val activity = requireActivity() as MainActivity
val context = requireContext()
val input = EditText(context)
input.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI
input.hint = "https://example.com/channels.txt"
val layout = LinearLayout(context).apply {
setPadding(50, 20, 50, 20)
addView(input, LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
))
}
AlertDialog.Builder(activity)
.setTitle("输入 M3U8 源链接")
.setView(layout)
.setPositiveButton("确定") { _, _ ->
val url = input.text.toString().trim()
if (url.isNotEmpty()) {
// 简单验证 URL 格式
if (!isValidUrl(url)) {
Toast.makeText(context, "链接格式不正确,请检查后重试", Toast.LENGTH_LONG).show()
return@setPositiveButton
}
activity.onConfigSaved(url)
}
}
.setNegativeButton("取消", null)
.show()
}
private fun isValidUrl(url: String): Boolean {
return try {
val u = URL(url)
u.protocol == "http" || u.protocol == "https"
} catch (e: Exception) {
false
}
}
private fun handleFileUri(uri: Uri) {
try {
// 提前捕获引用
val context = requireContext()
val activity = requireActivity() as MainActivity
val inputStream = context.contentResolver.openInputStream(uri)
val lines = mutableListOf<String>()
BufferedReader(InputStreamReader(inputStream, "UTF-8")).use { reader ->
reader.forEachLine { lines.add(it) }
}
ChannelManager.saveChannels(context, lines)
activity.startPlayback()
Toast.makeText(context, "已加载本地文件", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
// 此时如果 Fragment 已分离,requireContext() 可能抛异常,用安全方式获取
try {
Toast.makeText(requireContext(), "读取文件失败", Toast.LENGTH_LONG).show()
} catch (e2: Exception) {
// 忽略,Fragment 已销毁
}
}
}
}
@@ -1,20 +1,592 @@
package cn.flyzxf.FengTv
import android.content.SharedPreferences
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.KeyEvent
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ImageButton
import android.widget.ListView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView
import java.io.BufferedReader
import java.io.File
import java.io.FileReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import java.util.concurrent.Executors
class MainActivity : AppCompatActivity() {
private lateinit var playerView: PlayerView
private lateinit var channelNameOverlay: TextView
private lateinit var loadingOverlay: View
private lateinit var loadingText: TextView
private lateinit var bottomToolbar: View
private lateinit var channelPanel: View
private lateinit var channelListView: ListView
private lateinit var toolbarIndicator: TextView
private lateinit var btnPrev: ImageButton
private lateinit var btnNext: ImageButton
private lateinit var btnToggleReverse: ImageButton
private lateinit var btnShowList: ImageButton
private lateinit var btnSettings: ImageButton
private lateinit var channelPanelClose: TextView
private lateinit var switchingIndicator: View
private lateinit var switchingChannelNum: TextView
private lateinit var switchingChannelTotal: TextView
private lateinit var switchingStatus: TextView
private var player: ExoPlayer? = null
private val mainHandler = Handler(Looper.getMainLooper())
private val executor = Executors.newSingleThreadExecutor()
private var retryCount = 0
private val maxRetries = 3
private var isFirstLaunch = true
private var hasShownNetworkRetryToast = false
private var isReverseOrder = false
private var isToolbarShowing = false
// 频道名消失 Runnable
private val hideChannelNameRunnable = Runnable {
channelNameOverlay.visibility = View.GONE
}
// 工具栏消失 Runnable
private val hideToolbarRunnable = Runnable {
hideToolbar()
}
companion object {
private const val PREFS_NAME = "fengtv_prefs"
private const val KEY_SOURCE_URL = "source_url"
private const val KEY_FIRST_LAUNCH = "first_launch"
private const val KEY_HAS_CONFIG = "has_config"
private const val KEY_REVERSE = "reverse_order"
private const val TIMEOUT_MS = 3000
private const val CHANNEL_NAME_DURATION_MS = 3000L
private const val NETWORK_DELAY_MS = 5000L
private const val TOOLBAR_DURATION_MS = 5000L
private const val LOADING_TIMEOUT_MS = 4000L
private const val SWITCH_STATUS_DURATION_MS = 2000L
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
playerView = findViewById(R.id.player_view)
channelNameOverlay = findViewById(R.id.channel_name_overlay)
loadingOverlay = findViewById(R.id.loading_overlay)
loadingText = findViewById(R.id.loading_text)
bottomToolbar = findViewById(R.id.bottom_toolbar)
channelPanel = findViewById(R.id.channel_panel)
channelListView = findViewById(R.id.channel_list_view)
toolbarIndicator = findViewById(R.id.toolbar_channel_indicator)
btnPrev = findViewById(R.id.btn_prev_channel)
btnNext = findViewById(R.id.btn_next_channel)
btnToggleReverse = findViewById(R.id.btn_toggle_reverse)
btnShowList = findViewById(R.id.btn_show_list)
btnSettings = findViewById(R.id.btn_settings)
channelPanelClose = findViewById(R.id.channel_panel_close)
switchingIndicator = findViewById(R.id.switching_indicator)
switchingChannelNum = findViewById(R.id.switching_channel_num)
switchingChannelTotal = findViewById(R.id.switching_channel_total)
switchingStatus = findViewById(R.id.switching_status)
playerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
// 读取反转设置
val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
isReverseOrder = prefs.getBoolean(KEY_REVERSE, false)
updateReverseButton()
registerNetworkCallback()
setupControls()
isFirstLaunch = prefs.getBoolean(KEY_FIRST_LAUNCH, true)
if (isFirstLaunch) {
prefs.edit().putBoolean(KEY_FIRST_LAUNCH, false).apply()
showConfigDialog()
} else {
startPlayback()
}
}
}
// ======================== 控件绑定 ========================
private fun setupControls() {
// 点击画面切换工具栏
findViewById<View>(R.id.main).setOnClickListener {
toggleToolbar()
}
// 上一台
btnPrev.setOnClickListener { switchChannel(false) }
btnPrev.setOnLongClickListener {
showChannelPanel()
true
}
// 下一台
btnNext.setOnClickListener { switchChannel(true) }
btnNext.setOnLongClickListener {
showChannelPanel()
true
}
// 换台反转
btnToggleReverse.setOnClickListener {
isReverseOrder = !isReverseOrder
getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
.edit().putBoolean(KEY_REVERSE, isReverseOrder).apply()
updateReverseButton()
Toast.makeText(this, if (isReverseOrder) "已反转换台方向" else "已恢复默认方向", Toast.LENGTH_SHORT).show()
refreshToolbar()
}
// 频道列表
btnShowList.setOnClickListener { showChannelPanel() }
// 设置
btnSettings.setOnClickListener { showConfigDialog() }
// 频道面板关闭
channelPanelClose.setOnClickListener { hideChannelPanel() }
// 频道列表点击
channelListView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
val channels = ChannelManager.loadChannels(this)
if (position in channels.indices) {
ChannelManager.saveCurrentIndex(this@MainActivity, position)
playChannel(channels[position])
hideChannelPanel()
}
}
}
// ======================== 工具栏 ========================
private fun toggleToolbar() {
if (channelPanel.visibility == View.VISIBLE) return
if (isToolbarShowing) {
hideToolbar()
} else {
showToolbar()
}
}
private fun showToolbar() {
refreshToolbar()
bottomToolbar.visibility = View.VISIBLE
isToolbarShowing = true
mainHandler.removeCallbacks(hideToolbarRunnable)
mainHandler.postDelayed(hideToolbarRunnable, TOOLBAR_DURATION_MS)
}
private fun hideToolbar() {
bottomToolbar.visibility = View.GONE
isToolbarShowing = false
mainHandler.removeCallbacks(hideToolbarRunnable)
}
private fun refreshToolbar() {
val channel = ChannelManager.getCurrentChannel()
val total = ChannelManager.getChannelCount()
val num = ChannelManager.currentIndex + 1
toolbarIndicator.text = if (channel != null) "$num/$total ${channel.name}" else "无频道"
}
// ======================== 频道列表面板 ========================
private fun showChannelPanel() {
hideToolbar()
val channels = ChannelManager.loadChannels(this)
if (channels.isEmpty()) {
Toast.makeText(this, "没有频道", Toast.LENGTH_SHORT).show()
return
}
val names = channels.mapIndexed { index, ch ->
val prefix = if (index == ChannelManager.currentIndex) "" else " "
"$prefix${index + 1}. ${ch.name}"
}
val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, names)
channelListView.adapter = adapter
channelListView.setSelection(ChannelManager.currentIndex)
channelPanel.visibility = View.VISIBLE
channelListView.requestFocus()
}
private fun hideChannelPanel() {
channelPanel.visibility = View.GONE
}
// ======================== 反转按钮 ========================
private fun updateReverseButton() {
btnToggleReverse.alpha = if (isReverseOrder) 1.0f else 0.4f
}
// ======================== 按键 & 触摸 ========================
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
return when (keyCode) {
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_CHANNEL_UP -> {
if (channelPanel.visibility == View.VISIBLE) {
event?.let { channelListView.dispatchKeyEvent(it) }
return true
}
switchChannel(true)
true
}
KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_CHANNEL_DOWN -> {
if (channelPanel.visibility == View.VISIBLE) {
event?.let { channelListView.dispatchKeyEvent(it) }
return true
}
switchChannel(false)
true
}
KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_ENTER -> {
if (channelPanel.visibility == View.VISIBLE) {
// 选中当前项
val pos = channelListView.selectedItemPosition
if (pos >= 0) {
channelListView.performItemClick(null, pos, 0)
}
return true
}
showChannelName()
true
}
KeyEvent.KEYCODE_BACK -> {
if (channelPanel.visibility == View.VISIBLE) {
hideChannelPanel()
return true
}
if (isToolbarShowing) {
hideToolbar()
return true
}
super.onKeyDown(keyCode, event)
}
KeyEvent.KEYCODE_SETTINGS, KeyEvent.KEYCODE_MENU -> {
showConfigDialog()
true
}
else -> super.onKeyDown(keyCode, event)
}
}
// ======================== 配置 ========================
private fun showConfigDialog() {
val configDialog = ConfigDialogFragment()
configDialog.show(supportFragmentManager, "config_dialog")
}
fun onConfigSaved(url: String) {
val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
prefs.edit().putString(KEY_SOURCE_URL, url).putBoolean(KEY_HAS_CONFIG, true).apply()
ChannelManager.saveSourceUrl(this, url)
startPlayback()
}
fun onLocalFileSelected(filePath: String) {
try {
val file = File(filePath)
if (file.exists()) {
val lines = mutableListOf<String>()
BufferedReader(FileReader(file)).use { reader ->
reader.forEachLine { lines.add(it) }
}
ChannelManager.saveChannels(this, lines)
startPlayback()
}
} catch (e: Exception) {
Toast.makeText(this, "读取文件失败: ${e.message}", Toast.LENGTH_LONG).show()
}
}
// ======================== 播放逻辑 ========================
fun startPlayback() {
val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
val hasConfig = prefs.getBoolean(KEY_HAS_CONFIG, false)
if (!hasConfig) {
showConfigDialog()
return
}
hasShownNetworkRetryToast = false
val channels = ChannelManager.loadChannels(this)
if (channels.isNotEmpty()) {
playChannel(ChannelManager.getCurrentChannel()!!)
} else {
loadingOverlay.visibility = View.VISIBLE
loadingText.text = "正在加载频道列表..."
loadingText.visibility = View.VISIBLE
}
val sourceUrl = ChannelManager.getSavedSourceUrl(this)
if (sourceUrl != null) {
executor.execute {
downloadAndUpdateSource(sourceUrl)
}
}
}
private fun downloadAndUpdateSource(sourceUrl: String) {
try {
val url = URL(sourceUrl)
val conn = url.openConnection() as HttpURLConnection
conn.connectTimeout = TIMEOUT_MS
conn.readTimeout = TIMEOUT_MS
conn.instanceFollowRedirects = true
val responseCode = conn.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
val lines = mutableListOf<String>()
BufferedReader(InputStreamReader(conn.inputStream, "UTF-8")).use { reader ->
reader.forEachLine { lines.add(it) }
}
if (lines.isNotEmpty()) {
val channelCountBefore = ChannelManager.getChannelCount()
ChannelManager.saveChannels(this, lines)
val newCount = ChannelManager.getChannelCount()
mainHandler.post {
if (channelCountBefore == 0 && newCount > 0) {
playChannel(ChannelManager.getCurrentChannel()!!)
} else {
showChannelName()
}
}
}
} else {
mainHandler.post {
Toast.makeText(this@MainActivity, "下载源失败: HTTP $responseCode", Toast.LENGTH_SHORT).show()
}
}
conn.disconnect()
} catch (e: Exception) {
e.printStackTrace()
if (ChannelManager.getChannelCount() == 0) {
if (!hasShownNetworkRetryToast) {
hasShownNetworkRetryToast = true
mainHandler.post {
Toast.makeText(this@MainActivity, "无法连接服务器,将在后台继续尝试...", Toast.LENGTH_LONG).show()
}
}
mainHandler.postDelayed({
executor.execute { downloadAndUpdateSource(sourceUrl) }
}, NETWORK_DELAY_MS)
} else {
mainHandler.post {
Toast.makeText(this@MainActivity, "更新源失败,使用缓存", Toast.LENGTH_SHORT).show()
}
}
}
}
// 加载超时 Runnable
private var loadingTimeoutRunnable: Runnable? = null
private fun showSwitchingIndicator() {
val total = ChannelManager.getChannelCount()
val num = ChannelManager.currentIndex + 1
switchingChannelNum.text = "$num"
switchingChannelTotal.text = "/$total"
switchingStatus.text = ""
switchingIndicator.visibility = View.VISIBLE
}
private fun showSwitchingStatus(text: String) {
switchingStatus.text = text
switchingIndicator.visibility = View.VISIBLE
}
private fun hideSwitchingIndicator() {
switchingIndicator.visibility = View.GONE
}
private fun startLoadingTimeout() {
cancelLoadingTimeout()
loadingTimeoutRunnable = Runnable {
// 加载超时,自动随机跳到另一个台(避免两个坏台间死循环)
showSwitchingStatus("超时,随机切换")
player?.stop()
mainHandler.postDelayed({
autoNextChannel()
}, 500L)
}
loadingTimeoutRunnable?.let {
mainHandler.postDelayed(it, LOADING_TIMEOUT_MS)
}
}
private fun cancelLoadingTimeout() {
loadingTimeoutRunnable?.let { mainHandler.removeCallbacks(it) }
loadingTimeoutRunnable = null
}
private fun playChannel(channel: ChannelInfo) {
loadingOverlay.visibility = View.VISIBLE
loadingText.text = "加载中..."
loadingText.visibility = View.VISIBLE
retryCount = 0
hideToolbar()
// 显示左上角切换指示(频道序号)
showSwitchingIndicator()
// 启动加载超时定时器
startLoadingTimeout()
player?.release()
player = ExoPlayer.Builder(this).build().apply {
setMediaItem(MediaItem.fromUri(Uri.parse(channel.url)))
prepare()
playWhenReady = true
addListener(object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
if (playbackState == Player.STATE_READY) {
cancelLoadingTimeout()
hideSwitchingIndicator()
loadingOverlay.visibility = View.GONE
loadingText.visibility = View.GONE
showChannelName()
}
}
override fun onPlayerError(error: PlaybackException) {
cancelLoadingTimeout()
loadingOverlay.visibility = View.GONE
loadingText.visibility = View.GONE
if (retryCount < maxRetries) {
retryCount++
showSwitchingStatus("重试 $retryCount/$maxRetries")
mainHandler.postDelayed({
player?.seekTo(0)
player?.prepare()
// 重新设定加载超时(重试也算)
startLoadingTimeout()
}, 1000L)
} else {
showSwitchingStatus("加载失败,随机切换")
mainHandler.postDelayed({
autoNextChannel()
}, 800L)
}
}
})
}
playerView.player = player
ChannelManager.saveCurrentIndex(this, ChannelManager.currentIndex)
}
private fun switchChannel(next: Boolean) {
val channel = if (isReverseOrder xor next) {
ChannelManager.prevChannel()
} else {
ChannelManager.nextChannel()
}
if (channel != null) {
playChannel(channel)
}
}
private fun autoNextChannel() {
val channels = ChannelManager.loadChannels(this)
if (channels.isEmpty()) return
if (channels.size == 1) {
playChannel(channels[0])
return
}
// 随机跳到另一个台,避免在两个坏台间无限循环
var nextIndex: Int
do {
nextIndex = (0 until channels.size).random()
} while (nextIndex == ChannelManager.currentIndex && channels.size > 1)
ChannelManager.saveCurrentIndex(this, nextIndex)
ChannelManager.getChannel(nextIndex)?.let { playChannel(it) }
}
private fun showChannelName() {
val channel = ChannelManager.getCurrentChannel()
if (channel != null) {
val total = ChannelManager.getChannelCount()
val num = ChannelManager.currentIndex + 1
channelNameOverlay.text = "[$num/$total] " + channel.name
channelNameOverlay.visibility = View.VISIBLE
mainHandler.removeCallbacks(hideChannelNameRunnable)
mainHandler.postDelayed(hideChannelNameRunnable, CHANNEL_NAME_DURATION_MS)
refreshToolbar()
}
}
// ======================== 网络回调 ========================
private fun registerNetworkCallback() {
val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
val builder = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
connectivityManager.registerNetworkCallback(builder.build(), object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
val sourceUrl = ChannelManager.getSavedSourceUrl(this@MainActivity)
if (sourceUrl != null && ChannelManager.getChannelCount() > 0) {
executor.execute {
downloadAndUpdateSource(sourceUrl)
}
}
}
})
}
// ======================== 生命周期 ========================
override fun onResume() {
super.onResume()
player?.play()
}
override fun onPause() {
super.onPause()
player?.pause()
}
override fun onDestroy() {
super.onDestroy()
player?.release()
executor.shutdown()
mainHandler.removeCallbacksAndMessages(null)
}
}
@@ -1,170 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- 纯深蓝色背景 -->
<path
android:fillColor="#3DDC84"
android:fillColor="#0D47A1"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -1,30 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<!-- 背景色(深蓝圆角方块) -->
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
android:pathData="M14,24 L94,24 Q100,24 100,30 L100,86 Q100,92 94,92 L14,92 Q8,92 8,86 L8,30 Q8,24 14,24 Z"
android:fillColor="#1565C0" />
<!-- 屏幕高光(内部梯形) -->
<path
android:pathData="M15,28 L93,28 L93,76 L15,76 Z"
android:fillColor="#1976D2" />
<!-- 屏幕浅色区域 -->
<path
android:pathData="M18,32 L90,32 Q91,32 91,33 L91,74 Q91,75 90,75 L18,75 Q17,75 17,74 L17,33 Q17,32 18,32 Z"
android:fillColor="#42A5F5" />
<!-- 播放按钮(白色三角) -->
<path
android:pathData="M42,42 L42,66 L68,54 Z"
android:fillColor="#FFFFFF" />
<!-- 底座 -->
<path
android:pathData="M44,92 L64,92 L64,98 L44,98 Z"
android:fillColor="#1565C0" />
<!-- 底座阴影 -->
<path
android:pathData="M42,98 L66,98 L66,100 L42,100 Z"
android:fillColor="#0D47A1" />
</vector>
+222 -9
View File
@@ -1,19 +1,232 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
android:keepScreenOn="true">
<!-- 视频播放器(隐藏默认控制器) -->
<androidx.media3.ui.PlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
app:use_controller="false" />
<!-- 右上角频道名 -->
<TextView
android:id="@+id/channel_name_overlay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_marginTop="20dp"
android:layout_marginEnd="20dp"
android:background="#88000000"
android:padding="12dp"
android:textColor="#FFFFFF"
android:textSize="24sp"
android:textStyle="bold"
android:visibility="gone" />
<!-- 左上角切换指示(频道序号 + 状态) -->
<LinearLayout
android:id="@+id/switching_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|start"
android:layout_marginTop="20dp"
android:layout_marginStart="20dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone">
<TextView
android:id="@+id/switching_channel_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#FFFF00"
android:textSize="56sp"
android:textStyle="bold" />
<TextView
android:id="@+id/switching_channel_total"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:paddingTop="20dp"
android:textColor="#FFFF00"
android:textSize="18sp" />
<TextView
android:id="@+id/switching_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:paddingTop="18dp"
android:textColor="#FFCC00"
android:textSize="16sp" />
</LinearLayout>
<!-- 加载中遮罩 -->
<View
android:id="@+id/loading_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF000000"
android:visibility="gone" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:layout_gravity="center"
android:text="加载中..."
android:textColor="#FFFFFF"
android:textSize="18sp"
android:id="@+id/loading_text"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- ============ 频道列表覆盖层 ============ -->
<LinearLayout
android:id="@+id/channel_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#CC000000"
android:orientation="vertical"
android:visibility="gone">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="#88000000"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="20dp"
android:text="频道列表"
android:textColor="#FFFFFF"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/channel_panel_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="20dp"
android:background="?attr/selectableItemBackground"
android:padding="12dp"
android:text="✕ 关闭"
android:textColor="#FFAAAA"
android:textSize="16sp" />
</RelativeLayout>
<ListView
android:id="@+id/channel_list_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="#00000000"
android:cacheColorHint="#00000000"
android:divider="#33FFFFFF"
android:dividerHeight="1dp"
android:scrollbars="vertical" />
</LinearLayout>
<!-- ============ 底部工具栏 ============ -->
<LinearLayout
android:id="@+id/bottom_toolbar"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_gravity="bottom"
android:background="#CC000000"
android:gravity="center"
android:orientation="horizontal"
android:paddingHorizontal="8dp"
android:visibility="gone">
<!-- 上一台 -->
<ImageButton
android:id="@+id/btn_prev_channel"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_margin="4dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="center"
android:src="@android:drawable/ic_media_previous"
android:contentDescription="上一台" />
<!-- 下一台 -->
<ImageButton
android:id="@+id/btn_next_channel"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_margin="4dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="center"
android:src="@android:drawable/ic_media_next"
android:contentDescription="下一台" />
<!-- 间隔 -->
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<!-- 频道指示器 -->
<TextView
android:id="@+id/toolbar_channel_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:gravity="center"
android:textColor="#FFFFFF"
android:textSize="14sp" />
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<!-- 换台反转 -->
<ImageButton
android:id="@+id/btn_toggle_reverse"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_margin="4dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="center"
android:src="@android:drawable/ic_menu_sort_by_size"
android:contentDescription="换台反转" />
<!-- 频道列表 -->
<ImageButton
android:id="@+id/btn_show_list"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_margin="4dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="center"
android:src="@android:drawable/ic_menu_agenda"
android:contentDescription="频道列表" />
<!-- 设置 -->
<ImageButton
android:id="@+id/btn_settings"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_margin="4dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="center"
android:src="@android:drawable/ic_menu_manage"
android:contentDescription="设置" />
</LinearLayout>
</FrameLayout>
+7 -6
View File
@@ -1,7 +1,8 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.FengTv" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.FengTv" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowFullscreen">true</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowBackground">@android:color/black</item>
</style>
</resources>
</resources>
+1 -1
View File
@@ -2,4 +2,4 @@
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
</resources>
+3 -2
View File
@@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">FengTv</string>
</resources>
<string name="app_name">FengTV</string>
</resources>
+7 -8
View File
@@ -1,9 +1,8 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.FengTv" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.FengTv" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowFullscreen">true</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowBackground">@android:color/black</item>
</style>
<style name="Theme.FengTv" parent="Base.Theme.FengTv" />
</resources>
</resources>
+4 -19
View File
@@ -1,23 +1,8 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
org.gradle.java.home=C\:/Program Files/Java/jdk-17
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.nonTransitiveRClass=true
systemProp.gradle.wrapperUser=
systemProp.gradle.wrapperPassword=
+6 -3
View File
@@ -1,6 +1,6 @@
[versions]
agp = "8.4.0-rc02"
kotlin = "1.9.0"
agp = "8.4.0"
kotlin = "1.9.22"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
@@ -9,6 +9,7 @@ appcompat = "1.6.1"
material = "1.10.0"
activity = "1.8.0"
constraintlayout = "2.1.4"
media3 = "1.2.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -19,8 +20,10 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" }
androidx-media3-exoplayer-hls = { group = "androidx.media3", name = "media3-exoplayer-hls", version.ref = "media3" }
androidx-media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+1 -2
View File
@@ -1,6 +1,5 @@
#Wed Jun 17 14:18:24 CST 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.6-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+6 -1
View File
@@ -1,5 +1,8 @@
pluginManagement {
repositories {
maven { setUrl("https://maven.aliyun.com/repository/public") }
maven { setUrl("https://maven.aliyun.com/repository/google") }
maven { setUrl("https://maven.aliyun.com/repository/gradle-plugin") }
google {
content {
includeGroupByRegex("com\\.android.*")
@@ -14,6 +17,8 @@ pluginManagement {
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
maven { setUrl("https://maven.aliyun.com/repository/public") }
maven { setUrl("https://maven.aliyun.com/repository/google") }
google()
mavenCentral()
}
@@ -21,4 +26,4 @@ dependencyResolutionManagement {
rootProject.name = "FengTv"
include(":app")