@@ -104,6 +104,10 @@ class MainActivity : Activity() {
/** 首次启动简易教程遮罩 */
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
@@ -408,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 {
@@ -606,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
@@ -860,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 }
@@ -943,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 ( ) }
@@ -967,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 -> {
@@ -1101,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 (
@@ -1126,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 {
@@ -1309,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 ( )
}
}
}
/** 点「继续播放」→ 重新连接当前频道(直播流最稳的恢复方式) */