This commit is contained in:
lunaticbum 2026-02-04 17:49:36 +09:00
parent 4f389dea45
commit 98d4d3e463
7 changed files with 141 additions and 88 deletions

View File

@ -25,15 +25,19 @@ import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.logging.HttpLoggingInterceptor
object OkHttpClientInstance {
val client: OkHttpClient by lazy {
// 네트워크 로그를 보기 위한 인터셉터 설정
// [수정] 누수 발생 시 어디서 할당되었는지 추적하기 위해 FINE 레벨 설정 제안을 반영
java.util.logging.Logger.getLogger(OkHttpClient::class.java.name).level = java.util.logging.Level.FINE
val loggingInterceptor = HttpLoggingInterceptor().apply {
// 개발 중에는 BASIC이나 HEADERS로 두면 연결 상태를 더 잘 볼 수 있습니다.
level = HttpLoggingInterceptor.Level.NONE
}
OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
// [추가] 연결 풀 설정을 명시적으로 관리하여 누수 가능성을 줄입니다.
.connectionPool(okhttp3.ConnectionPool(5, 5, java.util.concurrent.TimeUnit.MINUTES))
.build()
}
}
@ -373,7 +377,21 @@ class BookmarkApiService {
private val bookmarkLikeUrl = "$baseUrl/bookmarks/{bookmarkId}/like"
private val bookmarkUnlikeUrl = "$baseUrl/bookmarks/{bookmarkId}/unlike"
val client: OkHttpClient by lazy {
// [수정] 누수 발생 시 어디서 할당되었는지 추적하기 위해 FINE 레벨 설정 제안을 반영
java.util.logging.Logger.getLogger(OkHttpClient::class.java.name).level = java.util.logging.Level.FINE
val loggingInterceptor = HttpLoggingInterceptor().apply {
// 개발 중에는 BASIC이나 HEADERS로 두면 연결 상태를 더 잘 볼 수 있습니다.
level = HttpLoggingInterceptor.Level.NONE
}
OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
// [추가] 연결 풀 설정을 명시적으로 관리하여 누수 가능성을 줄입니다.
.connectionPool(okhttp3.ConnectionPool(5, 5, java.util.concurrent.TimeUnit.MINUTES))
.build()
}
/**
* 서버에서 북마크 목록을 가져오는 suspend 함수
@ -393,7 +411,7 @@ class BookmarkApiService {
.get()
.build()
val responseJson = OkHttpClientInstance.client.newCall(request).execute().use { response ->
val responseJson = client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
println("❌ 북마크 API 호출 실패: ${response.code} ${response.message}")
return@withContext null
@ -430,7 +448,7 @@ class BookmarkApiService {
.post(requestBody)
.build()
val responseJson = OkHttpClientInstance.client.newCall(request).execute().use { response ->
val responseJson = client.newCall(request).execute().use { response ->
if (!response.isSuccessful) return@withContext null
response.body?.string()
}
@ -455,7 +473,7 @@ class BookmarkApiService {
.post(requestBody)
.build()
val responseJson = OkHttpClientInstance.client.newCall(request).execute().use { response ->
val responseJson = client.newCall(request).execute().use { response ->
if (!response.isSuccessful) return@withContext null
response.body?.string()
}

View File

@ -25,12 +25,16 @@ import bums.lunatic.launcher.helpers.HourlyLogWriter
import bums.lunatic.launcher.helpers.PrefHelper
import bums.lunatic.launcher.home.Base64ImageCache
import bums.lunatic.launcher.home.Base64RequestHandler
import bums.lunatic.launcher.home.NeoRssActivity.Companion.lActivity
import bums.lunatic.launcher.utils.Blog
import com.squareup.picasso.OkHttp3Downloader
import com.squareup.picasso.Picasso
import okhttp3.Cache
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.mozilla.geckoview.GeckoRuntime
import org.mozilla.geckoview.GeckoRuntimeSettings
import org.mozilla.geckoview.GeckoRuntimeSettings.ALLOW_ALL
import java.io.File
import java.util.concurrent.TimeUnit
@ -39,6 +43,47 @@ internal class LunaticLauncher : Application() {
companion object {
var appContext : LunaticLauncher? = null
var mHourlyLogWriter : HourlyLogWriter? = null
private var sRuntime: GeckoRuntime? = null
fun getRuntime() : GeckoRuntime? {
appContext?.initGeckoRuntime()
return sRuntime
}
}
private fun initGeckoRuntime() {
if (sRuntime == null) {
try {
val profileDir = File(this.getExternalFilesDir("file"), "geckoview_profile")
Blog.LOGE("profileDir >>> ${profileDir.absolutePath} ${profileDir.exists()}")
if (!profileDir.exists()) profileDir.mkdirs()
val configFile = File(profileDir, "geckoview-config.yaml")
configFile.writeText("""
args: ["--profile", "${profileDir.absolutePath}"]
prefs:
network.cookie.lifetimePolicy: 0
network.cookie.maxNumber: 3000
network.cookie.thirdparty.sessionOnly: false
""".trimIndent())
sRuntime = GeckoRuntime.create(this, GeckoRuntimeSettings.Builder()
.arguments(arrayOf("--profile", profileDir.absolutePath))
.configFilePath(File(profileDir, "geckoview-config.yaml").absolutePath) // 추가!
.aboutConfigEnabled(true)
.allowInsecureConnections(ALLOW_ALL)
.javaScriptEnabled(true)
.extensionsProcessEnabled(true)
.extensionsWebAPIEnabled(true)
.loginAutofillEnabled(true)
.debugLogging(false)
.consoleOutput(false)
.remoteDebuggingEnabled(true).build())
} catch (e : Exception) {
e.printStackTrace()
}
}
}
override fun onCreate() {
@ -56,7 +101,7 @@ internal class LunaticLauncher : Application() {
}
mHourlyLogWriter = HourlyLogWriter(dir)
val logging = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.HEADERS
level = HttpLoggingInterceptor.Level.NONE
}
val cacheSize = 1024L * 1024 * 1024 * 6 // 60MB
val cache = Cache(File(this.filesDir, "picasso-cache"), cacheSize)

View File

@ -31,11 +31,11 @@ import androidx.appcompat.app.AlertDialog
import androidx.core.net.toUri
import androidx.core.view.isVisible
import bums.lunatic.launcher.BookmarkUploader
import bums.lunatic.launcher.LunaticLauncher.Companion.getRuntime
import bums.lunatic.launcher.R
import bums.lunatic.launcher.helpers.ForeGroundService
import bums.lunatic.launcher.helpers.ForeGroundService.Companion.ACTION_VIDEO_DOWNLOAD
import bums.lunatic.launcher.helpers.ForeGroundService.Companion.EXTRA_TARGET_URL
import bums.lunatic.launcher.home.NeoRssActivity.Companion.getRuntime
import bums.lunatic.launcher.home.tokiz.PortMessage
import bums.lunatic.launcher.model.Dotax
import bums.lunatic.launcher.model.DotaxArticles
@ -61,6 +61,7 @@ import org.jsoup.Jsoup
import org.mozilla.gecko.util.ThreadUtils
import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.GeckoSession
import org.mozilla.geckoview.GeckoSessionSettings
import org.mozilla.geckoview.GeckoView
import org.mozilla.geckoview.MediaSession
import org.mozilla.geckoview.WebExtension
@ -101,7 +102,7 @@ open class GeckoWeb @JvmOverloads constructor(
}
}
// 3. 저장된 세션 상태 복구 메서드
// // 3. 저장된 세션 상태 복구 메서드
fun restoreSessionState(session: GeckoSession) {
val stateJson = context.getSharedPreferences("GeckoPrefs", Context.MODE_PRIVATE)
.getString("gecko_session_state", null)
@ -109,7 +110,9 @@ open class GeckoWeb @JvmOverloads constructor(
if (!stateJson.isNullOrEmpty()) {
// 문자열에서 SessionState 객체 생성
val state = GeckoSession.SessionState.fromString(stateJson)
Blog.LOGE("Restored state >>> ${state}")
if (state != null) {
session.restoreState(state)
Log.d("GeckoWeb", "Session State Restored")
}
@ -357,8 +360,9 @@ open class GeckoWeb @JvmOverloads constructor(
onPageStopCallback?.invoke(success)
saveCurrentSessionState()
}
override fun onSessionStateChange(session: GeckoSession, sessionState: GeckoSession.SessionState) {
override fun onSessionStateChange(session: GeckoSession, sessionState: GeckoSession.SessionState) {
lastSessionState = sessionState
onSessionStateChangeCallback?.invoke(sessionState)
}
}
@ -421,7 +425,16 @@ open class GeckoWeb @JvmOverloads constructor(
private fun buildWeb() {
getRuntime()?.let { runtime ->
val session = GeckoSession()
val sessionSettings = GeckoSessionSettings.Builder()
// 1. 뷰포트 모드를 모바일로 강제
.userAgentOverride("Mozilla/5.0 (Android 14; Mobile; rv:120.0) Gecko/120.0 Firefox/120.0")
.viewportMode(GeckoSessionSettings.VIEWPORT_MODE_MOBILE)
.allowJavascript(true)
.contextId("JUST_ONE")
.usePrivateMode(false)
.build()
val session = GeckoSession(sessionSettings)
restoreSessionState(session)
session.open(runtime)
this.setSession(session)
@ -433,6 +446,7 @@ open class GeckoWeb @JvmOverloads constructor(
session.mediaDelegate = mediaDelegate
session.promptDelegate = promptDelegate
session.mediaSessionDelegate = mediaSessionDelegate
runtime.settings.loginAutofillEnabled = true
runtime.webExtensionController.setAddonManagerDelegate(addonManagerDelegate)
runtime.webExtensionController

View File

@ -58,6 +58,7 @@ import org.mozilla.geckoview.ExperimentDelegate
import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.GeckoRuntime
import org.mozilla.geckoview.GeckoRuntimeSettings
import org.mozilla.geckoview.GeckoRuntimeSettings.ALLOW_ALL
import java.io.File
@ -67,11 +68,7 @@ open class NeoRssActivity : CommonActivity() {
private lateinit var binding: RssActivityBinding
companion object {
private var sRuntime: GeckoRuntime? = null
fun getRuntime() : GeckoRuntime? {
lActivity?.initGeckoRuntime()
return sRuntime
}
var isOpendFold = false
@ -450,8 +447,8 @@ open class NeoRssActivity : CommonActivity() {
binding.controllPanel.visibility = View.GONE
binding.floatingActionMenu.visibility = View.GONE
}
sRuntime?.shutdown()
sRuntime = null
// sRuntime?.shutdown()
// sRuntime = null
finish()
}
else -> {}
@ -459,27 +456,7 @@ open class NeoRssActivity : CommonActivity() {
binding.floatingActionMenu.close(false)
}
private fun initGeckoRuntime() {
if (sRuntime == null) {
try {
val profileDir = File(this.filesDir, "geckoview_profile")
if (!profileDir.exists()) profileDir.mkdirs()
sRuntime = GeckoRuntime.create(this, GeckoRuntimeSettings.Builder()
.configFilePath(profileDir.absolutePath)
.aboutConfigEnabled(true)
.javaScriptEnabled(true)
.extensionsProcessEnabled(true)
.extensionsWebAPIEnabled(true)
.experimentDelegate(experimentDelegate)
.debugLogging(false)
.remoteDebuggingEnabled(true).build())
} catch (e : Exception) {
e.printStackTrace()
}
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
return super.onTouchEvent(event)
@ -495,8 +472,8 @@ open class NeoRssActivity : CommonActivity() {
override fun onDestroy() {
try {
sRuntime?.shutdown()
sRuntime = null
// sRuntime?.shutdown()
// sRuntime = null
} catch (e: Exception) { e.printStackTrace() }
super.onDestroy()
@ -552,33 +529,33 @@ open class NeoRssActivity : CommonActivity() {
})
}
val experimentDelegate = object : ExperimentDelegate {
override fun onGetExperimentFeature(feature: String): GeckoResult<JSONObject?> {
Blog.LOGE("onGetExperimentFeature $feature")
return super.onGetExperimentFeature(feature)
}
override fun onRecordExposureEvent(feature: String): GeckoResult<Void?> {
Blog.LOGE("onRecordExposureEvent $feature")
return super.onRecordExposureEvent(feature)
}
override fun onRecordExperimentExposureEvent(
feature: String,
slug: String
): GeckoResult<Void?> {
Blog.LOGE("onRecordExperimentExposureEvent $feature , $slug")
return super.onRecordExperimentExposureEvent(feature, slug)
}
override fun onRecordMalformedConfigurationEvent(
feature: String,
part: String
): GeckoResult<Void?> {
Blog.LOGE("onRecordMalformedConfigurationEvent $feature , $part")
return super.onRecordMalformedConfigurationEvent(feature, part)
}
}
// val experimentDelegate = object : ExperimentDelegate {
// override fun onGetExperimentFeature(feature: String): GeckoResult<JSONObject?> {
// Blog.LOGE("onGetExperimentFeature $feature")
// return super.onGetExperimentFeature(feature)
// }
//
// override fun onRecordExposureEvent(feature: String): GeckoResult<Void?> {
// Blog.LOGE("onRecordExposureEvent $feature")
// return super.onRecordExposureEvent(feature)
// }
//
// override fun onRecordExperimentExposureEvent(
// feature: String,
// slug: String
// ): GeckoResult<Void?> {
// Blog.LOGE("onRecordExperimentExposureEvent $feature , $slug")
// return super.onRecordExperimentExposureEvent(feature, slug)
// }
//
// override fun onRecordMalformedConfigurationEvent(
// feature: String,
// part: String
// ): GeckoResult<Void?> {
// Blog.LOGE("onRecordMalformedConfigurationEvent $feature , $part")
// return super.onRecordMalformedConfigurationEvent(feature, part)
// }
// }
val callBackHandler = Handler(Looper.getMainLooper())

View File

@ -915,26 +915,26 @@ internal class RssHome : Fragment() {
}
fun randomOrNull() : RssData? = lasted.randomOrNull()
fun rett(imageView: ImageView,imageUrl: String){
// OkHttp로 직접 이미지 다운로드 후
val request = Request.Builder().url(imageUrl).build()
val client = OkHttpClient()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
// 실패 시 기본 이미지 처리 or 로깅
}
override fun onResponse(call: Call, response: Response) {
response.body?.byteStream()?.let { inputStream ->
val bitmap = BitmapFactory.decodeStream(inputStream)
mainHandler.post({
imageView.setImageBitmap(bitmap)
})
}
}
})
}
// fun rett(imageView: ImageView,imageUrl: String){
// // OkHttp로 직접 이미지 다운로드 후
// val request = Request.Builder().url(imageUrl).build()
// val client = OkHttpClient()
// client.newCall(request).enqueue(object : Callback {
// override fun onFailure(call: Call, e: IOException) {
// // 실패 시 기본 이미지 처리 or 로깅
//
// }
//
// override fun onResponse(call: Call, response: Response) {
// response.body?.byteStream()?.let { inputStream ->
// val bitmap = BitmapFactory.decodeStream(inputStream)
// mainHandler.post({
// imageView.setImageBitmap(bitmap)
// })
// }
// }
// })
// }
}
var toast: Toast? = null
fun Context.toast(string: String) {

View File

@ -35,7 +35,6 @@ import bums.lunatic.launcher.databinding.BooktokiBinding
import bums.lunatic.launcher.home.GeckoWeb
import bums.lunatic.launcher.home.GeckoWeb.JxEvent
import bums.lunatic.launcher.home.NeoRssActivity
import bums.lunatic.launcher.home.NeoRssActivity.Companion.getRuntime
import bums.lunatic.launcher.home.toast
import bums.lunatic.launcher.home.tokiz.view.PagedTextLayout
import bums.lunatic.launcher.home.tokiz.view.PagedTextViewInterface

View File

@ -18,7 +18,7 @@ import java.util.concurrent.TimeUnit
class CustomOkHttpGlideModule : AppGlideModule() {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
val logging = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.HEADERS
level = HttpLoggingInterceptor.Level.NONE
}
val cacheSize = 1024L * 1024 * 1024 * 6 // 60MB
val cache = Cache(File(context.filesDir, "clide-cache"), cacheSize)