This commit is contained in:
lunaticbum 2026-03-31 18:15:28 +09:00
parent 5a8e2293ca
commit 92d228d2e0
7 changed files with 399 additions and 11 deletions

View File

@ -54,7 +54,7 @@ target_link_libraries(native_renderer
avutil
swscale
swresample
jnigraphics
#
${log-lib}
${android-lib}

View File

@ -237,6 +237,26 @@ void Renderer::handleTransitionState(ANativeWindow_Buffer& buffer, int surfaceWi
}
}
void Renderer::updateWeatherBitmap(void* pixels, int width, int height) {
std::lock_guard<std::mutex> lock(weatherMutex_);
weatherWidth_ = width;
weatherHeight_ = height;
size_t size = static_cast<size_t>(width * height * 4);
weatherPixels_.assign(static_cast<uint8_t*>(pixels), static_cast<uint8_t*>(pixels) + size);
}
void Renderer::clearWeatherBitmap() {
std::lock_guard<std::mutex> lock(weatherMutex_);
weatherPixels_.clear();
}
void Renderer::setImageRenderFrame(int frame) {
std::lock_guard<std::mutex> lock(weatherMutex_);
imageFrame_ = frame;
}
// ====================================================================
// 메인 렌더링 루프 (교통정리 담당)
// ====================================================================
@ -252,7 +272,7 @@ void Renderer::renderFrame(ANativeWindow* window) {
currentFrameDelay_ = std::chrono::milliseconds(static_cast<long long>(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<std::mutex> 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<int>(weatherY_) + y;
if (targetY < 0 || targetY >= surfaceHeight) continue;
for (int x = 0; x < weatherWidth_; ++x) {
int targetX = static_cast<int>(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);
}

View File

@ -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<uint8_t> 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_;
};

View File

@ -5,7 +5,7 @@
#include <thread>
#include "Renderer.h"
#include "Preloader.h"
#include <android/bitmap.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__)
@ -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<Renderer*>(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<Renderer>(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<Renderer>(nativeHandle);

View File

@ -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<Int, Boolean> {
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<String>, 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<String>()
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<Float>() // 각 줄이 차지하는 총 높이
val lineBaselines = mutableListOf<Float>() // 각 줄의 글자가 그려질 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)
}
}
}

View File

@ -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)

View File

@ -41,6 +41,7 @@ class LocationUpdateService : Service(), LocationListener {
companion object {
var longitude: Double = 0.0
var lastWeather = mutableListOf<String>()
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<CurrentWeather>(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<String>("${this.current?.condition?.text}",
"${this.current?.temp_c}${this.current?.humidity}%"
,"${addr}")
}