@@ -99,9 +99,15 @@ class MainActivity : Activity() {
private var confirmOverlay : View ? = null
/** 右上角「立即授权」按钮(未授权时显示) */
private var authBtn : TextView ? = null
/** 右下角「已授权」小字标识(已授权时显示,未授权隐藏) */
private var authStateText : TextView ? = null
/** 首次启动简易教程遮罩 */
private var tutorialOverlay : View ? = null
private var tutorialVisible = false
/** 更新弹窗的可聚焦按钮 + 当前焦点索引(遥控器左右切换用) */
private var updBtns : Array < TextView ? > = emptyArray ( )
private var updFocusIdx = 0
private var updForce = false
// 安装统计 + 版本推送
private var updateOverlay : View ? = null
private var pendingUpdate : AuthApi . UpdateInfo ? = null
@@ -216,6 +222,20 @@ class MainActivity : Activity() {
ViewGroup . LayoutParams . WRAP _CONTENT , ViewGroup . LayoutParams . WRAP _CONTENT , Gravity . TOP or Gravity . END )
root . addView ( rightCol , rcLP )
// 4b) 右下角:已授权标识(小字,不打扰观看;未授权时隐藏)
authStateText = TextView ( this ) . apply {
text = " ✓ 已授权 "
textSize = 12f
setTextColor ( 0xB3FFFFFF . toInt ( ) ) // 70% 白,弱化存在感
setBackgroundColor ( 0x59000000 . toInt ( ) ) // 35% 黑底,保证在亮画面上也看得清
setPadding ( 14 , 6 , 14 , 6 )
setShadowLayer ( 3f , 1f , 1f , Color . BLACK )
visibility = View . GONE
}
root . addView ( authStateText , FrameLayout . LayoutParams (
ViewGroup . LayoutParams . WRAP _CONTENT , ViewGroup . LayoutParams . WRAP _CONTENT ,
Gravity . BOTTOM or Gravity . END ) . apply { setMargins ( 0 , 0 , 26 , 26 ) } )
// 5) 数字键大字
digitText = TextView ( this ) . apply {
textSize = 56f ; setTextColor ( Color . WHITE )
@@ -229,8 +249,9 @@ class MainActivity : Activity() {
// 6) 加载罩
loadingMask = FrameLayout ( this ) . apply { setBackgroundColor ( 0xB2000000u . toInt ( ) ) ; visibility = View . GONE }
loadingText = TextView ( this ) . apply {
textSize = 28f ; setTextColor ( Color . WHITE ) ; gravity = Gravity . CENTER
setShadowLayer ( 6f , 3f , 3f , Color . BLACK )
textSize = 16f ; setTextColor ( 0xE6FFFFFF . toInt ( ) ) ; gravity = Gravity . CENTER
setShadowLayer ( 4f , 2f , 2f , Color . BLACK )
setPadding ( 32 , 0 , 32 , 0 )
}
loadingMask . addView ( loadingText , FrameLayout . LayoutParams (
ViewGroup . LayoutParams . MATCH _PARENT , ViewGroup . LayoutParams . MATCH _PARENT ) )
@@ -347,7 +368,11 @@ class MainActivity : Activity() {
runOnUiThread {
isVip = r . ok
prefs . edit ( ) . putBoolean ( " is_vip " , r . ok ) . apply ( )
if ( r . ok ) { trialText . visibility = View . GONE ; trialText . text = " VIP " ; showAuthButton ( false ) } else enterFreeMode ( )
if ( r . ok ) {
trialText . visibility = View . GONE ; trialText . text = " VIP "
showAuthButton ( false )
showAuthState ( true ) // 右下角显示「✓ 已授权」
} else enterFreeMode ( )
}
} . start ( )
}
@@ -355,10 +380,20 @@ class MainActivity : Activity() {
private fun enterFreeMode ( ) {
trialText . visibility = View . VISIBLE
showAuthButton ( true )
showAuthState ( false ) // 免费模式下不显示右下角标识
handler . removeCallbacks ( uiTick )
handler . post ( uiTick )
}
/** 右下角「✓ 已授权」标识 显示/隐藏 */
private fun showAuthState ( show : Boolean ) {
runOnUiThread {
val t = authStateText ?: return @runOnUiThread
t . visibility = if ( show ) View . VISIBLE else View . GONE
t . text = if ( show ) " ✓ 已授权 " else " "
}
}
/** 显示/隐藏右上角「立即授权」按钮 */
private fun showAuthButton ( show : Boolean ) {
runOnUiThread {
@@ -377,16 +412,38 @@ class MainActivity : Activity() {
}
}
/** 打开授权/激活页面(含卡密激活与购买入口) */
private fun openAuthPage ( ) {
try {
startActivity ( Intent ( this , SettingsActivity :: class . java ) )
} catch ( e : Exception ) {
statusText . text = " 打开授权页失败: ${e.javaClass.simpleName} : ${e.message} "
android . widget . Toast . makeText ( this , " 打开授权页失败: ${e.message} " , android . widget . Toast . LENGTH _LONG ) . show ( )
/** 菜单键/其他入口统一打开设置页;带三重诊断,打不开时屏幕上有原因 */
private fun openSettingsPage ( ) {
val intent = Intent ( this , SettingsActivity :: class . java )
// ① 预检查:系统能否解析到这个 Activity( manifest 没注册会返回 null)
val resolve = try { packageManager . resolveActivity ( intent , 0 ) } catch ( e : Exception ) { null }
if ( resolve == null ) {
statusText . text = " ⚠ 设置页无法解析:manifest 未注册 SettingsActivity "
android . widget . Toast . makeText ( this , " 设置页无法解析(manifest 未注册) " , android . widget . Toast . LENGTH _LONG ) . show ( )
return
}
// ② 启动
val before = try { getSharedPreferences ( " iptv_main " , MODE _PRIVATE ) . getLong ( " settings_opened_at " , 0L ) } catch ( e : Exception ) { 0L }
try {
startActivity ( intent )
} catch ( e : Exception ) {
statusText . text = " ⚠ 打开设置失败: ${e.javaClass.simpleName} : ${e.message} "
android . widget . Toast . makeText ( this , " 打开设置失败: ${e.message} " , android . widget . Toast . LENGTH _LONG ) . show ( )
return
}
// ③ 1.4 秒后核对:SettingsActivity.onCreate 有没有真的跑起来
handler . postDelayed ( {
val after = try { getSharedPreferences ( " iptv_main " , MODE _PRIVATE ) . getLong ( " settings_opened_at " , 0L ) } catch ( e : Exception ) { 0L }
if ( after <= before ) {
statusText . text = " ⚠ 设置页未响应:onCreate 未执行(启动被拦截或立刻结束) "
android . widget . Toast . makeText ( this , " 设置页未响应,请把这行字告诉我 " , android . widget . Toast . LENGTH _LONG ) . show ( )
}
} , 1400 )
}
/** 打开授权/激活页面(就是设置页:卡密激活 + 购买入口 + 撤销授权) */
private fun openAuthPage ( ) { openSettingsPage ( ) }
/** 首次启动:简易使用教程(可关闭,只弹一次) */
private fun maybeShowTutorial ( ) {
try {
@@ -575,13 +632,7 @@ class MainActivity : Activity() {
setItemsCanFocus ( false )
onItemClickListener = AdapterView . OnItemClickListener { _ , _ , pos , _ ->
if ( pos == 0 ) {
try { startActivity ( Intent ( this @MainActivity , SettingsActivity :: class . java ) ) }
catch ( e : Exception ) {
statusText . text = " 打开设置失败: ${e.javaClass.simpleName} : ${e.message} "
android . widget . Toast . makeText ( this @MainActivity , " 打开设置失败: ${e.message} " , android . widget . Toast . LENGTH _LONG ) . show ( )
return @OnItemClickListener
}
hidePanel ( ) ; return @OnItemClickListener
openSettingsPage ( ) ; hidePanel ( ) ; return @OnItemClickListener
}
if ( pos == 0 ) return @OnItemClickListener
curGroupPos = pos - 1
@@ -829,6 +880,28 @@ class MainActivity : Activity() {
}
return true
}
// ★ 更新弹窗显示时:所有按键都由弹窗接管。
// 否则会落到下面的 showPanel()/exitApp()/换台 —— 遥控器就"选不了"了
if ( isUpdateDialogVisible ( ) ) {
when ( keyCode ) {
KeyEvent . KEYCODE _BACK -> { hideUpdateDialog ( ) ; return true }
KeyEvent . KEYCODE _DPAD _CENTER , KeyEvent . KEYCODE _ENTER ,
KeyEvent . KEYCODE _NUMPAD _ENTER , KeyEvent . KEYCODE _SPACE -> {
val btn = updBtns . getOrNull ( updFocusIdx )
?. takeIf { it . visibility == View . VISIBLE }
?: updBtns . filterNotNull ( ) . firstOrNull { it . visibility == View . VISIBLE }
btn ?. performClick ( )
return true
}
KeyEvent . KEYCODE _DPAD _LEFT , KeyEvent . KEYCODE _DPAD _UP -> { moveUpdateFocus ( - 1 ) ; return true }
KeyEvent . KEYCODE _DPAD _RIGHT , KeyEvent . KEYCODE _DPAD _DOWN -> { moveUpdateFocus ( 1 ) ; return true }
else -> return true // 数字键 / 菜单键 等全部吞掉(弹窗期间不能换台、不能进设置)
}
}
// 退出确认框显示时:返回=取消,其余交给按钮焦点系统处理(左右切换,OK 确认)
if ( confirmVisible ) {
if ( keyCode == KeyEvent . KEYCODE _BACK ) { hideExitConfirm ( ) ; return true }
@@ -912,13 +985,7 @@ class MainActivity : Activity() {
if ( panelCol == 0 ) {
val np = groupAdapter ?. selected ?: 0
if ( np == 0 ) {
// 保险:打开失败要能在屏幕上看到原因,便于排查
try { startActivity ( Intent ( this , SettingsActivity :: class . java ) ) }
catch ( e : Exception ) {
statusText . text = " 打开设置失败: ${e.javaClass.simpleName} : ${e.message} "
android . widget . Toast . makeText ( this @MainActivity , " 打开设置失败: ${e.message} " , android . widget . Toast . LENGTH _LONG ) . show ( )
return true
}
openSettingsPage ( ) // 统一走带诊断的入口
hidePanel ( )
}
else { curGroupPos = np - 1 ; updateChannelListOfGroup ( curGroupPos ) ; panelCol = 1 ; refreshPanelHighlight ( ) }
@@ -936,7 +1003,8 @@ class MainActivity : Activity() {
// ★ 修复:返回键必须弹退出确认,绝不直接 finish(原来漏了这个分支,
// 导致 BACK 落到 super.onKeyDown() = 系统默认无提示退出)
KeyEvent . KEYCODE _BACK -> { exitApp ( ) ; return true }
KeyEvent . KEYCODE _MENU -> { autoSwitchLine ( ) ; return true }
// ★ 菜单键 → 打开设置页(原来这里是「换线」,换线已移到设置页里)
KeyEvent . KEYCODE _MENU -> { openSettingsPage ( ) ; return true }
KeyEvent . KEYCODE _CHANNEL _UP -> { playChannel ( ( currentChannel - 1 + channels . size ) % channels . size ) ; return true }
KeyEvent . KEYCODE _CHANNEL _DOWN -> { playChannel ( ( currentChannel + 1 ) % channels . size ) ; return true }
KeyEvent . KEYCODE _DPAD _UP , KeyEvent . KEYCODE _DPAD _DOWN -> {
@@ -1070,10 +1138,11 @@ class MainActivity : Activity() {
} else setBackgroundColor ( Color . parseColor ( " #2A2F3A " ) )
setOnClickListener { onClick ( ) }
}
val laterBtn = mkBtn ( " 稍后 " , true ) { hideUpdateDialog ( ) }
val dlBtn = mkBtn ( " 立即更新 " , false ) {
// ★ 两个按钮都必须可聚焦,否则遥控器永远选不到「立即更新」
val laterBtn = mkBtn ( " 稍后 " , true ) { hideUpdateDialog ( ) } . apply { tag = " later " }
val dlBtn = mkBtn ( " 立即更新 " , true ) {
pendingUpdate ?. let { startUpdateDownload ( it ) } ; hideUpdateDialog ( )
}
} . apply { tag = " dl " }
row . addView ( laterBtn , LinearLayout . LayoutParams (
ViewGroup . LayoutParams . WRAP _CONTENT , ViewGroup . LayoutParams . WRAP _CONTENT ) . apply { rightMargin = 32 } )
row . addView ( dlBtn , LinearLayout . LayoutParams (
@@ -1095,12 +1164,48 @@ class MainActivity : Activity() {
meta . text = " 当前 v ${AuthApi.clientVersion(this)} " + ( if ( mb . isNotEmpty ( ) ) " · $mb " else " " ) +
( if ( force ) " \n ⚠ 请升级到此版本 " else " " )
chg . text = info . changelog . ifEmpty { " 本次更新包含功能优化与问题修复 " }
// ★ 强制更新时不给「稍后」(原代码只写了注释没实现)
val laterB = updateOverlay !! . findViewWithTag < TextView > ( " later " )
val dlB = updateOverlay !! . findViewWithTag < TextView > ( " dl " )
updForce = info . updateRequired || ( info . minClientVersion . isNotEmpty ( ) &&
compareVersion ( AuthApi . clientVersion ( this ) , info . minClientVersion ) < 0 )
laterB ?. visibility = if ( updForce ) View . GONE else View . VISIBLE
// 可见的按钮列表(强制更新时只剩「立即更新」)
updBtns = arrayOf ( if ( updForce ) null else laterB , dlB )
updFocusIdx = if ( updForce ) 1 else 1 // 默认聚焦「立即更新」(主操作)
updateOverlay !! . visibility = View . VISIBLE
// 强制更新时不给"稍后"
updateOverlay !! . findViewWithTag < TextView > ( " ver " ) // touch to ensure rendered
// ★ 必须主动聚焦,否则遥控器按 OK 不会命中任何按钮
handler . postDelayed ( {
if ( updateOverlay ?. visibility == View . VISIBLE ) {
updBtns . getOrNull ( updFocusIdx ) ?. requestFocus ( )
}
} , 120 )
}
private fun hideUpdateDialog ( ) { updateOverlay ?. visibility = View . GONE }
private fun hideUpdateDialog ( ) {
updateOverlay ?. visibility = View . GONE
// 清掉焦点,避免残留焦点挡住后续按键
updBtns . forEach { it ?. clearFocus ( ) }
updFocusIdx = 0
}
/** 更新弹窗内切换按钮焦点(遥控器左右/上下) */
private fun moveUpdateFocus ( delta : Int ) {
val visible = updBtns . filterNotNull ( ) . filter { it . visibility == View . VISIBLE }
if ( visible . isEmpty ( ) ) return
val cur = updBtns . getOrNull ( updFocusIdx )
var i = visible . indexOf ( cur )
if ( i < 0 ) i = 0
i = ( i + delta + visible . size ) % visible . size
val target = visible [ i ]
updFocusIdx = updBtns . indexOf ( target )
target . requestFocus ( )
}
/** 更新弹窗是否显示 */
private fun isUpdateDialogVisible ( ) : Boolean = updateOverlay ?. visibility == View . VISIBLE
/** 比较 "9.10.0" vs "9.3.0" 之类 */
fun compareVersion ( a : String , b : String ) : Int {
@@ -1242,10 +1347,12 @@ class MainActivity : Activity() {
isVip = true
trialText . visibility = View . GONE
showAuthButton ( false )
showAuthState ( true ) // 右下角显示「✓ 已授权」
handler . removeCallbacks ( uiTick )
} else if ( ! nowVip && isVip ) {
// ★ 设置页里撤销了授权 → 回到免费模式(倒计时 + 立即授权按钮都恢复)
isVip = false
showAuthState ( false ) // 右下角标识隐藏
enterFreeMode ( )
}
}
@@ -1276,6 +1383,14 @@ class MainActivity : Activity() {
playRequested = - 1
if ( idx >= 0 ) playChannel ( idx )
}
// 设置页点「切换线路」→ 回来执行(原菜单键功能,现已移到设置页)
run {
val sp = getSharedPreferences ( " iptv_main " , MODE _PRIVATE )
if ( sp . getString ( " pending_action " , " " ) == " switch_line " ) {
sp . edit ( ) . remove ( " pending_action " ) . apply ( )
autoSwitchLine ( )
}
}
}
/** 点「继续播放」→ 重新连接当前频道(直播流最稳的恢复方式) */