...
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user