v1.0.4 - PlaybackActivity 업데이트
This commit is contained in:
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "com.example.tvmon"
|
||||
minSdk 28
|
||||
targetSdk 34
|
||||
versionCode 4
|
||||
versionName "1.0.3"
|
||||
versionCode 5
|
||||
versionName "1.0.4"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.webkit.WebViewClient
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.example.tvmon.R
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.net.HttpURLConnection
|
||||
|
||||
class PlaybackActivity : AppCompatActivity() {
|
||||
|
||||
@@ -31,9 +32,13 @@ class PlaybackActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var webView: WebView
|
||||
private lateinit var loadingOverlay: View
|
||||
private var isVideoPlaying = false
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
// [FIX 1] loadingRunnable을 명시적으로 관리하여 중복 postDelayed 방지
|
||||
private val hideLoadingRunnable = Runnable {
|
||||
loadingOverlay.visibility = View.GONE
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -67,45 +72,9 @@ class PlaybackActivity : AppCompatActivity() {
|
||||
setTitle("Tvmon")
|
||||
}
|
||||
|
||||
val existingWebView = findViewById<WebView>(R.id.webview)
|
||||
val parent = existingWebView.parent as android.view.ViewGroup
|
||||
val index = parent.indexOfChild(existingWebView)
|
||||
val layoutParams = existingWebView.layoutParams
|
||||
parent.removeView(existingWebView)
|
||||
|
||||
webView = object : WebView(this) {
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
android.util.Log.d("WebViewKey", "WebView.onKeyDown: keyCode=$keyCode")
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT ||
|
||||
keyCode == KeyEvent.KEYCODE_DPAD_RIGHT ||
|
||||
keyCode == KeyEvent.KEYCODE_DPAD_UP ||
|
||||
keyCode == KeyEvent.KEYCODE_DPAD_DOWN ||
|
||||
keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
return false
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
|
||||
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
|
||||
val keyCode = event?.keyCode ?: return super.dispatchKeyEvent(event)
|
||||
android.util.Log.d("WebViewKey", "WebView.dispatchKeyEvent: keyCode=$keyCode, action=${event.action}")
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
override fun scrollTo(x: Int, y: Int) {}
|
||||
override fun scrollBy(x: Int, y: Int) {}
|
||||
}
|
||||
|
||||
webView.id = R.id.webview
|
||||
webView.layoutParams = layoutParams
|
||||
webView.isFocusable = true
|
||||
webView.isFocusableInTouchMode = true
|
||||
parent.addView(webView, index)
|
||||
// [FIX 2] 불필요한 WebView 제거/재생성 패턴 제거.
|
||||
// XML에 정의된 WebView를 그대로 사용하고, 서브클래스 기능은 setOnKeyListener로 처리.
|
||||
webView = findViewById(R.id.webview)
|
||||
|
||||
webView.setOnKeyListener { _, keyCode, event ->
|
||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
@@ -115,31 +84,12 @@ class PlaybackActivity : AppCompatActivity() {
|
||||
toggleVideoPlayback()
|
||||
true
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
||||
seekVideo(-10000)
|
||||
android.util.Log.d("PlaybackActivity", "setOnKeyListener: LEFT")
|
||||
true
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
||||
seekVideo(10000)
|
||||
android.util.Log.d("PlaybackActivity", "setOnKeyListener: RIGHT")
|
||||
true
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_UP -> {
|
||||
adjustVolume(0.1f)
|
||||
android.util.Log.d("PlaybackActivity", "setOnKeyListener: UP")
|
||||
true
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_DOWN -> {
|
||||
adjustVolume(-0.1f)
|
||||
android.util.Log.d("PlaybackActivity", "setOnKeyListener: DOWN")
|
||||
true
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> { seekVideo(-10000); true }
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> { seekVideo(10000); true }
|
||||
KeyEvent.KEYCODE_DPAD_UP -> { adjustVolume(0.1f); true }
|
||||
KeyEvent.KEYCODE_DPAD_DOWN -> { adjustVolume(-0.1f); true }
|
||||
KeyEvent.KEYCODE_BACK -> {
|
||||
if (webView.canGoBack()) {
|
||||
webView.goBack()
|
||||
}
|
||||
true
|
||||
if (webView.canGoBack()) { webView.goBack(); true } else false
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
@@ -153,125 +103,88 @@ class PlaybackActivity : AppCompatActivity() {
|
||||
webView.loadUrl(url)
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private fun setupWebView() {
|
||||
val webSettings: WebSettings = webView.settings
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private fun setupWebView() {
|
||||
val webSettings: WebSettings = webView.settings
|
||||
|
||||
webSettings.javaScriptEnabled = true
|
||||
webSettings.domStorageEnabled = true
|
||||
webSettings.databaseEnabled = true
|
||||
webSettings.allowFileAccess = true
|
||||
webSettings.allowContentAccess = true
|
||||
webSettings.mediaPlaybackRequiresUserGesture = false
|
||||
webSettings.loadsImagesAutomatically = true
|
||||
webSettings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
||||
webSettings.userAgentString = USER_AGENT
|
||||
webSettings.useWideViewPort = true
|
||||
webSettings.loadWithOverviewMode = true
|
||||
webSettings.cacheMode = WebSettings.LOAD_DEFAULT
|
||||
webSettings.allowUniversalAccessFromFileURLs = true
|
||||
webSettings.allowFileAccessFromFileURLs = true
|
||||
webSettings.javaScriptEnabled = true
|
||||
webSettings.domStorageEnabled = true
|
||||
webSettings.databaseEnabled = true
|
||||
webSettings.allowFileAccess = true
|
||||
webSettings.allowContentAccess = true
|
||||
webSettings.mediaPlaybackRequiresUserGesture = false
|
||||
webSettings.loadsImagesAutomatically = true
|
||||
webSettings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
||||
webSettings.userAgentString = USER_AGENT
|
||||
webSettings.useWideViewPort = true
|
||||
webSettings.loadWithOverviewMode = true
|
||||
webSettings.cacheMode = WebSettings.LOAD_DEFAULT
|
||||
webSettings.allowUniversalAccessFromFileURLs = true
|
||||
webSettings.allowFileAccessFromFileURLs = true
|
||||
webSettings.blockNetworkImage = false
|
||||
|
||||
// Video playback optimizations
|
||||
webSettings.mediaPlaybackRequiresUserGesture = false
|
||||
webSettings.blockNetworkImage = false
|
||||
|
||||
webView.addJavascriptInterface(object {
|
||||
@android.webkit.JavascriptInterface
|
||||
fun hideLoadingOverlay() {
|
||||
runOnUiThread {
|
||||
loadingOverlay.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}, "Android")
|
||||
|
||||
val cookieManager = CookieManager.getInstance()
|
||||
cookieManager.setAcceptCookie(true)
|
||||
cookieManager.setAcceptThirdPartyCookies(webView, true)
|
||||
|
||||
webView.webChromeClient = object : WebChromeClient() {
|
||||
override fun onConsoleMessage(cm: ConsoleMessage?): Boolean {
|
||||
val message = cm?.message() ?: return false
|
||||
android.util.Log.d("WebViewConsole", "[$${cm.sourceId()}:${cm.lineNumber()}] $message")
|
||||
|
||||
if (message.contains("player") || message.contains("video") || message.contains(".m3u8") || message.contains(".mp4")) {
|
||||
android.util.Log.i("WebViewConsole", "Player info: $message")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPermissionRequest(request: android.webkit.PermissionRequest?) {
|
||||
android.util.Log.d("PlaybackActivity", "Permission request: ${request?.resources?.joinToString()}")
|
||||
request?.grant(request.resources)
|
||||
}
|
||||
}
|
||||
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
|
||||
val url = request?.url?.toString() ?: ""
|
||||
|
||||
if (url.contains(".css") || url.contains(".js") && url.contains("tvmon.site")) {
|
||||
try {
|
||||
val conn = java.net.URL(url).openConnection() as java.net.HttpURLConnection
|
||||
conn.requestMethod = "GET"
|
||||
conn.connectTimeout = 8000
|
||||
conn.readTimeout = 8000
|
||||
conn.setRequestProperty("User-Agent", USER_AGENT)
|
||||
conn.setRequestProperty("Referer", "https://tvmon.site/")
|
||||
|
||||
val contentType = conn.contentType ?: "text/plain"
|
||||
val encoding = conn.contentEncoding ?: "UTF-8"
|
||||
val inputStream = conn.inputStream
|
||||
val content = inputStream.bufferedReader().use { it.readText() }
|
||||
inputStream.close()
|
||||
|
||||
var modifiedContent = content
|
||||
|
||||
if (contentType.contains("text/html", true)) {
|
||||
modifiedContent = injectFullscreenStyles(content)
|
||||
}
|
||||
|
||||
return WebResourceResponse(
|
||||
contentType, encoding,
|
||||
200, "OK",
|
||||
mapOf("Access-Control-Allow-Origin" to "*"),
|
||||
ByteArrayInputStream(modifiedContent.toByteArray(charset(encoding)))
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.w("PlaybackActivity", "Intercept failed for $url: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
return super.shouldInterceptRequest(view, request)
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
runOnUiThread {
|
||||
loadingOverlay.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
android.util.Log.i("PlaybackActivity", "Page finished: $url")
|
||||
|
||||
handler.postDelayed({
|
||||
injectFullscreenScript()
|
||||
}, 300)
|
||||
|
||||
handler.postDelayed({
|
||||
webView.addJavascriptInterface(object {
|
||||
@android.webkit.JavascriptInterface
|
||||
fun hideLoadingOverlay() {
|
||||
runOnUiThread {
|
||||
loadingOverlay.visibility = View.GONE
|
||||
}
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, "Android")
|
||||
|
||||
// Use hardware layer only when needed, not always
|
||||
webView.setLayerType(View.LAYER_TYPE_NONE, null)
|
||||
}
|
||||
val cookieManager = CookieManager.getInstance()
|
||||
cookieManager.setAcceptCookie(true)
|
||||
cookieManager.setAcceptThirdPartyCookies(webView, true)
|
||||
|
||||
webView.webChromeClient = object : WebChromeClient() {
|
||||
override fun onConsoleMessage(cm: ConsoleMessage?): Boolean {
|
||||
val message = cm?.message() ?: return false
|
||||
if (message.contains("player") || message.contains("video") ||
|
||||
message.contains(".m3u8") || message.contains(".mp4")) {
|
||||
android.util.Log.i("WebViewConsole", "Player info: $message")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPermissionRequest(request: android.webkit.PermissionRequest?) {
|
||||
request?.grant(request.resources)
|
||||
}
|
||||
}
|
||||
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
|
||||
// [FIX 3] shouldInterceptRequest 메모리 누수 수정.
|
||||
// 기존 코드는 요청마다 HttpURLConnection을 열어 전체 응답을 String으로 로드하고
|
||||
// disconnect()를 호출하지 않아 연결과 메모리가 누적되었음.
|
||||
// CSS/JS 인터셉트는 실익이 없으므로 제거하고 null 반환(WebView 기본 처리)으로 대체.
|
||||
// HTML 스타일 주입은 onPageFinished의 JS 인젝션으로 충분히 커버됨.
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?
|
||||
): WebResourceResponse? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
// [FIX 4] 페이지 시작 시 이전 hideLoading 콜백만 제거 (다른 콜백은 유지)
|
||||
handler.removeCallbacks(hideLoadingRunnable)
|
||||
loadingOverlay.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
android.util.Log.i("PlaybackActivity", "Page finished: $url")
|
||||
|
||||
// [FIX 5] injectFullscreenScript는 한 번만, hideLoading은 명시적 Runnable로 관리
|
||||
handler.postDelayed({ injectFullscreenScript() }, 300)
|
||||
handler.removeCallbacks(hideLoadingRunnable)
|
||||
handler.postDelayed(hideLoadingRunnable, 1500)
|
||||
}
|
||||
}
|
||||
|
||||
webView.setLayerType(View.LAYER_TYPE_NONE, null)
|
||||
}
|
||||
|
||||
private fun injectFullscreenStyles(html: String): String {
|
||||
val styleInjection = """
|
||||
@@ -422,8 +335,8 @@ webView.setLayerType(View.LAYER_TYPE_NONE, null)
|
||||
});
|
||||
};
|
||||
|
||||
makePlayerFullscreen();
|
||||
})();
|
||||
makePlayerFullscreen();
|
||||
})();
|
||||
""".trimIndent(),
|
||||
null
|
||||
)
|
||||
@@ -433,13 +346,12 @@ webView.setLayerType(View.LAYER_TYPE_NONE, null)
|
||||
val keyCode = event?.keyCode ?: return super.dispatchKeyEvent(event)
|
||||
val action = event.action
|
||||
|
||||
if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER)
|
||||
&& action == KeyEvent.ACTION_DOWN) {
|
||||
android.util.Log.d("PlaybackActivity", "OK button pressed")
|
||||
simulateCenterClick()
|
||||
toggleVideoPlayback()
|
||||
return true
|
||||
}
|
||||
if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER)
|
||||
&& action == KeyEvent.ACTION_DOWN) {
|
||||
simulateCenterClick()
|
||||
toggleVideoPlayback()
|
||||
return true
|
||||
}
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK && action == KeyEvent.ACTION_DOWN) {
|
||||
if (webView.canGoBack()) {
|
||||
@@ -453,16 +365,6 @@ if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTE
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
private fun handleDpadKey(keyCode: Int) {
|
||||
android.util.Log.d("PlaybackActivity", "handleDpadKey: $keyCode")
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> seekVideo(-10000)
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> seekVideo(10000)
|
||||
KeyEvent.KEYCODE_DPAD_UP -> adjustVolume(0.1f)
|
||||
KeyEvent.KEYCODE_DPAD_DOWN -> adjustVolume(-0.1f)
|
||||
}
|
||||
}
|
||||
|
||||
private fun adjustVolume(delta: Float) {
|
||||
webView.evaluateJavascript(
|
||||
"""
|
||||
@@ -485,15 +387,9 @@ if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTE
|
||||
(function() {
|
||||
var video = document.querySelector('video');
|
||||
if (video && video.duration > 0) {
|
||||
if (video.paused) {
|
||||
video.play();
|
||||
return 'played';
|
||||
} else {
|
||||
video.pause();
|
||||
return 'paused';
|
||||
}
|
||||
if (video.paused) { video.play(); return 'played'; }
|
||||
else { video.pause(); return 'paused'; }
|
||||
}
|
||||
|
||||
var iframes = document.querySelectorAll('iframe');
|
||||
for (var i = 0; i < iframes.length; i++) {
|
||||
try {
|
||||
@@ -501,12 +397,10 @@ if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTE
|
||||
iframes[i].contentWindow.postMessage('toggle', '*');
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
var focused = document.activeElement;
|
||||
if (focused && focused !== document.body) {
|
||||
try { focused.click(); } catch(e) {}
|
||||
}
|
||||
|
||||
return 'sent';
|
||||
})();
|
||||
""".trimIndent()
|
||||
@@ -525,14 +419,12 @@ if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTE
|
||||
video.currentTime = Math.max(0, Math.min(video.duration, video.currentTime + $offsetSec));
|
||||
return 'seeked';
|
||||
}
|
||||
|
||||
var iframes = document.querySelectorAll('iframe');
|
||||
for (var i = 0; i < iframes.length; i++) {
|
||||
try {
|
||||
iframes[i].contentWindow.postMessage({ type: 'seek', offset: $offsetSec }, '*');
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
return 'sent';
|
||||
})();
|
||||
""".trimIndent(),
|
||||
@@ -541,28 +433,18 @@ if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTE
|
||||
}
|
||||
|
||||
private fun simulateCenterClick() {
|
||||
runOnUiThread {
|
||||
val centerX = webView.width / 2f
|
||||
val centerY = webView.height / 2f
|
||||
val downTime = SystemClock.uptimeMillis()
|
||||
val centerX = webView.width / 2f
|
||||
val centerY = webView.height / 2f
|
||||
val downTime = SystemClock.uptimeMillis()
|
||||
|
||||
val downEvent = MotionEvent.obtain(
|
||||
downTime, downTime,
|
||||
MotionEvent.ACTION_DOWN, centerX, centerY, 0
|
||||
)
|
||||
val upEvent = MotionEvent.obtain(
|
||||
downTime, downTime + 100,
|
||||
MotionEvent.ACTION_UP, centerX, centerY, 0
|
||||
)
|
||||
val downEvent = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, centerX, centerY, 0)
|
||||
val upEvent = MotionEvent.obtain(downTime, downTime + 100, MotionEvent.ACTION_UP, centerX, centerY, 0)
|
||||
|
||||
webView.dispatchTouchEvent(downEvent)
|
||||
webView.dispatchTouchEvent(upEvent)
|
||||
webView.dispatchTouchEvent(downEvent)
|
||||
webView.dispatchTouchEvent(upEvent)
|
||||
|
||||
downEvent.recycle()
|
||||
upEvent.recycle()
|
||||
|
||||
android.util.Log.d("PlaybackActivity", "Center click simulated at ($centerX, $centerY)")
|
||||
}
|
||||
downEvent.recycle()
|
||||
upEvent.recycle()
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
@@ -590,10 +472,15 @@ if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTE
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
// [FIX 6] handler 콜백 전부 제거 후 WebView 정리. loadingOverlay 참조 해제.
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
if (::webView.isInitialized) {
|
||||
webView.stopLoading()
|
||||
webView.clearHistory()
|
||||
webView.webViewClient = WebViewClient() // 기존 client 참조 해제
|
||||
webView.webChromeClient = null
|
||||
webView.destroy()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user