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 import okhttp3.logging.HttpLoggingInterceptor
object OkHttpClientInstance { object OkHttpClientInstance {
val client: OkHttpClient by lazy { 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 { val loggingInterceptor = HttpLoggingInterceptor().apply {
// 개발 중에는 BASIC이나 HEADERS로 두면 연결 상태를 더 잘 볼 수 있습니다.
level = HttpLoggingInterceptor.Level.NONE level = HttpLoggingInterceptor.Level.NONE
} }
OkHttpClient.Builder() OkHttpClient.Builder()
.addInterceptor(loggingInterceptor) .addInterceptor(loggingInterceptor)
// [추가] 연결 풀 설정을 명시적으로 관리하여 누수 가능성을 줄입니다.
.connectionPool(okhttp3.ConnectionPool(5, 5, java.util.concurrent.TimeUnit.MINUTES))
.build() .build()
} }
} }
@ -373,7 +377,21 @@ class BookmarkApiService {
private val bookmarkLikeUrl = "$baseUrl/bookmarks/{bookmarkId}/like" private val bookmarkLikeUrl = "$baseUrl/bookmarks/{bookmarkId}/like"
private val bookmarkUnlikeUrl = "$baseUrl/bookmarks/{bookmarkId}/unlike" 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 함수 * 서버에서 북마크 목록을 가져오는 suspend 함수
@ -393,7 +411,7 @@ class BookmarkApiService {
.get() .get()
.build() .build()
val responseJson = OkHttpClientInstance.client.newCall(request).execute().use { response -> val responseJson = client.newCall(request).execute().use { response ->
if (!response.isSuccessful) { if (!response.isSuccessful) {
println("❌ 북마크 API 호출 실패: ${response.code} ${response.message}") println("❌ 북마크 API 호출 실패: ${response.code} ${response.message}")
return@withContext null return@withContext null
@ -430,7 +448,7 @@ class BookmarkApiService {
.post(requestBody) .post(requestBody)
.build() .build()
val responseJson = OkHttpClientInstance.client.newCall(request).execute().use { response -> val responseJson = client.newCall(request).execute().use { response ->
if (!response.isSuccessful) return@withContext null if (!response.isSuccessful) return@withContext null
response.body?.string() response.body?.string()
} }
@ -455,7 +473,7 @@ class BookmarkApiService {
.post(requestBody) .post(requestBody)
.build() .build()
val responseJson = OkHttpClientInstance.client.newCall(request).execute().use { response -> val responseJson = client.newCall(request).execute().use { response ->
if (!response.isSuccessful) return@withContext null if (!response.isSuccessful) return@withContext null
response.body?.string() 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.helpers.PrefHelper
import bums.lunatic.launcher.home.Base64ImageCache import bums.lunatic.launcher.home.Base64ImageCache
import bums.lunatic.launcher.home.Base64RequestHandler import bums.lunatic.launcher.home.Base64RequestHandler
import bums.lunatic.launcher.home.NeoRssActivity.Companion.lActivity
import bums.lunatic.launcher.utils.Blog import bums.lunatic.launcher.utils.Blog
import com.squareup.picasso.OkHttp3Downloader import com.squareup.picasso.OkHttp3Downloader
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import okhttp3.Cache import okhttp3.Cache
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor 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.io.File
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -39,6 +43,47 @@ internal class LunaticLauncher : Application() {
companion object { companion object {
var appContext : LunaticLauncher? = null var appContext : LunaticLauncher? = null
var mHourlyLogWriter : HourlyLogWriter? = 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() { override fun onCreate() {
@ -56,7 +101,7 @@ internal class LunaticLauncher : Application() {
} }
mHourlyLogWriter = HourlyLogWriter(dir) mHourlyLogWriter = HourlyLogWriter(dir)
val logging = HttpLoggingInterceptor().apply { val logging = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.HEADERS level = HttpLoggingInterceptor.Level.NONE
} }
val cacheSize = 1024L * 1024 * 1024 * 6 // 60MB val cacheSize = 1024L * 1024 * 1024 * 6 // 60MB
val cache = Cache(File(this.filesDir, "picasso-cache"), cacheSize) 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.net.toUri
import androidx.core.view.isVisible import androidx.core.view.isVisible
import bums.lunatic.launcher.BookmarkUploader import bums.lunatic.launcher.BookmarkUploader
import bums.lunatic.launcher.LunaticLauncher.Companion.getRuntime
import bums.lunatic.launcher.R import bums.lunatic.launcher.R
import bums.lunatic.launcher.helpers.ForeGroundService import bums.lunatic.launcher.helpers.ForeGroundService
import bums.lunatic.launcher.helpers.ForeGroundService.Companion.ACTION_VIDEO_DOWNLOAD import bums.lunatic.launcher.helpers.ForeGroundService.Companion.ACTION_VIDEO_DOWNLOAD
import bums.lunatic.launcher.helpers.ForeGroundService.Companion.EXTRA_TARGET_URL 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.home.tokiz.PortMessage
import bums.lunatic.launcher.model.Dotax import bums.lunatic.launcher.model.Dotax
import bums.lunatic.launcher.model.DotaxArticles import bums.lunatic.launcher.model.DotaxArticles
@ -61,6 +61,7 @@ import org.jsoup.Jsoup
import org.mozilla.gecko.util.ThreadUtils import org.mozilla.gecko.util.ThreadUtils
import org.mozilla.geckoview.GeckoResult import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.GeckoSession import org.mozilla.geckoview.GeckoSession
import org.mozilla.geckoview.GeckoSessionSettings
import org.mozilla.geckoview.GeckoView import org.mozilla.geckoview.GeckoView
import org.mozilla.geckoview.MediaSession import org.mozilla.geckoview.MediaSession
import org.mozilla.geckoview.WebExtension import org.mozilla.geckoview.WebExtension
@ -101,7 +102,7 @@ open class GeckoWeb @JvmOverloads constructor(
} }
} }
// 3. 저장된 세션 상태 복구 메서드 // // 3. 저장된 세션 상태 복구 메서드
fun restoreSessionState(session: GeckoSession) { fun restoreSessionState(session: GeckoSession) {
val stateJson = context.getSharedPreferences("GeckoPrefs", Context.MODE_PRIVATE) val stateJson = context.getSharedPreferences("GeckoPrefs", Context.MODE_PRIVATE)
.getString("gecko_session_state", null) .getString("gecko_session_state", null)
@ -109,7 +110,9 @@ open class GeckoWeb @JvmOverloads constructor(
if (!stateJson.isNullOrEmpty()) { if (!stateJson.isNullOrEmpty()) {
// 문자열에서 SessionState 객체 생성 // 문자열에서 SessionState 객체 생성
val state = GeckoSession.SessionState.fromString(stateJson) val state = GeckoSession.SessionState.fromString(stateJson)
Blog.LOGE("Restored state >>> ${state}")
if (state != null) { if (state != null) {
session.restoreState(state) session.restoreState(state)
Log.d("GeckoWeb", "Session State Restored") Log.d("GeckoWeb", "Session State Restored")
} }
@ -357,8 +360,9 @@ open class GeckoWeb @JvmOverloads constructor(
onPageStopCallback?.invoke(success) onPageStopCallback?.invoke(success)
saveCurrentSessionState() saveCurrentSessionState()
} }
override fun onSessionStateChange(session: GeckoSession, sessionState: GeckoSession.SessionState) {
override fun onSessionStateChange(session: GeckoSession, sessionState: GeckoSession.SessionState) {
lastSessionState = sessionState
onSessionStateChangeCallback?.invoke(sessionState) onSessionStateChangeCallback?.invoke(sessionState)
} }
} }
@ -421,7 +425,16 @@ open class GeckoWeb @JvmOverloads constructor(
private fun buildWeb() { private fun buildWeb() {
getRuntime()?.let { runtime -> 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) restoreSessionState(session)
session.open(runtime) session.open(runtime)
this.setSession(session) this.setSession(session)
@ -433,6 +446,7 @@ open class GeckoWeb @JvmOverloads constructor(
session.mediaDelegate = mediaDelegate session.mediaDelegate = mediaDelegate
session.promptDelegate = promptDelegate session.promptDelegate = promptDelegate
session.mediaSessionDelegate = mediaSessionDelegate session.mediaSessionDelegate = mediaSessionDelegate
runtime.settings.loginAutofillEnabled = true
runtime.webExtensionController.setAddonManagerDelegate(addonManagerDelegate) runtime.webExtensionController.setAddonManagerDelegate(addonManagerDelegate)
runtime.webExtensionController runtime.webExtensionController

View File

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

View File

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

View File

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