diff --git a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt index 6883e08d..91b99021 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt @@ -104,8 +104,6 @@ import java.util.Date open class LauncherActivity : CommonActivity() { private lateinit var binding: LauncherActivityBinding - private lateinit var settingsPrefs: SharedPreferences -// lateinit var viewPager: ViewPager2 companion object { private var sRuntime: GeckoRuntime? = null @@ -143,10 +141,6 @@ open class LauncherActivity : CommonActivity() { override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - Blog.LOGE("onConfigurationChanged Configuration >> ${newConfig}") - - Blog.LOGE("onConfigurationChanged newConfig?.screenWidthDp >> ${newConfig?.screenWidthDp}") - Blog.LOGE("onConfigurationChanged newConfig?.screenHeightDp >> ${newConfig?.screenHeightDp}") isOpendFold = (newConfig.screenWidthDp * 1.1f) > newConfig.screenHeightDp val nullCursor = PointerIcon.getSystemIcon(this, PointerIcon.TYPE_NULL) binding.root.setPointerIcon(nullCursor) @@ -289,25 +283,17 @@ open class LauncherActivity : CommonActivity() { .setRssList(arrayListOf().apply { this.addAll(jjjj)}) .setRssId(origin) -// .webViewDesktopMode(true) .showIconClose(true).showIconBack(false).showProgressBar(true).backPressToClose(false).webViewMixedContentMode(1) .show(origin) } } } override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean { -// Blog.LOGE("dispatchKeyEvent ev?.device?.name >>> ${ev?.device?.name}") - /// || ev?.device?.name?.contains("SM-031N Mouse") == true if (ev?.device?.name?.contains("BLE-M3") == true) { Blog.LOGE("keyEvent >>>>> dispatchGenericMotionEvent ${ev}") ev?.action?.let { action -> when(action) { MotionEvent.ACTION_HOVER_ENTER -> { - -// if (onExit) { -// onExit = false -// return true -// } return false } MotionEvent.ACTION_HOVER_MOVE ->{ @@ -399,8 +385,6 @@ open class LauncherActivity : CommonActivity() { } } } -// BLog.LOGE("onNewIntent intent?.dataString >> ${intent?.dataString}") -// BLog.LOGE("onNewIntent intent?.data >> ${intent?.data}") } else if (intent?.action == Intent.ACTION_WEB_SEARCH) { openWithIntent(intent) } else { @@ -431,13 +415,6 @@ open class LauncherActivity : CommonActivity() { } } } -// android.intent.extra.EXTRA_START_REASON :: value >> startDockOrHome - - -// binding.viewPager.invalidate() -// binding.viewPager.post { -// binding.viewPager?.adapter?.notifyDataSetChanged() -// } super.onNewIntent(intent) } @@ -462,7 +439,7 @@ open class LauncherActivity : CommonActivity() { binding.currentAddress.text = str } - @SuppressLint("NewApi", "MissingPermission") + @SuppressLint("NewApi", "MissingPermission", "ClickableViewAccessibility") override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() super.onCreate(savedInstanceState) @@ -484,34 +461,22 @@ open class LauncherActivity : CommonActivity() { getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); lActivity = this -// DynamicColors.applyToActivityIfAvailable(this) - settingsPrefs = getSharedPreferences(PREFS_SETTINGS, 0) - - + DynamicColors.applyToActivityIfAvailable(this) binding = LauncherActivityBinding.inflate(layoutInflater) setContentView(binding.root) HeadsetActionButtonReceiver.register(this) ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets -> - - // 시스템바 인셋 가져오기 (상단 상태바 + 하단 네비게이션바) val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) - - // 뷰에 패딩 적용 (겹치지 않도록) view.setPadding(insets.left, insets.top, insets.right, insets.bottom) - - // 변경된 인셋 반환 WindowInsetsCompat.CONSUMED } - /* handle navigation back events */ handleBackPress() updateLocationService() - - showContents(binding.feeds.id) - binding.floatingActionMenu.setOnTouchListener { v,e-> + binding.floatingActionMenu.setOnTouchListener { v: View, e: MotionEvent -> if (binding.floatingActionMenu.isOpened) { binding.floatingActionMenu.close(true) return@setOnTouchListener true diff --git a/app/src/main/kotlin/bums/lunatic/launcher/tokiz/BaseToki.kt b/app/src/main/kotlin/bums/lunatic/launcher/tokiz/BaseToki.kt index 9673987b..d1fd0e63 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/tokiz/BaseToki.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/tokiz/BaseToki.kt @@ -319,6 +319,7 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { override fun onPageStart(session: GeckoSession, url: String) { super.onPageStart(session, url) + binding.progress.visibility = VISIBLE if (aceptUrl(url)) { @@ -1263,6 +1264,7 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface { activity?.runOnUiThread { view.text = contents view.visibility = VISIBLE + binding.menuWeb.visibility = View.GONE } view.forceUpdateUI() lastedUrl?.let { diff --git a/app/src/main/kotlin/bums/lunatic/launcher/tokiz/Novels.kt b/app/src/main/kotlin/bums/lunatic/launcher/tokiz/Novels.kt index 50bc6a31..e850beaf 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/tokiz/Novels.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/tokiz/Novels.kt @@ -171,6 +171,7 @@ class Novels : BaseToki(), PagedTextViewInterface { if (binding.pagedLayer.isVisible) { binding.pagedLayer.visibility = GONE } + binding.menuWeb.visibility = View.VISIBLE } } diff --git a/app/src/main/kotlin/bums/lunatic/launcher/tokiz/view/PagedTextLayout.kt b/app/src/main/kotlin/bums/lunatic/launcher/tokiz/view/PagedTextLayout.kt index 3fda96d7..b434fe80 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/tokiz/view/PagedTextLayout.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/tokiz/view/PagedTextLayout.kt @@ -197,7 +197,7 @@ class PagedTextLayout : ConstraintLayout , PagedTextGenerateInterface { if (w != oldw || oldh != h) { postDelayed(Runnable { layoutChange(w > (h * 0.7f)) - + Blog.LOGD(log = "onSizeChanged>> ${w} ${h} ${h * 0.7}") },20) }else { @@ -216,13 +216,15 @@ class PagedTextLayout : ConstraintLayout , PagedTextGenerateInterface { } hiddenTextView?.visibility = GONE } - + var defaultAlpha = 171 fun setColorStyle(colors : Array) { - setBackgroundColor(Color.parseColor(colors.get(1))) - mainTextView?.setBackgroundColor(Color.parseColor(colors.get(1))) - sencondTextView?.setBackgroundColor(Color.parseColor(colors.get(1))) - mainTextView?.setTextColor(Color.parseColor(colors.get(0))) - sencondTextView?.setTextColor(Color.parseColor(colors.get(0))) + var bgColor = Color.parseColor(colors.get(1)) + val newColorInt = (bgColor and 0x00FFFFFF) or (defaultAlpha shl 24) + setBackgroundColor(newColorInt) + mainTextView?.setBackgroundColor(newColorInt) + sencondTextView?.setBackgroundColor(newColorInt) + mainTextView?.setTextColor(Color.parseColor("#FFFFFF")) + sencondTextView?.setTextColor(Color.parseColor("#FFFFFF")) } // fun setPagedTextViewInterface(pagedTextViewInterface: PagedTextViewInterface) = hiddenTextView?.setPagedTextViewInterface(pagedTextViewInterface) diff --git a/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt b/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt index a457abff..4bf6ff42 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt @@ -2,6 +2,8 @@ package bums.lunatic.launcher.wall import android.content.Context import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.RectF @@ -10,6 +12,9 @@ import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar import android.media.MediaExtractor import android.media.MediaFormat +import android.os.Environment +import android.os.Handler +import android.os.HandlerThread import android.renderscript.Allocation import android.renderscript.Element import android.renderscript.RenderScript @@ -21,254 +26,495 @@ import android.view.SurfaceHolder import android.view.WindowManager import bums.lunatic.launcher.R import bums.lunatic.launcher.utils.Blog -import java.nio.ByteBuffer - +import java.io.File +import kotlin.random.Random class MyWallpaperService : WallpaperService() { override fun onCreateEngine(): Engine { return VideoEngine() } - inner class VideoEngine : Engine() { - lateinit var holder: SurfaceHolder - private var renderThread: RenderThread? = null + inner class VideoEngine : WallpaperService.Engine() { + private lateinit var holder: SurfaceHolder + private lateinit var renderLooper: RenderLooper + private lateinit var handlerThread: HandlerThread private var screenWidth: Int = 0 private var screenHeight: Int = 0 override fun onCreate(surfaceHolder: SurfaceHolder) { super.onCreate(surfaceHolder) - this.holder = surfaceHolder + holder = surfaceHolder val wm = getSystemService(Context.WINDOW_SERVICE) as WindowManager val metrics = DisplayMetrics() if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { - // Android 11 이상 권장 방법 val display = wm.currentWindowMetrics - // Insets(네비게이션바 등 제외 영역 계산 가능), 필요한 경우 처리 - val bounds = display.bounds - screenWidth = bounds.width() - screenHeight = bounds.height() + screenWidth = display.bounds.width() + screenHeight = display.bounds.height() } else { - // 하위 버전 처리 방법 @Suppress("DEPRECATION") wm.defaultDisplay.getMetrics(metrics) screenWidth = metrics.widthPixels screenHeight = metrics.heightPixels } - - } override fun onSurfaceCreated(holder: SurfaceHolder) { super.onSurfaceCreated(holder) - renderThread = RenderThread(holder, getApplicationContext(), screenWidth, screenHeight) - renderThread?.start() + handlerThread = HandlerThread("RenderHandlerThread") + handlerThread.start() + renderLooper = RenderLooper(this@MyWallpaperService, holder, screenWidth, screenHeight, handlerThread) + renderLooper.start() } - override fun onSurfaceDestroyed(holder: SurfaceHolder?) { + override fun onSurfaceDestroyed(holder: SurfaceHolder) { super.onSurfaceDestroyed(holder) - renderThread?.interrupt() - renderThread = null + renderLooper.stop() + handlerThread.quitSafely() + } + + override fun onSurfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { + super.onSurfaceChanged(holder, format, width, height) + screenWidth = width + screenHeight = height + renderLooper.updateScreenSize(width, height) } } } -class RenderThread( - private val holder: SurfaceHolder, + +interface Renderer { + fun render(canvas: Canvas) + fun release() + fun updateScreenSize(width: Int, height: Int) + fun resetOffset() + fun update() + fun isCycleCompleted(): Boolean +} + +class ImageRenderer( private val context: Context, - private val screenWidth: Int, - private val screenHeight: Int -) : Thread() { + private var bitmap: Bitmap, + private var screenWidth: Int, + private var screenHeight: Int +) : Renderer { - private var running = true - - private var extractor: MediaExtractor? = null - private var codec: MediaCodec? = null - private var videoTrackIndex: Int = -1 - private val bufferInfo = MediaCodec.BufferInfo() - - private var rs: RenderScript? = null - private var yuvToRgb: ScriptIntrinsicYuvToRGB? = null - - private var videoWidth = 0 - private var videoHeight = 0 - - // 왕복 이동 관련 변수 - private var offset = 0f - private var direction = 1 - private val speed = 2.5f // 픽셀/프레임, 효과 속도 조절 - - // 비율 맞춤 렌더 사이즈와 경계 private var renderWidth = 0f private var renderHeight = 0f private var renderStartX = 0f private var renderStartY = 0f - private var maxOffset = 0f + private var moveXAxis = false + private var moveYAxis = false + private var maxOffset = 0f + + private var offset = 0f + private var direction = 1 + private val speed = 2.5f + + private val holdDurationMs = 10_000L + private var noMoveStartTime: Long = 0L + private var moveCycleCompleted = false + + private val paintOverlay = Paint().apply { + color = Color.WHITE + textSize = 40f + setShadowLayer(4f, 2f, 2f, Color.DKGRAY) + } + + init { + setupScaling(bitmap.width, bitmap.height) + } + + override fun render(canvas: Canvas) { + updateOffset() + val drawX = if (moveXAxis) renderStartX - offset else renderStartX + val drawY = if (moveYAxis) renderStartY - offset else renderStartY + val dstRect = RectF(drawX, drawY, drawX + renderWidth, drawY + renderHeight) + + canvas.drawColor(Color.BLACK) + canvas.drawBitmap(bitmap, null, dstRect, null) + // canvas.drawText("Live Wallpaper Overlay", 40f, 80f, paintOverlay) + } + + override fun release() { + bitmap.recycle() + } + + override fun updateScreenSize(width: Int, height: Int) { + screenWidth = width + screenHeight = height + setupScaling(bitmap.width, bitmap.height) + resetOffset() + } + + override fun resetOffset() { + offset = 0f + direction = 1 + noMoveStartTime = 0L + moveCycleCompleted = false + } + + override fun update() { + if (!moveXAxis && !moveYAxis) { + val now = System.currentTimeMillis() + if (noMoveStartTime == 0L) noMoveStartTime = now + else if (now - noMoveStartTime >= holdDurationMs) { + noMoveStartTime = 0L + moveCycleCompleted = true + } + } else { + if (moveCycleCompleted) { + moveCycleCompleted = false + } + noMoveStartTime = 0L + } + } + + override fun isCycleCompleted(): Boolean = moveCycleCompleted + + private fun setupScaling(mediaWidth: Int, mediaHeight: Int) { + val isLandscape = mediaWidth > mediaHeight + + var scale = if (isLandscape) { + // 가로가 긴 경우 => 세로 기준 스케일링 + screenHeight.toFloat() / mediaHeight + } else { + // 세로가 긴 경우 => 가로 기준 스케일링 + screenWidth.toFloat() / mediaWidth + } + + var targetWidth = mediaWidth * scale + var targetHeight = mediaHeight * scale + + // 2차 보정: 스케일 후 크기가 기기 해상도보다 작으면 크기 맞춤 + if (targetWidth < screenWidth) { + val scaleX = screenWidth.toFloat() / mediaWidth + if (scaleX > scale) { + scale = scaleX + targetWidth = mediaWidth * scale + targetHeight = mediaHeight * scale + } + } + if (targetHeight < screenHeight) { + val scaleY = screenHeight.toFloat() / mediaHeight + if (scaleY > scale) { + scale = scaleY + targetWidth = mediaWidth * scale + targetHeight = mediaHeight * scale + } + } + + renderWidth = targetWidth + renderHeight = targetHeight + + renderStartX = 0f + renderStartY = 0f + + val diffWidth = renderWidth - screenWidth + val diffHeight = renderHeight - screenHeight + + moveXAxis = diffWidth > 1f + moveYAxis = diffHeight > 1f + + maxOffset = if (moveXAxis) diffWidth else if (moveYAxis) diffHeight else 0f + } + + private fun updateOffset() { + if (moveXAxis || moveYAxis) { + offset += direction * speed + if (offset < 0f) { + offset = 0f + direction = 1 + } + if (offset > maxOffset) { + offset = maxOffset + direction = -1 + } + } + } +} + +class VideoRenderer( + private val context: Context, + private val extractor: MediaExtractor, + private var screenWidth: Int, + private var screenHeight: Int +) : Renderer, Runnable { + private var codec: MediaCodec? = null + private var videoTrackIndex: Int = -1 + private val bufferInfo = MediaCodec.BufferInfo() + + private var videoWidth = 0 + private var videoHeight = 0 + + private var offset = 0f + private var direction = 1 + private val speed = 2.5f + + private var renderWidth = 0f + private var renderHeight = 0f + private var renderStartX = 0f + private var renderStartY = 0f + + private var moveXAxis = false + private var moveYAxis = false + private var maxOffset = 0f + + private var running = true + private var isEOS = false + private var keyFrameRate = 60 + private var rs: RenderScript? = null + private var yuvToRgb: ScriptIntrinsicYuvToRGB? = null + + private var bitmap: Bitmap? = null + + private val paintOverlay = Paint().apply { + color = Color.WHITE + textSize = 40f + setShadowLayer(4f, 2f, 2f, Color.DKGRAY) + } + + private var noMoveStartTime: Long = 0L + private val holdDurationMs = 10_000L + private var moveCycleCompleted = false + + private val lock = Object() + + init { + setupDecoder() + } + + override fun render(canvas: Canvas) { + synchronized(lock) { + if (bitmap != null) { + val drawX = if (moveXAxis) renderStartX - offset else renderStartX + val drawY = if (moveYAxis) renderStartY - offset else renderStartY + val dstRect = RectF(drawX, drawY, drawX + renderWidth, drawY + renderHeight) + + canvas.drawColor(Color.BLACK) + canvas.drawBitmap(bitmap!!, null, dstRect, null) + // canvas.drawText("Live Wallpaper Overlay", 40f, 80f, paintOverlay) + } + } + } + + override fun release() { + codec?.stop() + codec?.release() + extractor.release() + rs?.destroy() + bitmap?.recycle() + running = false + } + + override fun updateScreenSize(width: Int, height: Int) { + synchronized(lock) { + screenWidth = width + screenHeight = height + setupScaling(videoWidth, videoHeight) + resetOffset() + } + } + + override fun resetOffset() { + synchronized(lock) { + offset = 0f + direction = 1 + noMoveStartTime = 0L + moveCycleCompleted = false + } + } + + override fun update() { + synchronized(lock) { + if (!moveXAxis && !moveYAxis) { + val now = System.currentTimeMillis() + if (noMoveStartTime == 0L) noMoveStartTime = now + else if (now - noMoveStartTime >= holdDurationMs) { + noMoveStartTime = 0L + moveCycleCompleted = true + } + } else { + if (moveCycleCompleted) { + moveCycleCompleted = false + } + noMoveStartTime = 0L + } + } + } + + override fun isCycleCompleted(): Boolean = moveCycleCompleted + + operator fun invoke() = run() + override fun run() { try { - // 1. MediaExtractor 초기화 - extractor = MediaExtractor().apply { - // 영상 소스 위치에 따라 아래 setDataSource 예시를 변경하십시오. - val afd = context.resources.openRawResourceFd(R.raw.sample) // ex) res/raw/sample.mp4 - setDataSource(afd.fileDescriptor, afd.startOffset, afd.length) - afd.close() - for (i in 0 until trackCount) { - val format = getTrackFormat(i) - val mime = format.getString(MediaFormat.KEY_MIME) ?: "" + val inputBuffersTimeoutUs = 10000L - if (format.containsKey(MediaFormat.KEY_FRAME_RATE)) { - keyFrameRate = format.getInteger(MediaFormat.KEY_FRAME_RATE) - } else { - keyFrameRate = 60 - } - if (mime.startsWith("video/")) { - videoTrackIndex = i - selectTrack(i) + while (running && !Thread.currentThread().isInterrupted) { + if (isEOS) { - videoWidth = format.getInteger(MediaFormat.KEY_WIDTH) - videoHeight = format.getInteger(MediaFormat.KEY_HEIGHT) - break - } + continue } - } - if (videoTrackIndex < 0) return - - // 2. MediaCodec 초기화 - extractor?.getTrackFormat(videoTrackIndex)?.let { format -> - codec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME)!!).apply { - configure(format, null, null, 0) - start() - } - } - - // 3. RenderScript 초기화 - rs = RenderScript.create(context) - yuvToRgb = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs)) - - // 4. 비율 맞춤 Scaling 및 이동 관련 변수 설정 - val scale = if (videoWidth > videoHeight) { - // 가로가 더 긴 경우: 화면 세로에 맞춤 - screenHeight.toFloat() / videoHeight - } else { - // 세로가 더 긴 경우: 화면 가로에 맞춤 - screenWidth.toFloat() / videoWidth - } - renderWidth = videoWidth * scale - renderHeight = videoHeight * scale - renderStartX = 0f;//(screenWidth - renderWidth) / 2f - renderStartY = 0f;//(screenHeight - renderHeight) / 2f - - moveXAxis = renderWidth > screenWidth - maxOffset = if (moveXAxis) renderWidth - screenWidth else if (renderHeight > screenHeight) renderHeight - screenHeight else 0f - - var isEOS = false - - while (running && !isInterrupted) { - // 5. MediaCodec 프레임 디코딩 - if (!isEOS) { - val inputBufferIndex = codec?.dequeueInputBuffer(10000) ?: -1 - if (inputBufferIndex >= 0) { - val inputBuffer = codec?.getInputBuffer(inputBufferIndex) - if (inputBuffer != null) { - val sampleSize = extractor?.readSampleData(inputBuffer, 0) ?: -1 - if (sampleSize < 0) { -// codec?.queueInputBuffer( -// inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM) -// isEOS = true - extractor?.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC) - isEOS = false // false로 재설정해서 계속 받게 - continue - } else { - val pts = extractor?.sampleTime ?: 0L - codec?.queueInputBuffer(inputBufferIndex, 0, sampleSize, pts, 0) - extractor?.advance() - } + val inputBufferIndex = codec?.dequeueInputBuffer(inputBuffersTimeoutUs) ?: -1 + if (inputBufferIndex >= 0) { + val inputBuffer = codec?.getInputBuffer(inputBufferIndex) + if (inputBuffer != null) { + val sampleSize = extractor.readSampleData(inputBuffer, 0) + if (sampleSize < 0) { + codec?.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM) + isEOS = true + } else { + val pts = extractor.sampleTime + codec?.queueInputBuffer(inputBufferIndex, 0, sampleSize, pts, 0) + extractor.advance() } } } - val outputBufferIndex = codec?.dequeueOutputBuffer(bufferInfo, 10000) ?: -1 + val outputBufferIndex = codec?.dequeueOutputBuffer(bufferInfo, inputBuffersTimeoutUs) ?: -1 if (outputBufferIndex >= 0) { val outputBuffer = codec?.getOutputBuffer(outputBufferIndex) - Blog.LOGE("codec.outputFormat >>> ${codec?.outputFormat}") if (bufferInfo.size > 0 && outputBuffer != null) { val format = codec?.outputFormat - val colorFormat = format?.getInteger(MediaFormat.KEY_COLOR_FORMAT) ?: /*기본값*/0 - val stride = format?.getInteger(MediaFormat.KEY_STRIDE) ?: videoWidth - val sliceHeight = format?.getInteger(MediaFormat.KEY_SLICE_HEIGHT) ?: videoHeight -// outputBuffer.get(yuvData) - val expectedSize = videoWidth * videoHeight * 3 / 2 - val yuvData = ByteArray(expectedSize) + val colorFormat = format?.getInteger(MediaFormat.KEY_COLOR_FORMAT) ?: 0 + val rawYUV = ByteArray(bufferInfo.size) outputBuffer.get(rawYUV) outputBuffer.position(0) -// 포맷별 변환 val nv21Data = when (colorFormat) { COLOR_FormatYUV420SemiPlanar -> nv12ToNv21(rawYUV, videoWidth, videoHeight) COLOR_FormatYUV420Planar -> i420ToNv21(rawYUV, videoWidth, videoHeight) - else -> rawYUV // 이미 NV21 등 + else -> rawYUV } -// NV21 데이터 → Bitmap - val bitmap = convertYUVToBitmap(nv21Data, videoWidth, videoHeight) -// outputBuffer.get(yuvData, 0, minOf(bufferInfo.size, expectedSize)) -// getYUVDataWithStride(outputBuffer, videoWidth, videoHeight, stride, height) -// val bitmap = convertYUVToBitmap(yuvData, videoWidth, videoHeight) + val bmp = convertYUVToBitmap(nv21Data, videoWidth, videoHeight) + synchronized(lock) { + bitmap?.recycle() + bitmap = bmp + } - // 6. 왕복 이동 애니메이션 offset 계산 -// Blog.LOGE("maxOffset >>> $maxOffset , offset >>> $offset , direction >> $direction , keyFrameRate >>> $keyFrameRate") - if (maxOffset > 0f) { + if (moveXAxis || moveYAxis) { offset += direction * speed - if (offset < 0f) { offset = 0f; direction = 1 } - if (offset > maxOffset) { offset = maxOffset; direction = -1 } - } - - // 7. Canvas에 그리고 unlock - val canvas = holder.lockCanvas() - if (canvas != null) { - val drawX = if (moveXAxis) renderStartX - offset else renderStartX - val drawY = if (!moveXAxis) renderStartY - offset else renderStartY - val dstRect = RectF( - drawX, drawY, - drawX + renderWidth, drawY + renderHeight - ) - canvas.drawColor(Color.BLACK) - canvas.drawBitmap(bitmap, null, dstRect, null) - // 오버레이 예시 - val paint = Paint().apply { - color = Color.WHITE - textSize = 40f - setShadowLayer(4f, 2f, 2f, Color.DKGRAY) + if (offset < 0f) { + offset = 0f + direction = 1 + } + if (moveXAxis && offset > maxOffset) { + offset = maxOffset + direction = -1 + } else if (moveYAxis && offset > maxOffset) { + offset = maxOffset + direction = -1 + } + + noMoveStartTime = 0L + } else { + val now = System.currentTimeMillis() + if (noMoveStartTime == 0L) noMoveStartTime = now + else if (now - noMoveStartTime >= holdDurationMs) { + noMoveStartTime = 0L + moveCycleCompleted = true } - canvas.drawText("Live Wallpaper Overlay", 40f, 80f, paint) - holder.unlockCanvasAndPost(canvas) } - outputBuffer.clear() } codec?.releaseOutputBuffer(outputBufferIndex, false) } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { - // 필요하다면 포맷 변경 처리 + // 필요시 처리 } - sleep(1000L / keyFrameRate) // 약 30fps + + } } catch (e: Exception) { e.printStackTrace() - } finally { - try { - codec?.stop(); codec?.release() - extractor?.release() - rs?.destroy() - } catch (e: Exception) { } } } + private fun setupDecoder() { + for (i in 0 until extractor.trackCount) { + val format = extractor.getTrackFormat(i) + val mime = format.getString(MediaFormat.KEY_MIME) ?: "" + if (format.containsKey(MediaFormat.KEY_FRAME_RATE)) { + keyFrameRate = format.getInteger(MediaFormat.KEY_FRAME_RATE) + } else { + keyFrameRate = 60 + } + if (mime.startsWith("video/")) { + videoTrackIndex = i + extractor.selectTrack(i) + videoWidth = format.getInteger(MediaFormat.KEY_WIDTH) + videoHeight = format.getInteger(MediaFormat.KEY_HEIGHT) + break + } + } + if (videoTrackIndex < 0) return + + codec?.stop() + codec?.release() + codec = MediaCodec.createDecoderByType(extractor.getTrackFormat(videoTrackIndex).getString(MediaFormat.KEY_MIME)!!).apply { + configure(extractor.getTrackFormat(videoTrackIndex), null, null, 0) + start() + } + + rs?.destroy() + rs = RenderScript.create(context) + yuvToRgb?.destroy() + yuvToRgb = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs)) + + setupScaling(videoWidth, videoHeight) + resetOffset() + } + + private fun setupScaling(mediaWidth: Int, mediaHeight: Int) { + val isLandscape = mediaWidth > mediaHeight + + var scale = if (isLandscape) { + screenHeight.toFloat() / mediaHeight + } else { + screenWidth.toFloat() / mediaWidth + } + + var targetWidth = mediaWidth * scale + var targetHeight = mediaHeight * scale + + if (targetWidth < screenWidth) { + val scaleX = screenWidth.toFloat() / mediaWidth + if (scaleX > scale) { + scale = scaleX + targetWidth = mediaWidth * scale + targetHeight = mediaHeight * scale + } + } + if (targetHeight < screenHeight) { + val scaleY = screenHeight.toFloat() / mediaHeight + if (scaleY > scale) { + scale = scaleY + targetWidth = mediaWidth * scale + targetHeight = mediaHeight * scale + } + } + + renderWidth = targetWidth + renderHeight = targetHeight + + renderStartX = 0f + renderStartY = 0f + + val diffWidth = renderWidth - screenWidth + val diffHeight = renderHeight - screenHeight + + moveXAxis = diffWidth > 1f + moveYAxis = diffHeight > 1f + + maxOffset = if (moveXAxis) diffWidth else if (moveYAxis) diffHeight else 0f + } + private fun convertYUVToBitmap(yuvByteArray: ByteArray, width: Int, height: Int): Bitmap { - // YUV420の場合, 바이트 수는 width * height * 3 / 2 여야 함 val minSize = width * height * 3 / 2 val realData = if (yuvByteArray.size >= minSize) yuvByteArray.copyOf(minSize) else yuvByteArray @@ -291,33 +537,12 @@ class RenderThread( return bitmap } - private fun getYUVDataWithStride( - buffer: ByteBuffer, - width: Int, - height: Int, - stride: Int, - sliceHeight: Int - ): ByteArray { - val yuvData = ByteArray(width * height * 3 / 2) - // Y plane 복사 - for (y in 0 until height) { - buffer.position(y * stride) - buffer.get(yuvData, y * width, width) - } - // UV plane... (NV21 등 포맷 따라 별도 구현 필요) - // 이 영역은 포맷에 따라 다름, 기본은 Y만 참고! - return yuvData - } - - // NV12 to NV21 변환 fun nv12ToNv21(nv12: ByteArray, width: Int, height: Int): ByteArray { val frameSize = width * height val nv21 = ByteArray(frameSize * 3 / 2) - // Y는 그대로 복사 System.arraycopy(nv12, 0, nv21, 0, frameSize) - // UV를 VU로 뒤집어서 복사 var i = 0 while (i < frameSize / 2 - 1) { nv21[frameSize + i] = nv12[frameSize + i + 1] @@ -327,54 +552,135 @@ class RenderThread( return nv21 } - // I420(YUV420Planar) to NV21 변환 fun i420ToNv21(i420: ByteArray, width: Int, height: Int): ByteArray { val frameSize = width * height val qFrameSize = frameSize / 4 val nv21 = ByteArray(frameSize * 3 / 2) - // Y 복사 System.arraycopy(i420, 0, nv21, 0, frameSize) - // VU interleave val u = frameSize val v = frameSize + qFrameSize for (i in 0 until qFrameSize) { - nv21[frameSize + i * 2] = i420[v + i] // V - nv21[frameSize + i * 2 + 1] = i420[u + i] // U + nv21[frameSize + i * 2] = i420[v + i] + nv21[frameSize + i * 2 + 1] = i420[u + i] } return nv21 } - fun i420ToNv21WithStride(src: ByteBuffer, width: Int, height: Int, stride: Int, sliceHeight: Int): ByteArray { - val frameSize = width * height - val qFrameSize = frameSize / 4 - val nv21 = ByteArray(frameSize * 3 / 2) +} - // 1. Y plane 복사 - for (y in 0 until height) { - src.position(y * stride) - src.get(nv21, y * width, width) +class RenderLooper( + private val context: Context, + private val holder: SurfaceHolder, + private var screenWidth: Int, + private var screenHeight: Int, + handlerThread: HandlerThread +) { + private val handler = Handler(handlerThread.looper) + @Volatile + private var running = false + + private var renderer: Renderer? = null + private var mediaFiles: Array? = null + + private val frameDelayMs = 1000L / 33L // 약 60fps + + private val renderRunnable = object : Runnable { + override fun run() { + if (!running) return + val canvas = holder.lockCanvas() + val currentRenderer = renderer + if (canvas != null && currentRenderer != null) { + synchronized(currentRenderer) { + currentRenderer.render(canvas) + } + holder.unlockCanvasAndPost(canvas) + } + renderer?.update() + if (renderer?.isCycleCompleted() == true) { + loadNextMedia() + handler.postDelayed(this, frameDelayMs) + } + handler.postDelayed(this, frameDelayMs) } + } - // 2. U/V plane 복사, 각각 stride/2씩 적용 - val uOffsetBuf = sliceHeight * stride - val vOffsetBuf = uOffsetBuf + (sliceHeight / 2) * (stride / 2) + fun start() { + running = true + initMediaFiles() + loadNextMedia() + handler.post(renderRunnable) + } - for (y in 0 until height/2) { - src.position(uOffsetBuf + y * (stride / 2)) - src.get(nv21, frameSize + y * width, width/2) // 임시: U만 + fun stop() { + running = false + handler.removeCallbacks(renderRunnable) + } - src.position(vOffsetBuf + y * (stride / 2)) - for (x in 0 until width/2) { - // NV21 순서: V, U - val v = src.get() - val u = nv21[frameSize + y * width + x] // U - nv21[frameSize + y * width + x*2] = v - nv21[frameSize + y * width + x*2 + 1] = u + private fun initMediaFiles() { + val youtubeDLDir = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + "wallPapers" + ) + if (youtubeDLDir.exists() && youtubeDLDir.isDirectory && youtubeDLDir.listFiles().size > 2) { + val supportedImageExtensions = listOf("jpg", "jpeg", "png", "bmp", "webp") + val supportedVideoExtensions = listOf("mp4", "mkv", "avi") + mediaFiles = youtubeDLDir.listFiles { file -> + val ext = file.extension.lowercase() + ext in supportedImageExtensions || ext in supportedVideoExtensions } } - - return nv21 + Blog.LOGE("mediaFiles >>> ${mediaFiles?.size}") } -} \ No newline at end of file + + fun loadNextMedia() { + renderer?.release() + if (mediaFiles == null || mediaFiles!!.isEmpty()) { + prepareRawVideo() + return + } + + mediaFiles?.random()?.let { + loadMedia(it) + } + + } + + private fun loadMedia(file: File) { + val ext = file.extension.lowercase() + renderer?.resetOffset() + + if (ext in listOf("jpg", "jpeg", "png", "bmp", "webp")) { + val bitmap = BitmapFactory.decodeFile(file.absolutePath) + renderer = ImageRenderer(context, bitmap, screenWidth, screenHeight) + } else { + try { + val extractor = MediaExtractor() + extractor.setDataSource(file.absolutePath) + renderer = VideoRenderer(context, extractor, screenWidth, screenHeight) + } catch (e: Exception) { + e.printStackTrace() + prepareRawVideo() + } + } + } + + fun updateScreenSize(width: Int, height: Int) { + screenWidth = width + screenHeight = height + renderer?.updateScreenSize(width, height) + } + + private fun prepareRawVideo() { + try { + val extractor = MediaExtractor() + val afd = context.resources.openRawResourceFd(R.raw.sample) + extractor.setDataSource(afd.fileDescriptor, afd.startOffset, afd.length) + afd.close() + renderer = VideoRenderer(context, extractor, screenWidth, screenHeight) + } catch (e: Exception) { + e.printStackTrace() + } + } +} diff --git a/app/src/main/res/layout/booktoki.xml b/app/src/main/res/layout/booktoki.xml index 4058ab6f..79096670 100644 --- a/app/src/main/res/layout/booktoki.xml +++ b/app/src/main/res/layout/booktoki.xml @@ -89,14 +89,14 @@