...
This commit is contained in:
parent
1e06d82015
commit
87fa624983
@ -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 {
|
||||
|
||||
@ -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">
|
||||
|
||||
|
||||
|
||||
|
||||
8
app/src/main/cpp/CMakeLists.txt
Normal file
8
app/src/main/cpp/CMakeLists.txt
Normal 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)
|
||||
82
app/src/main/cpp/native_renderer.cpp
Normal file
82
app/src/main/cpp/native_renderer.cpp
Normal 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");
|
||||
}
|
||||
}
|
||||
@ -21,6 +21,7 @@ class HourlyLogWriter(private val logDir: File) {
|
||||
|
||||
// 로그 기록 함수 (비동기)
|
||||
fun writeLog(data: String) {
|
||||
if (true)return
|
||||
Blog.LOGE("writeLog >>> ${data}")
|
||||
ioScope.launch {
|
||||
try {
|
||||
|
||||
@ -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" -> {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user