This commit is contained in:
lunaticbum 2025-08-26 13:32:53 +09:00
parent 1e06d82015
commit 87fa624983
9 changed files with 357 additions and 642 deletions

View File

@ -18,8 +18,15 @@ android {
versionCode = 38
versionName = "2.8.2"
multiDexEnabled = true
externalNativeBuild {
cmake {
cppFlags += "-std=c++11"
}
}
}
buildTypes {
getByName("debug") {
isMinifyEnabled = false
@ -27,19 +34,23 @@ android {
isDebuggable = true
// applicationIdSuffix = ".debug"
// versionNameSuffix = "-debug"
buildConfigField("Long","BuildDateTime", getDateTime().toString().plus("L"))
buildConfigField("Long","BuildDateTime", "${getDateTime()}L")
resValue ("string", "app_name", "Bums Launcher Debug")
}
getByName("release") {
isMinifyEnabled = true
isShrinkResources = true
buildConfigField("Long","BuildDateTime", getDateTime().toString().plus("L"))
buildConfigField("Long","BuildDateTime", "${getDateTime()}L")
proguardFiles (getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
resValue ("string", "app_name", "Bums Launcher")
}
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
}
}
signingConfigs {
getByName("debug") {
@ -80,6 +91,7 @@ android {
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {

View File

@ -79,6 +79,7 @@
android:stateNotNeeded="true"
android:enableOnBackInvokedCallback="true"
android:largeHeap="true"
android:debuggable="false"
android:extractNativeLibs="true"
android:networkSecurityConfig="@xml/network_security_config"
android:hardwareAccelerated="true"
@ -86,7 +87,7 @@
android:screenOrientation="nosensor"
android:windowSoftInputMode="adjustResize"
android:requestLegacyExternalStorage="true"
>
tools:ignore="HardcodedDebugMode">

View File

@ -0,0 +1,8 @@
cmake_minimum_required(VERSION 3.10.2)
project("native_renderer")
add_library(native-renderer SHARED native_renderer.cpp)
find_library(log-lib log)
target_link_libraries(native-renderer ${log-lib} android)

View File

@ -0,0 +1,82 @@
#include <jni.h>
#include <android/native_window_jni.h>
#include <android/native_window.h>
#include <android/log.h>
#define LOG_TAG "NativeRenderer"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
static ANativeWindow* window = nullptr;
extern "C" JNIEXPORT void JNICALL
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeInit(JNIEnv* env, jobject, jobject surface) {
if (window != nullptr) {
ANativeWindow_release(window);
window = nullptr;
}
window = ANativeWindow_fromSurface(env, surface);
LOGI("Native window initialized");
}
extern "C" JNIEXPORT void JNICALL
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeRender(
JNIEnv* env, jobject, jintArray pixels, jint width, jint height,
jfloat offsetX, jfloat offsetY, jfloat fadeAlpha) {
if (!window) return;
ANativeWindow_Buffer buffer;
if (ANativeWindow_lock(window, &buffer, nullptr) < 0) {
LOGE("Failed to lock native window");
return;
}
jint* srcPixels = env->GetIntArrayElements(pixels, nullptr);
if (!srcPixels) {
ANativeWindow_unlockAndPost(window);
return;
}
uint32_t* dstPixels = (uint32_t*)buffer.bits;
int stride = buffer.stride;
int offsetXInt = static_cast<int>(offsetX);
int offsetYInt = static_cast<int>(offsetY);
for (int y = 0; y < buffer.height; y++) {
int srcY = y - offsetYInt;
if (srcY < 0 || srcY >= height) continue;
for (int x = 0; x < buffer.width; x++) {
int srcX = x - offsetXInt;
if (srcX < 0 || srcX >= width) continue;
uint32_t pixel = srcPixels[srcY * width + srcX];
uint8_t alpha = static_cast<uint8_t>(fadeAlpha * 255);
uint8_t r = (pixel >> 16) & 0xFF;
uint8_t g = (pixel >> 8) & 0xFF;
uint8_t b = pixel & 0xFF;
r = (r * alpha) / 255;
g = (g * alpha) / 255;
b = (b * alpha) / 255;
dstPixels[y * stride + x] = (0xFF << 24) | (r << 16) | (g << 8) | b;
}
}
env->ReleaseIntArrayElements(pixels, srcPixels, JNI_ABORT);
ANativeWindow_unlockAndPost(window);
}
extern "C" JNIEXPORT void JNICALL
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeDestroy(JNIEnv*, jobject) {
if (window) {
ANativeWindow_release(window);
window = nullptr;
LOGI("Native window released");
}
}

View File

@ -21,6 +21,7 @@ class HourlyLogWriter(private val logDir: File) {
// 로그 기록 함수 (비동기)
fun writeLog(data: String) {
if (true)return
Blog.LOGE("writeLog >>> ${data}")
ioScope.launch {
try {

View File

@ -75,7 +75,7 @@ class NLService : NotificationListenerService() {
@SuppressLint("MissingPermission")
@RequiresApi(Build.VERSION_CODES.S)
override fun onNotificationPosted(sbn: StatusBarNotification) {
Blog.LOGE("onNotificationPosted ${sbn}")
// Blog.LOGE("onNotificationPosted ${sbn}")
if (sbn.packageName.contains("bums.lunatic.launcher") == false) {
val notification = sbn.notification
val extras = notification.extras
@ -99,7 +99,7 @@ class NLService : NotificationListenerService() {
stringBuffer.append(conversationTitle).append("\n")
stringBuffer.append(summaryText).append("\n")
stringBuffer.append(verificationText).append("\n")
Blog.LOGE("title >> ${title} text >> ${text} bigText >> ${bigText} extraInfo >> ${extraInfo} subText >> ${subText} conversationTitle >> ${conversationTitle} summaryText >> ${summaryText} verificationText >> ${verificationText}")
// Blog.LOGE("title >> ${title} text >> ${text} bigText >> ${bigText} extraInfo >> ${extraInfo} subText >> ${subText} conversationTitle >> ${conversationTitle} summaryText >> ${summaryText} verificationText >> ${verificationText}")
mHourlyLogWriter?.writeLog("${sbn.packageName}\n${stringBuffer.toString()}")
when (sbn.packageName) {
"com.kakao.taxi" -> {

View File

@ -0,0 +1,34 @@
package bums.lunatic.launcher.utils
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever
import java.io.File
object MediaBitmapUtil {
fun decodeImageFile(file: File): Bitmap? {
return try {
val options = BitmapFactory.Options().apply {
inPreferredConfig = Bitmap.Config.RGB_565
}
BitmapFactory.decodeFile(file.absolutePath, options)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
fun decodeVideoFrame(file: File, frameTimeUs: Long = 0L): Bitmap? {
return try {
val retriever = MediaMetadataRetriever()
retriever.setDataSource(file.absolutePath)
val bitmap = retriever.getFrameAtTime(frameTimeUs)
retriever.release()
bitmap
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}

View File

@ -1,686 +1,241 @@
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
import android.media.MediaCodec
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
import android.renderscript.ScriptIntrinsicYuvToRGB
import android.renderscript.Type
import android.service.wallpaper.WallpaperService
import android.util.DisplayMetrics
import android.view.SurfaceHolder
import android.view.WindowManager
import bums.lunatic.launcher.R
import bums.lunatic.launcher.utils.Blog
import java.io.File
import kotlin.random.Random
class MyWallpaperService : WallpaperService() {
override fun onCreateEngine(): Engine {
return VideoEngine()
}
override fun onCreateEngine(): Engine = NativeRenderEngine()
inner class VideoEngine : WallpaperService.Engine() {
inner class NativeRenderEngine : Engine() {
private lateinit var holder: SurfaceHolder
private lateinit var renderLooper: RenderLooper
private lateinit var handlerThread: HandlerThread
private lateinit var handler: Handler
private var running = false
private var screenWidth: Int = 0
private var screenHeight: Int = 0
private var nativeRenderer: NativeRenderer? = null
private var currentBitmap: Bitmap? = null
private var currentBitmapPixels: IntArray? = null
private var currentBitmapWidth = 0
private var currentBitmapHeight = 0
private var screenWidth = 0
private var screenHeight = 0
private var renderWidth = 0f
private var renderHeight = 0f
private var maxOffset = 0f
private var moveXAxis = false
private var moveYAxis = false
private var offsetX = 0f
private var offsetY = 0f
private var movingForward = true
private var fadeAlpha = 1f
private val fadeDuration = 3000L // 3초
private val imageDisplayDuration = 20_000L // 20초
private var currentMediaStartTime = 0L
private val frameDelayMs = 16L
private val renderRunnable = object : Runnable {
override fun run() {
if (!running) return
val now = System.currentTimeMillis()
val mediaElapsedMs = now - currentMediaStartTime
// 이동 애니메이션 계산
val travelDistance = maxOffset
val speed = if (travelDistance > 0f) (travelDistance * 2) / imageDisplayDuration.toFloat() else 0f
val deltaOffset = speed * frameDelayMs
if (moveXAxis) {
if (movingForward) {
offsetX += deltaOffset
if (offsetX >= maxOffset) offsetX = maxOffset.also { movingForward = false }
} else {
offsetX -= deltaOffset
if (offsetX <= 0f) offsetX = 0f.also { movingForward = true }
}
offsetY = 0f
} else if (moveYAxis) {
if (movingForward) {
offsetY += deltaOffset
if (offsetY >= maxOffset) offsetY = maxOffset.also { movingForward = false }
} else {
offsetY -= deltaOffset
if (offsetY <= 0f) offsetY = 0f.also { movingForward = true }
}
offsetX = 0f
} else {
offsetX = 0f
offsetY = 0f
}
// 페이드 인/아웃 계산
fadeAlpha = if (mediaElapsedMs >= imageDisplayDuration - fadeDuration) {
val elapsed = mediaElapsedMs - (imageDisplayDuration - fadeDuration)
1f - (elapsed.toFloat() / fadeDuration)
} else 1f
if (fadeAlpha < 0f) fadeAlpha = 0f
// 비트맵에서 픽셀 추출
currentBitmap?.let { bmp ->
if (currentBitmapPixels == null || currentBitmapWidth != bmp.width || currentBitmapHeight != bmp.height) {
currentBitmapWidth = bmp.width
currentBitmapHeight = bmp.height
currentBitmapPixels = IntArray(currentBitmapWidth * currentBitmapHeight)
}
bmp.getPixels(currentBitmapPixels!!, 0, currentBitmapWidth, 0, 0, currentBitmapWidth, currentBitmapHeight)
}
// 네이티브 렌더 호출
currentBitmapPixels?.let { pixels ->
nativeRenderer?.nativeRender(pixels, currentBitmapWidth, currentBitmapHeight, offsetX, offsetY, fadeAlpha)
}
// 20초 후 미디어 교체
if (mediaElapsedMs >= imageDisplayDuration) {
loadNextMedia()
}
handler.postDelayed(this, frameDelayMs)
}
}
override fun onCreate(surfaceHolder: SurfaceHolder) {
super.onCreate(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) {
val display = wm.currentWindowMetrics
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)
handlerThread = HandlerThread("RenderHandlerThread")
handlerThread.start()
renderLooper = RenderLooper(this@MyWallpaperService, holder, screenWidth, screenHeight, handlerThread)
renderLooper.start()
nativeRenderer = NativeRenderer()
nativeRenderer?.nativeInit(holder.surface)
handlerThread = HandlerThread("NativeRenderThread").apply { start() }
handler = Handler(handlerThread.looper)
running = true
val wm = getSystemService(WINDOW_SERVICE) as android.view.WindowManager
val metrics = android.util.DisplayMetrics()
wm.defaultDisplay.getMetrics(metrics)
screenWidth = metrics.widthPixels
screenHeight = metrics.heightPixels
loadMediaFiles()
handler.post(renderRunnable)
}
override fun onSurfaceDestroyed(holder: SurfaceHolder) {
super.onSurfaceDestroyed(holder)
renderLooper.stop()
running = false
handler.removeCallbacks(renderRunnable)
nativeRenderer?.nativeDestroy()
handlerThread.quitSafely()
super.onSurfaceDestroyed(holder)
}
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)
}
}
}
private var mediaFiles: List<File> = emptyList()
private var currentIndex = 0
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 var bitmap: Bitmap,
private var screenWidth: Int,
private var screenHeight: Int
) : Renderer {
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 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
}
private fun loadMediaFiles() {
val mediaDir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "wallpapers")
val extensions = listOf("jpg", "jpeg", "png", "bmp", "webp")
mediaFiles = if (mediaDir.exists() && mediaDir.isDirectory) {
mediaDir.listFiles()?.filter { extensions.contains(it.extension.lowercase()) }?.toList() ?: emptyList()
} else {
if (moveCycleCompleted) {
moveCycleCompleted = false
emptyList()
}
loadNextMedia()
}
private fun loadNextMedia() {
if (mediaFiles.isEmpty()) return
val file = mediaFiles[currentIndex]
currentIndex = (currentIndex + 1) % mediaFiles.size
loadBitmapFromFile(file)
currentMediaStartTime = System.currentTimeMillis()
offsetX = 0f
offsetY = 0f
movingForward = true
fadeAlpha = 1f
setupScaling(currentBitmapWidth, currentBitmapHeight)
}
private fun loadBitmapFromFile(file: File) {
val originalBitmap = BitmapFactory.decodeFile(file.absolutePath)
originalBitmap?.let {
currentBitmap?.recycle()
setupScaling(it.width, it.height)
val scaledWidth = renderWidth.toInt().coerceAtLeast(1)
val scaledHeight = renderHeight.toInt().coerceAtLeast(1)
val scaledBitmap = Bitmap.createScaledBitmap(it, scaledWidth, scaledHeight, true)
currentBitmap = scaledBitmap
currentBitmapWidth = scaledWidth
currentBitmapHeight = scaledHeight
currentBitmapPixels = IntArray(currentBitmapWidth * currentBitmapHeight)
scaledBitmap.getPixels(currentBitmapPixels!!, 0, currentBitmapWidth, 0, 0, currentBitmapWidth, currentBitmapHeight)
if (it != scaledBitmap) {
it.recycle()
}
noMoveStartTime = 0L
}
}
}
override fun isCycleCompleted(): Boolean = moveCycleCompleted
private fun setupScaling(mediaWidth: Int, mediaHeight: Int) {
if (screenWidth == 0 || screenHeight == 0) return
operator fun invoke() = run()
val isLandscape = mediaWidth > mediaHeight
var scale = if (isLandscape) screenHeight.toFloat() / mediaHeight else screenWidth.toFloat() / mediaWidth
override fun run() {
try {
val inputBuffersTimeoutUs = 10000L
var targetWidth = mediaWidth * scale
var targetHeight = mediaHeight * scale
while (running && !Thread.currentThread().isInterrupted) {
if (isEOS) {
continue
if (targetWidth < screenWidth) {
val scaleX = screenWidth.toFloat() / mediaWidth
if (scaleX > scale) {
scale = scaleX
targetWidth = mediaWidth * scale
targetHeight = mediaHeight * scale
}
}
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()
}
}
if (targetHeight < screenHeight) {
val scaleY = screenHeight.toFloat() / mediaHeight
if (scaleY > scale) {
scale = scaleY
targetWidth = mediaWidth * scale
targetHeight = mediaHeight * scale
}
val outputBufferIndex = codec?.dequeueOutputBuffer(bufferInfo, inputBuffersTimeoutUs) ?: -1
if (outputBufferIndex >= 0) {
val outputBuffer = codec?.getOutputBuffer(outputBufferIndex)
if (bufferInfo.size > 0 && outputBuffer != null) {
val format = codec?.outputFormat
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
}
val bmp = convertYUVToBitmap(nv21Data, videoWidth, videoHeight)
synchronized(lock) {
bitmap?.recycle()
bitmap = bmp
}
if (moveXAxis || moveYAxis) {
offset += direction * speed
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
}
}
}
codec?.releaseOutputBuffer(outputBufferIndex, false)
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 필요시 처리
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
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
renderWidth = targetWidth
renderHeight = targetHeight
codec?.stop()
codec?.release()
codec = MediaCodec.createDecoderByType(extractor.getTrackFormat(videoTrackIndex).getString(MediaFormat.KEY_MIME)!!).apply {
configure(extractor.getTrackFormat(videoTrackIndex), null, null, 0)
start()
}
val diffWidth = renderWidth - screenWidth
val diffHeight = renderHeight - screenHeight
rs?.destroy()
rs = RenderScript.create(context)
yuvToRgb?.destroy()
yuvToRgb = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs))
moveXAxis = diffWidth > 1f
moveYAxis = diffHeight > 1f
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 {
val minSize = width * height * 3 / 2
val realData = if (yuvByteArray.size >= minSize) yuvByteArray.copyOf(minSize) else yuvByteArray
val yuvType = Type.Builder(rs, Element.U8(rs)).setX(minSize)
val inAllocation = Allocation.createTyped(rs, yuvType.create(), Allocation.USAGE_SCRIPT)
val rgbaType = Type.Builder(rs, Element.RGBA_8888(rs)).setX(width).setY(height)
val outAllocation = Allocation.createTyped(rs, rgbaType.create(), Allocation.USAGE_SCRIPT)
inAllocation.copyFrom(realData)
yuvToRgb?.setInput(inAllocation)
yuvToRgb?.forEach(outAllocation)
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
outAllocation.copyTo(bitmap)
inAllocation.destroy()
outAllocation.destroy()
return bitmap
}
fun nv12ToNv21(nv12: ByteArray, width: Int, height: Int): ByteArray {
val frameSize = width * height
val nv21 = ByteArray(frameSize * 3 / 2)
System.arraycopy(nv12, 0, nv21, 0, frameSize)
var i = 0
while (i < frameSize / 2 - 1) {
nv21[frameSize + i] = nv12[frameSize + i + 1]
nv21[frameSize + i + 1] = nv12[frameSize + i]
i += 2
}
return nv21
}
fun i420ToNv21(i420: ByteArray, width: Int, height: Int): ByteArray {
val frameSize = width * height
val qFrameSize = frameSize / 4
val nv21 = ByteArray(frameSize * 3 / 2)
System.arraycopy(i420, 0, nv21, 0, frameSize)
val u = frameSize
val v = frameSize + qFrameSize
for (i in 0 until qFrameSize) {
nv21[frameSize + i * 2] = i420[v + i]
nv21[frameSize + i * 2 + 1] = i420[u + i]
}
return nv21
}
}
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<File>? = 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)
}
}
fun start() {
running = true
initMediaFiles()
loadNextMedia()
handler.post(renderRunnable)
}
fun stop() {
running = false
handler.removeCallbacks(renderRunnable)
}
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
}
}
Blog.LOGE("mediaFiles >>> ${mediaFiles?.size}")
}
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()
maxOffset = if (moveXAxis) diffWidth else if (moveYAxis) diffHeight else 0f
}
}
}

View File

@ -0,0 +1,22 @@
package bums.lunatic.launcher.wall
import android.view.Surface
class NativeRenderer {
companion object {
init {
System.loadLibrary("native-renderer")
}
}
external fun nativeInit(surface: Surface)
external fun nativeRender(
pixels: IntArray,
width: Int,
height: Int,
offsetX: Float,
offsetY: Float,
fadeAlpha: Float
)
external fun nativeDestroy()
}