This commit is contained in:
2025-08-28 15:13:40 +09:00
parent 6e64d739ac
commit 6404f4176e
3149 changed files with 738551 additions and 335 deletions
@@ -6,143 +6,258 @@ import android.os.Handler
import android.os.HandlerThread
import android.provider.MediaStore
import android.service.wallpaper.WallpaperService
import android.util.Log
import android.view.SurfaceHolder
import bums.lunatic.launcher.utils.Blog
import java.io.File
class MyWallpaperService : WallpaperService() {
override fun onCreateEngine(): Engine = NativeRenderEngine()
private val TAG = "MyWallpaperService"
override fun onCreateEngine(): Engine {
Log.d(TAG, "onCreateEngine: New engine instance created.")
return NativeRenderEngine()
}
inner class NativeRenderEngine : Engine() {
private lateinit var holder: SurfaceHolder
private lateinit var handlerThread: HandlerThread
private lateinit var handler: Handler
private var running = false
private lateinit var holder: SurfaceHolder
private var wasInPreview = false
private var nativeRenderer: NativeRenderer? = null
private var mediaFiles: List<File> = emptyList()
private var currentMediaIndex = -1
private val frameDelayMs = 16L // 약 60fps
private val videoExtensions = listOf("mp4", "mkv", "avi")
private val imageExtensions = listOf("jpg", "jpeg", "png", "bmp", "webp")
private var isVisible = false
private var isSurfaceValid = false
private val frameDelayMs = 1000L / 45L
private val syncDelayMs = 250L // 상태 변경을 처리하기 전의 지연 시간
private var frameCount = 0
private val debugLogCheck = 240
private val renderRunnable = object : Runnable {
override fun run() {
if (!running || holder.surface == null) return
nativeRenderer?.nativeRender(holder.surface)
if (!isVisible || !isSurfaceValid) return
// [수정] nativeRender -> render
nativeRenderer?.render(holder.surface)
frameCount++
if (frameCount % debugLogCheck == 0) {
frameCount = 0
// [수정] nativeGetDebugInfo -> getDebugInfo
val debugInfo = nativeRenderer?.getDebugInfo() ?: "Kotlin nativeRenderer is null"
Log.d(TAG, debugInfo)
}
handler.postDelayed(this, frameDelayMs)
}
}
private fun loadMediaFiles() {
val mediaDir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "wallPapers")
val supportedExtensions = videoExtensions + imageExtensions
mediaFiles = if (mediaDir.exists() && mediaDir.isDirectory) {
mediaDir.listFiles()?.filter { supportedExtensions.contains(it.extension.lowercase()) }?.toList() ?: emptyList()
private val destroyRenderer = Runnable {
nativeRenderer?.destroy()
nativeRenderer = null
}
// --- ⬇️ 렌더링 상태를 동기화하는 Runnable 추가 ⬇️ ---
private val syncRenderStateRunnable = Runnable {
// 딜레이 이후, 최종 상태를 확인하고 렌더링을 시작할지 중지할지 결정
if (isVisible && isSurfaceValid) {
startRendering()
} else {
emptyList()
stopRendering()
}
}
override fun onCreate(surfaceHolder: SurfaceHolder) {
super.onCreate(surfaceHolder)
this.holder = surfaceHolder
Log.d(TAG, "onCreate: Engine created.")
wasInPreview = isPreview()
Log.d(TAG, "onCreate: Engine created. Initial isPreview = $wasInPreview")
ensureHandlerThread()
}
private fun ensureHandlerThread() {
if (!::handlerThread.isInitialized || !handlerThread.isAlive) {
Log.w(TAG, "Handler thread was dead or not initialized. Recreating.")
handlerThread = HandlerThread("NativeRenderThread")
handlerThread.start()
handler = Handler(handlerThread.looper)
}
}
override fun onSurfaceCreated(holder: SurfaceHolder) {
super.onSurfaceCreated(holder)
Log.i(TAG, "onSurfaceCreated: Surface is ready.")
this.holder = holder
isSurfaceValid = true
ensureHandlerThread()
initializeRenderer()
// --- 수정: 즉시 시작 대신 상태 동기화 요청 ---
requestSyncRenderState()
}
override fun onSurfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
super.onSurfaceChanged(holder, format, width, height)
Log.i(TAG, "onSurfaceChanged: Surface changed.")
// this.holder = holder
isSurfaceValid = true
ensureHandlerThread()
if (nativeRenderer == null) {
Log.w(TAG, "onSurfaceChanged: Renderer was null. Re-initializing.")
initializeRenderer()
}
// --- 디버깅 로그 추가 ---
Blog.LOGE("MyWallpaperService", "Found ${mediaFiles.size} media files.")
requestSyncRenderState()
}
override fun onVisibilityChanged(visible: Boolean) {
super.onVisibilityChanged(visible)
val currentlyInPreview = isPreview()
if (wasInPreview && !currentlyInPreview) {
// 이 블록이 실행되는 시점이 바로 "배경화면으로 설정"을 누른 직후입니다.
Log.i(TAG, ">>>>>>>>>> TRANSITION: Preview -> Live Wallpaper! <<<<<<<<<<")
// 여기에 실제 배경화면으로 설정되었을 때 딱 한 번만 실행하고 싶은 코드를 넣을 수 있습니다.
}
Log.i(TAG, "onVisibilityChanged: Visibility changed to -> $visible")
isVisible = visible
if (isVisible) {
handler.removeCallbacks(destroyRenderer)
}
ensureHandlerThread()
if (nativeRenderer == null) {
Log.w(TAG, "onVisibilityChanged: Renderer was null. Re-initializing.")
initializeRenderer()
}
requestSyncRenderState()
}
// --- ⬇️ 상태 동기화를 요청하는 함수 추가 ⬇️ ---
private fun requestSyncRenderState() {
// 이전에 요청된 동기화 작업이 있다면 취소하고, 새로운 동기화 작업을 딜레이와 함께 예약
handler.removeCallbacks(syncRenderStateRunnable)
handler.postDelayed(syncRenderStateRunnable, syncDelayMs)
}
override fun onSurfaceDestroyed(holder: SurfaceHolder) {
super.onSurfaceDestroyed(holder)
Log.e(TAG, "onSurfaceDestroyed: Surface is destroyed.")
isSurfaceValid = false
// --- 수정: 즉시 중지 대신 상태 동기화 요청 ---
requestSyncRenderState()
handler.removeCallbacks(destroyRenderer)
handler.postDelayed(destroyRenderer, syncDelayMs)
}
override fun onDestroy() {
super.onDestroy()
Log.e(TAG, "onDestroy: Engine is being destroyed. Quitting thread.")
if (::handlerThread.isInitialized) {
handlerThread.quitSafely()
}
}
private fun startRendering() {
if (isVisible && isSurfaceValid) {
Log.d(TAG, "startRendering: Conditions met. Starting loop. ${handler}")
handler.removeCallbacks(renderRunnable)
handler.post(renderRunnable)
} else {
Log.d(TAG, "startRendering: Conditions not met. Starting loop fail.")
}
}
private fun stopRendering() {
Log.w(TAG, "stopRendering: Stopping loop.")
// 핸들러가 초기화된 경우에만 콜백 제거 시도
if (::handler.isInitialized) {
handler.removeCallbacks(renderRunnable)
} else {
Log.w(TAG, "stopRendering: Stopping loop. ${::handler.isInitialized}")
}
}
private fun initializeRenderer() {
Log.d(TAG, "Initializing Renderer...")
nativeRenderer = NativeRenderer()
nativeRenderer?.initialize() // nativeInit() -> initialize()
nativeRenderer?.setFadeDuration(1500)
nativeRenderer?.setTurnPageDuration(5000)
nativeRenderer?.setAnimationMode(NativeRenderer.ANIMATION_MODE_PAGE_TURN)
nativeRenderer?.setAnimationSpeed(1.0f) // nativeSetAnimationSpeed -> setAnimationSpeed
NativeRenderer.nativeSetNextMediaCallback(nextMediaCallback)
if (mediaFiles.isEmpty()) {
handler.post { loadMediaFiles() }
}
}
// ... loadMediaFiles, nextMediaCallback, getFdFromPath는 이전과 동일 ...
private fun loadMediaFiles() {
val mediaDir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "wallPapers")
if (!mediaDir.exists()) mediaDir.mkdirs()
val supportedExtensions = listOf("mp4", "mkv", "avi", "mov", "webm", "jpg", "jpeg", "png", "bmp", "webp")
mediaFiles = mediaDir.listFiles()
?.filter { supportedExtensions.contains(it.extension.lowercase()) }
?.toList() ?: emptyList()
Log.d(TAG, "Found ${mediaFiles.size} media files.")
if (mediaFiles.isNotEmpty()) {
currentMediaIndex = 0
val firstFilePath = mediaFiles[currentMediaIndex].absolutePath
Blog.LOGE("MyWallpaperService", "Attempting to load initial media: $firstFilePath")
getFdFromPath(firstFilePath)?.let { fd ->
Blog.LOGE("MyWallpaperService", "Successfully got fd ($fd) for initial media. Calling nativeSetCurrentMedia.")
nativeRenderer?.nativeSetCurrentMedia(fd)
val initialFile = mediaFiles.random()
Log.d(TAG, "Attempting to load initial random media via preloader: ${initialFile.absolutePath}")
getFdFromPath(initialFile.absolutePath)?.let { fd ->
nativeRenderer?.startNextPreload(fd)
} ?: run {
// 'let' 블록이 실행되지 않으면 fd가 null이라는 의미
Blog.LOGE("MyWallpaperService", "Failed to get fd for initial media: $firstFilePath")
Log.e(TAG, "Failed to get fd for initial media: ${initialFile.absolutePath}")
}
if (mediaFiles.size > 1) {
// Preload 로직도 동일하게 로그 추가 가능
val nextFilePath = mediaFiles[1].absolutePath
Blog.LOGE("MyWallpaperService", "Attempting to preload next media: $nextFilePath")
getFdFromPath(nextFilePath)?.let { fd ->
Blog.LOGE("MyWallpaperService", "Successfully got fd ($fd) for preload. Calling nativeStartNextPreload.")
nativeRenderer?.nativeStartNextPreload(fd)
} ?: run {
Blog.LOGE("MyWallpaperService", "Failed to get fd for preload media: $nextFilePath")
}
}
} else {
Blog.LOGE("MyWallpaperService", "Media files list is empty. No media will be loaded.")
}
}
private val nextMediaCallback = object : NativeRenderer.NextMediaCallback {
override fun onNextMediaRequested() {
if (mediaFiles.isEmpty()) return
currentMediaIndex = (currentMediaIndex + 1) % mediaFiles.size
// 다음 파일 예비 로딩 시에도 파일 디스크립터를 전달
getFdFromPath(mediaFiles[currentMediaIndex].absolutePath)?.let { fd ->
nativeRenderer?.nativeStartNextPreload(fd)
val mediaDir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "wallPapers")
if (!mediaDir.exists()) mediaDir.mkdirs()
val supportedExtensions = listOf("mp4", "mkv", "avi", "mov", "webm", "jpg", "jpeg", "png", "bmp", "webp")
mediaFiles = mediaDir.listFiles()
?.filter { supportedExtensions.contains(it.extension.lowercase()) }
?.toList() ?: emptyList()
val nextFile = mediaFiles.random()
Log.d(TAG, "Callback: Preloading next random media: ${nextFile.absolutePath}")
getFdFromPath(nextFile.absolutePath)?.let { fd ->
nativeRenderer?.startNextPreload(fd)
} ?: run {
Log.e(TAG, "Callback: Failed to get fd for ${nextFile.absolutePath}")
}
}
}
// 파일 경로로 MediaStore를 쿼리하여 File Descriptor를 얻는 함수
private fun getFdFromPath(path: String): Int? {
var fileDescriptor: Int? = null
val uri = MediaStore.Files.getContentUri("external")
val projection = arrayOf(MediaStore.Files.FileColumns._ID)
val selection = "${MediaStore.Files.FileColumns.DATA} = ?"
val selectionArgs = arrayOf(path)
try {
return try {
contentResolver.query(uri, projection, selection, selectionArgs, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID))
val fileUri = ContentUris.withAppendedId(uri, id)
contentResolver.openFileDescriptor(fileUri, "r")?.use { pfd ->
fileDescriptor = pfd.detachFd() // 중요: fd 소유권을 네이티브로 넘기기 위해 detach
pfd.detachFd()
}
}
} else null
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(TAG, "Error getting FD for path: $path", e)
null
}
return fileDescriptor
}
override fun onCreate(surfaceHolder: SurfaceHolder) {
super.onCreate(surfaceHolder)
holder = surfaceHolder
}
override fun onSurfaceCreated(holder: SurfaceHolder) {
super.onSurfaceCreated(holder)
nativeRenderer = NativeRenderer()
nativeRenderer?.nativeInit()
handlerThread = HandlerThread("NativeRenderThread").apply { start() }
handler = Handler(handlerThread.looper)
// 콜백 등록 및 최초 미디어 로딩
nativeRenderer?.nativeSetNextMediaCallback(nextMediaCallback)
loadMediaFiles()
running = true
handler.post(renderRunnable)
}
override fun onSurfaceDestroyed(holder: SurfaceHolder) {
running = false
handler.removeCallbacks(renderRunnable)
nativeRenderer?.nativeDestroy()
handlerThread.quitSafely()
super.onSurfaceDestroyed(holder)
}
}
}
@@ -1,5 +1,6 @@
package bums.lunatic.launcher.wall
import android.util.Log
import android.view.Surface
class NativeRenderer {
@@ -8,21 +9,99 @@ class NativeRenderer {
fun onNextMediaRequested()
}
// 네이티브 핸들을 저장할 멤버 변수. 0은 유효하지 않은 핸들
private var nativeHandle: Long = 0
// --- Public API ---
fun initialize(): Boolean {
if (nativeHandle != 0L) {
Log.w("NativeRenderer", "Already initialized.")
return true
}
nativeHandle = nativeInit()
return nativeHandle != 0L
}
fun destroy() {
if (nativeHandle != 0L) {
nativeDestroy(nativeHandle)
nativeHandle = 0L
}
}
fun render(surface: Surface) {
if (nativeHandle != 0L) {
nativeRender(nativeHandle, surface)
}
}
fun startNextPreload(fd: Int) {
if (nativeHandle != 0L) {
nativeStartNextPreload(nativeHandle, fd)
}
}
fun setAnimationSpeed(speed: Float) {
if (nativeHandle != 0L) {
nativeSetAnimationSpeed(nativeHandle, speed)
}
}
fun setAnimationMode(mode: Int) {
if (nativeHandle != 0L) {
nativeSetAnimationMode(nativeHandle, mode)
}
}
fun setFadeDuration(durationMs: Int) {
if (nativeHandle != 0L) {
nativeSetFadeDuration(nativeHandle, durationMs)
}
}
fun setTurnPageDuration(durationMs: Int) {
if (nativeHandle != 0L) {
nativeSetPageTurnDelay(nativeHandle, durationMs)
}
}
fun getDebugInfo(): String {
return if (nativeHandle != 0L) {
nativeGetDebugInfo(nativeHandle)
} else {
"Kotlin Wrapper: Native handle is null."
}
}
// --- Private JNI declarations ---
private external fun nativeInit(): Long
private external fun nativeDestroy(nativeHandle: Long)
private external fun nativeRender(nativeHandle: Long, surface: Surface)
private external fun nativeStartNextPreload(nativeHandle: Long, fd: Int)
private external fun nativeSetAnimationSpeed(nativeHandle: Long, speed: Float)
private external fun nativeSetAnimationMode(nativeHandle: Long, mode: Int)
private external fun nativeSetFadeDuration(nativeHandle: Long, duration: Int)
private external fun nativeGetDebugInfo(nativeHandle: Long): String
private external fun nativeSetPageTurnDelay(nativeHandle: Long, duration: Int)
// --- Companion Object ---
companion object {
init {
System.loadLibrary("native_renderer")
}
// --- ⬇️ 애니메이션 모드 상수 추가 ⬇️ ---
const val ANIMATION_MODE_PAN = 0
const val ANIMATION_MODE_ZOOM = 1
const val ANIMATION_MODE_NONE = 2
const val ANIMATION_MODE_RANDOM = 3
const val ANIMATION_MODE_PAN_ONE_WAY = 4
const val ANIMATION_MODE_PAGE_TURN = 5 // <-- 새로운 상수 추가
// 이 콜백은 전역이라 일단 public external로 유지
@JvmStatic
external fun nativeSetNextMediaCallback(callback: NextMediaCallback)
}
external fun nativeInit(): Boolean
external fun nativeDestroy()
external fun nativeRender(surface: Surface)
// ...
// external fun nativeSetCurrentMedia(path: String) // 기존 코드
external fun nativeSetCurrentMedia(fd: Int) // 수정
// external fun nativeStartNextPreload(path: String) // 기존 코드
external fun nativeStartNextPreload(fd: Int) // 수정
// ...
external fun nativeSetNextMediaCallback(callback: NextMediaCallback)
}