diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 6b8d1546..59ae1013 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -54,7 +54,7 @@ target_link_libraries(native_renderer avutil swscale swresample - + jnigraphics # 안드로이드 기본 라이브러리들 ${log-lib} ${android-lib} diff --git a/app/src/main/cpp/Renderer.cpp b/app/src/main/cpp/Renderer.cpp index 2aefd6ae..264d5fed 100644 --- a/app/src/main/cpp/Renderer.cpp +++ b/app/src/main/cpp/Renderer.cpp @@ -237,6 +237,26 @@ void Renderer::handleTransitionState(ANativeWindow_Buffer& buffer, int surfaceWi } } +void Renderer::updateWeatherBitmap(void* pixels, int width, int height) { + std::lock_guard lock(weatherMutex_); + weatherWidth_ = width; + weatherHeight_ = height; + + size_t size = static_cast(width * height * 4); + weatherPixels_.assign(static_cast(pixels), static_cast(pixels) + size); + +} + +void Renderer::clearWeatherBitmap() { + std::lock_guard lock(weatherMutex_); + weatherPixels_.clear(); +} + +void Renderer::setImageRenderFrame(int frame) { + std::lock_guard lock(weatherMutex_); + imageFrame_ = frame; +} + // ==================================================================== // 메인 렌더링 루프 (교통정리 담당) // ==================================================================== @@ -252,7 +272,7 @@ void Renderer::renderFrame(ANativeWindow* window) { currentFrameDelay_ = std::chrono::milliseconds(static_cast(1000.0 / currentMedia_.getFps())); LOGI("Media type: Video. Frame delay set to %lldms for %.2f FPS", currentFrameDelay_.count(), currentMedia_.getFps()); } else { - currentFrameDelay_ = std::chrono::milliseconds(70); // 이미지일 경우 30fps + currentFrameDelay_ = std::chrono::milliseconds(imageFrame_); // 이미지일 경우 30fps LOGI("Media type: Image. Frame delay set to 33ms (~30 FPS)"); } currentState_ = RenderState::ANIMATING; @@ -288,6 +308,65 @@ void Renderer::renderFrame(ANativeWindow* window) { break; } + { + std::lock_guard lock(weatherMutex_); + if (!weatherPixels_.empty()) { + // 1. 위치 업데이트 (픽셀 단위 이동) + weatherX_ += weatherVelX_; + weatherY_ += weatherVelY_; + + // 2. 화면 경계 검사 및 튕기기 (Bounce) + // 가로 경계 + if (weatherX_ < 0) { + weatherX_ = 0; + weatherVelX_ *= -1.0f; + } else if (weatherX_ + weatherWidth_ > surfaceWidth) { + weatherX_ = (float)surfaceWidth - weatherWidth_; + weatherVelX_ *= -1.0f; + } + + // 세로 경계 + if (weatherY_ < 0) { + weatherY_ = 0; + weatherVelY_ *= -1.0f; + } else if (weatherY_ + weatherHeight_ > surfaceHeight) { + weatherY_ = (float)surfaceHeight - weatherHeight_; + weatherVelY_ *= -1.0f; + } + + // 3. 그리기 (기존의 블렌딩 로직 사용) + uint32_t* dstPixels = (uint32_t*)buffer.bits; + int dstStride = buffer.stride; + + for (int y = 0; y < weatherHeight_; ++y) { + int targetY = static_cast(weatherY_) + y; + if (targetY < 0 || targetY >= surfaceHeight) continue; + + for (int x = 0; x < weatherWidth_; ++x) { + int targetX = static_cast(weatherX_) + x; + if (targetX < 0 || targetX >= surfaceWidth) continue; + + const uint8_t* src = &weatherPixels_[(y * weatherWidth_ + x) * 4]; + uint8_t alpha = src[3]; + if (alpha == 0) continue; + + uint32_t* dst = &dstPixels[targetY * dstStride + targetX]; + + // 단순 합성을 위해 하드코딩된 ARGB/RGBA 순서 주의 + uint8_t dr = (*dst >> 16) & 0xFF; + uint8_t dg = (*dst >> 8) & 0xFF; + uint8_t db = (*dst) & 0xFF; + + uint8_t r = (src[0] * alpha + dr * (255 - alpha)) / 255; + uint8_t g = (src[1] * alpha + dg * (255 - alpha)) / 255; + uint8_t b = (src[2] * alpha + db * (255 - alpha)) / 255; + + *dst = (0xFF << 24) | (r << 16) | (g << 8) | b; + } + } + } + } + ANativeWindow_unlockAndPost(window); } diff --git a/app/src/main/cpp/Renderer.h b/app/src/main/cpp/Renderer.h index 9c4a328d..f61965ff 100644 --- a/app/src/main/cpp/Renderer.h +++ b/app/src/main/cpp/Renderer.h @@ -39,7 +39,9 @@ public: void setFadeDuration(int durationMs); void setPageTurnDelay(int delayMs); void setTransitionMode(int mode); - + void setImageRenderFrame(int frame); + void updateWeatherBitmap(void* pixels, int width, int height); + void clearWeatherBitmap(); void drawMedia(ANativeWindow_Buffer& buffer, MediaAsset& media, float alpha, float finalOffsetX, float finalOffsetY, float finalScale); void renderVideoFrame(MediaAsset& media, ANativeWindow_Buffer& buffer, float scale, float offsetX, float offsetY, float alpha); void renderImageFrame(const MediaAsset& media, ANativeWindow_Buffer& buffer, float scale, float offsetX, float offsetY, float alpha); @@ -84,4 +86,20 @@ private: std::mt19937 randomEngine_; void determineActiveAnimationMode(); + + + std::vector weatherPixels_; + int weatherWidth_ = 0; + int weatherHeight_ = 0; + int imageFrame_ = 33; + // 위치 변수 + float weatherX_ = 100.0f; + float weatherY_ = 100.0f; + + // 속도 변수 (방향 및 속도 조절) + float weatherVelX_ = 2.5f; + float weatherVelY_ = 2.0f; + + std::mutex weatherMutex_; + }; \ No newline at end of file diff --git a/app/src/main/cpp/native_renderer.cpp b/app/src/main/cpp/native_renderer.cpp index ed9e0e40..f25cc7b3 100644 --- a/app/src/main/cpp/native_renderer.cpp +++ b/app/src/main/cpp/native_renderer.cpp @@ -5,7 +5,7 @@ #include #include "Renderer.h" #include "Preloader.h" - +#include #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__) @@ -113,6 +113,27 @@ Java_bums_lunatic_launcher_wall_NativeRenderer_nativeRender(JNIEnv* env, jobject renderer->renderFrame(window); ANativeWindow_release(window); } +extern "C" +JNIEXPORT void JNICALL +Java_bums_lunatic_launcher_wall_NativeRenderer_nativeSetWeatherBitmap(JNIEnv* env, jobject, jlong nativeHandle, jobject bitmap) { + Renderer* renderer = reinterpret_cast(nativeHandle); + if (!renderer) return; + + // 비트맵이 null로 넘어오면 데이터를 지움 + if (bitmap == nullptr) { + renderer->clearWeatherBitmap(); + return; + } + + AndroidBitmapInfo info; + void* pixels; + if (AndroidBitmap_getInfo(env, bitmap, &info) < 0) return; + if (AndroidBitmap_lockPixels(env, bitmap, &pixels) < 0) return; + + renderer->updateWeatherBitmap(pixels, info.width, info.height); + + AndroidBitmap_unlockPixels(env, bitmap); +} JNIEXPORT void JNICALL Java_bums_lunatic_launcher_wall_NativeRenderer_nativeStartNextPreload(JNIEnv* env, jobject, jlong nativeHandle, jint fd) { @@ -124,6 +145,18 @@ Java_bums_lunatic_launcher_wall_NativeRenderer_nativeStartNextPreload(JNIEnv* en } } + +JNIEXPORT void JNICALL +Java_bums_lunatic_launcher_wall_NativeRenderer_nativeSetImageRenderFrame(JNIEnv* env, jobject, jlong nativeHandle, jint frame) { + Renderer* renderer = toNative(nativeHandle); + if (renderer) { + // Preloader가 Renderer의 일부가 되었다고 가정하고 호출 (추후 Renderer 수정 필요) + // 여기서는 간단하게 setNextMedia를 호출하는 것으로 변경합니다. + renderer->setImageRenderFrame(frame); + } +} + + JNIEXPORT void JNICALL Java_bums_lunatic_launcher_wall_NativeRenderer_nativeSetAnimationSpeed(JNIEnv* env, jobject, jlong nativeHandle, jfloat speed) { Renderer* renderer = toNative(nativeHandle); 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 d808ef15..6c7a343c 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt @@ -1,19 +1,27 @@ package bums.lunatic.launcher.wall import android.app.WallpaperManager -import android.content.ContentUris +import android.content.Intent +import android.content.IntentFilter +import android.graphics.Bitmap import android.graphics.BitmapFactory -import android.os.Environment +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Typeface +import android.os.BatteryManager import android.os.Handler import android.os.HandlerThread import android.os.ParcelFileDescriptor -import android.provider.MediaStore import android.service.wallpaper.WallpaperService import android.util.Log import android.view.SurfaceHolder -import androidx.work.ListenableWorker import bums.lunatic.launcher.utils.Blog +import bums.lunatic.launcher.workers.LocationUpdateService import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale class MyWallpaperService : WallpaperService() { @@ -25,6 +33,18 @@ class MyWallpaperService : WallpaperService() { Log.d(TAG, "onCreateEngine: New engine instance created.") return NativeRenderEngine() } + private fun getBatteryStatus(): Pair { + val intent = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) + val level = intent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1 + val scale = intent?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1 + val status = intent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1 + + val batteryPct = (level / scale.toFloat() * 100).toInt() + val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || + status == BatteryManager.BATTERY_STATUS_FULL + + return Pair(batteryPct, isCharging) + } inner class NativeRenderEngine : Engine() { private lateinit var handlerThread: HandlerThread @@ -136,6 +156,16 @@ class MyWallpaperService : WallpaperService() { initializeRenderer() } requestSyncRenderState() + + if (visible) { + // 화면이 켜지면 즉시 업데이트하고 타이머 시작 + handler.removeCallbacks(updateWeatherRunnable) + handler.post(updateWeatherRunnable) + } else { + // 화면이 꺼지면 타이머 중지 + handler.removeCallbacks(updateWeatherRunnable) + } + } // --- ⬇️ 상태 동기화를 요청하는 함수 추가 ⬇️ --- @@ -194,12 +224,18 @@ class MyWallpaperService : WallpaperService() { nativeRenderer?.setAnimationMode(NativeRenderer.ANIMATION_MODE_PAN) nativeRenderer?.setTransitionMode(NativeRenderer.TRANSITION_MODE_FADE) nativeRenderer?.setAnimationSpeed(1.0f) + nativeRenderer?.setImageRenderFrame(70) NativeRenderer.nativeSetNextMediaCallback(nextMediaCallback) if (mediaFiles.isEmpty()) { handler.post { loadMediaFiles() } } + + handler.removeCallbacks(updateWeatherRunnable) + // 즉시 첫 번째 비트맵 생성 및 전송 + handler.post(updateWeatherRunnable) + } val requiredSizeRatio = 0.5 @@ -243,6 +279,110 @@ class MyWallpaperService : WallpaperService() { } } + private fun drawBitmapFromText(weatherLines: List, isCharging: Boolean = false): Bitmap? { + val dateFull = SimpleDateFormat("yyyy.MM.dd", Locale.KOREAN).format(Date()) +// val timeFull = SimpleDateFormat(if (isCharging) "HH:mm:ss" else "HH:mm", Locale.KOREAN).format(Date()) + + val finalLines = mutableListOf() + if (weatherLines.isNotEmpty()) finalLines.add(weatherLines[0]) // Index 0: 날씨 + +// finalLines.add(timeFull) // Index 2: 시간 + if (weatherLines.size > 1) { + for (i in 1 until weatherLines.size - 1) finalLines.add(weatherLines[i]) + finalLines.add(weatherLines.last()) // 마지막: 주소 + } + finalLines.add(dateFull) // Index 1: 날짜 + + val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + textAlign = Paint.Align.CENTER + typeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL) + setShadowLayer(10f, 3f, 3f, Color.BLACK) + } + + val titleSize = 80f + val dateSize = 50f + val addressSize = 30f + val lineSpacingMultiplier = 0.2f // 폰트 크기의 20%를 순수 여백으로 사용 + + // --- 4. 크기 측정 및 라인별 높이 저장 --- + var maxWidth = 0f + var totalHeight = 40f // 상단 여백 + val lineHeights = mutableListOf() // 각 줄이 차지하는 총 높이 + val lineBaselines = mutableListOf() // 각 줄의 글자가 그려질 Baseline 위치 + + finalLines.forEachIndexed { index, line -> + paint.textSize = when (index) { + 0 -> titleSize + 2 -> addressSize + else -> dateSize + } + + val width = paint.measureText(line) + if (width > maxWidth) maxWidth = width + + val metrics = paint.fontMetrics + val fontHeight = metrics.descent - metrics.ascent // 실제 글자 높이 + val leading = paint.textSize * lineSpacingMultiplier // 동적 행간 + + val fullLineHeight = fontHeight + leading + + // 현재 줄의 Baseline 계산: 이전까지의 높이 + 글자의 ascent 절대값 + lineBaselines.add(totalHeight - metrics.ascent) + totalHeight += fullLineHeight + lineHeights.add(fullLineHeight) + } + totalHeight += 20f // 하단 여백 + + // --- 5. 비트맵 생성 및 그리기 --- + val bitmap = Bitmap.createBitmap((maxWidth + 100).toInt(), totalHeight.toInt(), Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + val centerX = bitmap.width / 2f + + finalLines.forEachIndexed { index, line -> + paint.textSize = when (index) { + 0 -> titleSize + 2 -> addressSize + else -> dateSize + } + // 미리 계산된 Baseline에 그리기만 하면 끝! + canvas.drawText(line, centerX, lineBaselines[index], paint) + } + + return bitmap + } + + private val updateWeatherRunnable = object : Runnable { + override fun run() { +// if (!isVisible) return +// +// val (batteryLevel, isCharging) = getBatteryStatus() +// +// // --- ⬇️ 제안하신 조건별 로직 적용 ⬇️ --- +// var nextDelay = 30000L // 기본 20초 +// +// if (isCharging) { +// nextDelay = 1000L // 충전 중이거나 70% 이상이면 10초 +// nativeRenderer?.setImageRenderFrame(45) +// } else if (batteryLevel >= 80) { +// nativeRenderer?.setImageRenderFrame(70) +// } else { +// handler.removeCallbacks(this) +// return +// } +// +// // 비트맵 생성 및 전달 +// val weather = LocationUpdateService.lastWeather +// val bitmap = drawBitmapFromText(weather,isCharging) +// bitmap?.let { +// nativeRenderer?.setWeatherBitmap(it) +// } +// +// Log.d(TAG, "Next update in ${nextDelay / 1000}s (Battery: $batteryLevel%, Charging: $isCharging)") +// handler.postDelayed(this, nextDelay) + } + } + private fun loadMediaFiles() { loadFiles() if (mediaFiles.isNotEmpty()) { @@ -253,6 +393,7 @@ class MyWallpaperService : WallpaperService() { } ?: run { Log.e(TAG, "Failed to get fd for initial media: ${initialFile.absolutePath}") } + } } val mediaDir = File(File(this@MyWallpaperService.getExternalFilesDir(null), "completed_torrents"), "Images") @@ -269,6 +410,20 @@ class MyWallpaperService : WallpaperService() { } ?: run { Log.e(TAG, "Callback: Failed to get fd for ${nextFile.absolutePath}") } + + val (batteryLevel, isCharging) = getBatteryStatus() + + if (batteryLevel >= 70) { + val bitmap = drawBitmapFromText(LocationUpdateService.lastWeather) + bitmap?.let { nativeRenderer?.setWeatherBitmap(it) } + Log.d(TAG, "Battery Low: Weather updated only on media change.") + } else { + nativeRenderer?.setImageRenderFrame(99) + nativeRenderer?.setWeatherBitmap(null) + } + if (isCharging || batteryLevel > 90) { +// handler.post(updateWeatherRunnable) + } } } diff --git a/app/src/main/kotlin/bums/lunatic/launcher/wall/NativeRenderer.kt b/app/src/main/kotlin/bums/lunatic/launcher/wall/NativeRenderer.kt index 996c8bea..7d2b1d78 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/wall/NativeRenderer.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/wall/NativeRenderer.kt @@ -1,5 +1,6 @@ package bums.lunatic.launcher.wall +import android.graphics.Bitmap import android.util.Log import android.view.Surface @@ -29,7 +30,11 @@ open class NativeRenderer { nativeHandle = 0L } } - + fun setWeatherBitmap(bitmap: Bitmap?) { + if (nativeHandle != 0L) { + nativeSetWeatherBitmap(nativeHandle, bitmap) + } + } fun render(surface: Surface) { if (nativeHandle != 0L) { nativeRender(nativeHandle, surface) @@ -96,7 +101,13 @@ open class NativeRenderer { } } - + fun setImageRenderFrame(frame:Int) { + if (nativeHandle != 0L) { + nativeSetImageRenderFrame(nativeHandle, frame) + } else { + "Kotlin Wrapper: Native handle is null." + } + } // --- Private JNI declarations --- private external fun nativeStartRenderLoop(nativeHandle: Long, surface: Surface) @@ -104,7 +115,9 @@ open class NativeRenderer { private external fun nativeInit(): Long private external fun nativeDestroy(nativeHandle: Long) private external fun nativeRender(nativeHandle: Long, surface: Surface) + private external fun nativeSetWeatherBitmap(nativeHandle: Long, bitmap: Bitmap?) private external fun nativeStartNextPreload(nativeHandle: Long, fd: Int) + private external fun nativeSetImageRenderFrame(nativeHandle: Long, frame: Int) private external fun nativeSetAnimationSpeed(nativeHandle: Long, speed: Float) private external fun nativeSetAnimationMode(nativeHandle: Long, mode: Int) private external fun nativeSetFadeDuration(nativeHandle: Long, duration: Int) diff --git a/app/src/main/kotlin/bums/lunatic/launcher/workers/LocationUpdateService.kt b/app/src/main/kotlin/bums/lunatic/launcher/workers/LocationUpdateService.kt index d1218c18..d2391130 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/workers/LocationUpdateService.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/workers/LocationUpdateService.kt @@ -41,6 +41,7 @@ class LocationUpdateService : Service(), LocationListener { companion object { var longitude: Double = 0.0 + var lastWeather = mutableListOf() var latitude: Double = 0.0 fun pushLocation(context: Context, lat :Double, long : Double) { try { @@ -91,7 +92,7 @@ class LocationUpdateService : Service(), LocationListener { // 응답 받아서 처리 val body: ResponseBody? = response.body if (body != null) { -// Blog.LOGE("Location >>> ${body.string()}") + Blog.LOGE("Location >>> ${body.string()}") } } else Blog.LOGE("telegram Error Occurred") } @@ -99,6 +100,41 @@ class LocationUpdateService : Service(), LocationListener { e.printStackTrace() } }, 5, TimeUnit.SECONDS) + + Blog.LOGE("addresses ${addresses.first()}") + Executors.newSingleThreadScheduledExecutor().schedule({ + try { + val url = "https://api.weatherapi.com/v1/current.json?key=${PrefString.weatherApiKey.get()}&q=${lat},${long}&aqi=no" + if (url.length > 10) { + val client = OkHttpClient.Builder() + .connectionPool(ConnectionPool(5, 60, TimeUnit.SECONDS)) + .build() + + // GET 요청 객체 생성 + val builder: Request.Builder = Request.Builder().url(url).get() + val request: Request = builder.build() + +// Blog.LOGE("telegram before request ") + // OkHttp 클라이언트로 GET 요청 객체 전송 + val response: Response = client.newCall(request).execute() + if (response.isSuccessful) { + // 응답 받아서 처리 + val body: ResponseBody? = response.body + if (body != null) { + var result = body.string() + var w = Gson().fromJson(result,CurrentWeather::class.java) + w.addr = addresses.first().getAddressLine(0).replace("대한민국", "") + lastWeather.clear() + lastWeather.addAll(w.getSummaryInfo()) + Blog.LOGE("Location >>> ${result}\n${lastWeather}") + } + } else Blog.LOGE("telegram Error Occurred") + } + } catch (e: java.lang.Exception) { + e.printStackTrace() + } + }, 5, TimeUnit.SECONDS) + // } } } @@ -174,9 +210,63 @@ class LocationUpdateService : Service(), LocationListener { } } +} +open class Condition { + var text: String? = null + var icon: String? = null + var code: Int = 0 +} +open class Current { + var last_updated_epoch: Int = 0 + var last_updated: String? = null + var temp_c: Double = 0.0 + var temp_f: Double = 0.0 + var is_day: Int = 0 + var condition: Condition? = null + var wind_mph: Double = 0.0 + var wind_kph: Double = 0.0 + var wind_degree: Int = 0 + var wind_dir: String? = null + var pressure_mb: Double = 0.0 + var pressure_in: Double = 0.0 + var precip_mm: Double = 0.0 + var precip_in: Double = 0.0 + var humidity: Int = 0 + var cloud: Int = 0 + var feelslike_c: Double = 0.0 + var feelslike_f: Double = 0.0 + var windchill_c: Double = 0.0 + var windchill_f: Double = 0.0 + var heatindex_c: Double = 0.0 + var heatindex_f: Double = 0.0 + var dewpoint_c: Double = 0.0 + var dewpoint_f: Double = 0.0 + var vis_km: Double = 0.0 + var vis_miles: Double = 0.0 + var uv: Double = 0.0 + var gust_mph: Double = 0.0 + var gust_kph: Double = 0.0 +} +open class Location { + var name: String? = null + var region: String? = null + var country: String? = null + var lat: Double = 0.0 + var lon: Double = 0.0 + var tz_id: String? = null + var localtime_epoch: Int = 0 + var localtime: String? = null +} +open class CurrentWeather { + var addr: String? = null + var current: Current? = null + + fun getSummaryInfo() = listOf("${this.current?.condition?.text}", + "${this.current?.temp_c}℃ ${this.current?.humidity}%" + ,"${addr}") } \ No newline at end of file