package com.rnwallpapers import android.app.WallpaperManager import android.content.Context import android.graphics.* import android.os.Build import com.facebook.react.bridge.* import com.facebook.react.modules.core.DeviceEventManagerModule import kotlinx.coroutines.* import java.io.* import java.net.HttpURLConnection import java.net.URL class RNWallpapersModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { override fun getName(): String = "RNWallpapers" private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val wallpaperManager by lazy { WallpaperManager.getInstance(reactContext) } // ─── setWallpaper ───────────────────────────────────────────────────────── @ReactMethod fun setWallpaper(source: String, options: ReadableMap, promise: Promise) { scope.launch { try { val target = options.getStringSafe("target") ?: "home" val type = options.getStringSafe("type") ?: "static" // Short-circuit for non-bitmap types when (type) { "live" -> { setLiveWallpaper(source, options, target, promise) emitWallpaperChanged(target, type, source) return@launch } "video" -> { setVideoWallpaper(source, options, target, promise) emitWallpaperChanged(target, type, source) return@launch } } val fit = options.getStringSafe("fit") ?: "fill" val blurRadius = options.getDoubleOrDefault("blurRadius", 0.0).toFloat() val brightness = options.getDoubleOrDefault("brightness", 0.0).toFloat() val opacity = options.getDoubleOrDefault("opacity", 1.0).toFloat() var bitmap = loadBitmapFromSource(source) // Crop options.getMapSafe("crop")?.let { bitmap = applyCrop(bitmap, it) } // Fit/scale bitmap = applyFit(bitmap, fit) // Color filter options.getMapSafe("colorFilter")?.let { cf -> val filterType = cf.getStringSafe("type") ?: "none" val intensity = cf.getDoubleOrDefault("intensity", 0.5).toFloat() if (filterType != "none") { bitmap = applyColorFilter(bitmap, filterType, intensity) } } // Blur if (blurRadius > 0f) bitmap = applyBlur(bitmap, blurRadius) // Brightness if (brightness != 0f) bitmap = applyBrightness(bitmap, brightness) // Opacity if (opacity < 1f) bitmap = applyOpacity(bitmap, opacity) // Parallax or static when (type) { "parallax" -> setParallaxWallpaper(source, bitmap, options, target, promise) else -> setStaticWallpaper(bitmap, target, promise) } saveLastSetInfo(target, type, source) emitWallpaperChanged(target, type, source) } catch (e: SecurityException) { promise.reject("PERMISSION_DENIED", "Permission denied: ${e.message}", e) } catch (e: FileNotFoundException) { promise.reject("FILE_NOT_FOUND", "File not found: ${e.message}", e) } catch (e: Exception) { promise.reject("SET_FAILED", "Failed: ${e.message}", e) } } } // ─── Static wallpaper ───────────────────────────────────────────────────── private fun setStaticWallpaper(bitmap: Bitmap, target: String, promise: Promise) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val flag = when (target) { "home" -> WallpaperManager.FLAG_SYSTEM "lock" -> WallpaperManager.FLAG_LOCK else -> WallpaperManager.FLAG_SYSTEM or WallpaperManager.FLAG_LOCK } wallpaperManager.setBitmap(bitmap, null, true, flag) } else { wallpaperManager.setBitmap(bitmap) } promise.resolve(Arguments.createMap().apply { putBoolean("success", true) putString("target", target) putString("type", "static") }) } // ─── Parallax wallpaper ─────────────────────────────────────────────────── private fun setParallaxWallpaper( source: String, bgBitmap: Bitmap, options: ReadableMap, target: String, promise: Promise ) { val parallaxOpts = options.getMapSafe("parallax") val intensity = parallaxOpts?.getDoubleOrDefault("intensity", 0.5)?.toFloat() ?: 0.5f val maxOffset = parallaxOpts?.getIntOrDefault("maxOffset", 40) ?: 40 val objectUri = parallaxOpts?.getStringSafe("objectUri") val sw = reactContext.resources.displayMetrics.widthPixels val sh = reactContext.resources.displayMetrics.heightPixels // Widen background for parallax scroll room val parallaxWidth = (sw * (1f + intensity * 0.6f)).toInt() val bgScaled = Bitmap.createScaledBitmap(bgBitmap, parallaxWidth, sh, true) // Composite foreground object if provided val finalBitmap = if (!objectUri.isNullOrBlank()) { try { var objBitmap = loadBitmapFromSource(objectUri) // Scale object to 60% screen width, maintain aspect ratio val objW = (sw * 0.6f).toInt() val objH = (objBitmap.height.toFloat() / objBitmap.width * objW).toInt() objBitmap = Bitmap.createScaledBitmap(objBitmap, objW, objH, true) // Composite onto background val composite = bgScaled.copy(Bitmap.Config.ARGB_8888, true) val canvas = Canvas(composite) val matrix = Matrix() val paint = Paint(Paint.ANTI_ALIAS_FLAG) // Position: center horizontally, 25% from top val objX = (parallaxWidth - objW) / 2f val objY = sh * 0.25f // Slight skew for 3D realism matrix.setSkew(0.03f, 0.01f) matrix.postTranslate(objX, objY) canvas.drawBitmap(objBitmap, matrix, paint) composite } catch (e: Exception) { // Object load failed — use background only bgScaled } } else { bgScaled } // Persist parallax config for gyroscope service reactContext.getSharedPreferences("RNWallpapers", Context.MODE_PRIVATE).edit().apply { putFloat("parallax_intensity", intensity) putInt("parallax_max_offset", maxOffset) putBoolean("parallax_gyro", true) putString("parallax_target", target) putBoolean("parallax_active", true) putString("parallax_source", source) putString("parallax_object", objectUri ?: "") apply() } // Set the composite bitmap as wallpaper setStaticWallpaper(finalBitmap, target, promise) } // ─── Live (GIF) wallpaper ───────────────────────────────────────────────── private fun setLiveWallpaper(source: String, options: ReadableMap, target: String, promise: Promise) { scope.launch { try { // Download GIF to cache val bytes = downloadBytes(source) val localPath = saveToCache("live_wallpaper.gif", bytes) val liveOpts = options.getMapSafe("live") reactContext.getSharedPreferences("RNWallpapers", Context.MODE_PRIVATE).edit().apply { putString("live_source", localPath) putString("live_target", target) putFloat("live_speed", liveOpts?.getDoubleOrDefault("speed", 1.0)?.toFloat() ?: 1f) putInt("live_loop_count", liveOpts?.getIntOrDefault("loopCount", 0) ?: 0) apply() } promise.resolve(Arguments.createMap().apply { putBoolean("success", true) putString("target", target) putString("type", "live") putString("localPath", localPath) }) } catch (e: Exception) { promise.reject("SET_FAILED", "Live wallpaper failed: ${e.message}", e) } } } // ─── Video wallpaper ────────────────────────────────────────────────────── private fun setVideoWallpaper(source: String, options: ReadableMap, target: String, promise: Promise) { try { // Safe null-checked reading of every video option val videoOpts = options.getMapSafe("video") val loop = videoOpts?.takeIf { it.hasKey("loop") && !it.isNull("loop") }?.getBoolean("loop") ?: true val muted = videoOpts?.takeIf { it.hasKey("muted") && !it.isNull("muted") }?.getBoolean("muted") ?: true val speed = videoOpts?.takeIf { it.hasKey("speed") && !it.isNull("speed") }?.getDouble("speed")?.toFloat() ?: 1f val startTime = videoOpts?.takeIf { it.hasKey("startTime") && !it.isNull("startTime") }?.getDouble("startTime")?.toFloat() ?: 0f val endTime = videoOpts?.takeIf { it.hasKey("endTime") && !it.isNull("endTime") }?.getDouble("endTime")?.toFloat() ?: -1f reactContext.getSharedPreferences("RNWallpapers", Context.MODE_PRIVATE).edit().apply { putString("video_source", source) putString("video_target", target) putBoolean("video_loop", loop) putBoolean("video_muted", muted) putFloat("video_speed", speed) putFloat("video_start", startTime) putFloat("video_end", endTime) apply() } promise.resolve(Arguments.createMap().apply { putBoolean("success", true) putString("target", target) putString("type", "video") }) } catch (e: Exception) { promise.reject("SET_FAILED", "Video wallpaper failed: ${e.message}", e) } } // ─── downloadWallpaper ──────────────────────────────────────────────────── @ReactMethod fun downloadWallpaper(url: String, options: ReadableMap, promise: Promise) { scope.launch { try { val filename = options.getStringSafe("filename") ?: "wallpaper_${System.currentTimeMillis()}" val directory = options.getStringSafe("saveDirectory") ?: "Pictures/Wallpapers" val bytes = downloadBytes(url) val ext = guessExtension(url) val fullName = "$filename.$ext" val dir = File(android.os.Environment.getExternalStorageDirectory(), directory) if (!dir.exists()) dir.mkdirs() val file = File(dir, fullName) FileOutputStream(file).use { it.write(bytes) } // Notify gallery reactContext.sendBroadcast( android.content.Intent(android.content.Intent.ACTION_MEDIA_SCANNER_SCAN_FILE).apply { data = android.net.Uri.fromFile(file) } ) promise.resolve(Arguments.createMap().apply { putBoolean("success", true) putString("filePath", file.absolutePath) putString("fileName", fullName) putInt("fileSize", bytes.size) }) } catch (e: Exception) { promise.reject("DOWNLOAD_FAILED", "Download failed: ${e.message}", e) } } } // ─── Auto Change ────────────────────────────────────────────────────────── @ReactMethod fun startAutoChange(options: ReadableMap, promise: Promise) { try { val sources = options.getArray("sources")!! val srcList = (0 until sources.size()).map { sources.getString(it)!! } val interval = options.getIntOrDefault("intervalMinutes", 1440) val target = options.getStringSafe("target") ?: "home" val shuffle = options.getBooleanOrDefault("shuffle", false) reactContext.getSharedPreferences("RNWallpapers", Context.MODE_PRIVATE).edit().apply { putStringSet("auto_sources", srcList.toSet()) putInt("auto_interval", interval) putString("auto_target", target) putBoolean("auto_shuffle", shuffle) putBoolean("auto_active", true) putInt("auto_index", 0) putLong("auto_started_at", System.currentTimeMillis()) apply() } AutoChangeWorker.schedule(reactContext, interval.toLong()) promise.resolve(Arguments.createMap().apply { putBoolean("success", true) }) } catch (e: Exception) { promise.reject("UNKNOWN", e.message, e) } } @ReactMethod fun stopAutoChange(promise: Promise) { AutoChangeWorker.cancel(reactContext) reactContext.getSharedPreferences("RNWallpapers", Context.MODE_PRIVATE) .edit().putBoolean("auto_active", false).apply() promise.resolve(null) } @ReactMethod fun getAutoChangeStatus(promise: Promise) { val prefs = reactContext.getSharedPreferences("RNWallpapers", Context.MODE_PRIVATE) promise.resolve(Arguments.createMap().apply { putBoolean("isActive", prefs.getBoolean("auto_active", false)) putInt("intervalMinutes", prefs.getInt("auto_interval", 1440)) putInt("currentIndex", prefs.getInt("auto_index", 0)) putInt("totalSources", prefs.getStringSet("auto_sources", emptySet())?.size ?: 0) putString("target", prefs.getString("auto_target", "home")) }) } // ─── clearWallpaper ─────────────────────────────────────────────────────── @ReactMethod fun clearWallpaper(target: String, promise: Promise) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val flag = when (target) { "home" -> WallpaperManager.FLAG_SYSTEM "lock" -> WallpaperManager.FLAG_LOCK else -> WallpaperManager.FLAG_SYSTEM or WallpaperManager.FLAG_LOCK } wallpaperManager.clear(flag) } else { wallpaperManager.clear() } promise.resolve(Arguments.createMap().apply { putBoolean("success", true) }) } catch (e: Exception) { promise.reject("SET_FAILED", e.message, e) } } // ─── getCurrentWallpaperInfo ────────────────────────────────────────────── @ReactMethod fun getCurrentWallpaperInfo(target: String, promise: Promise) { val prefs = reactContext.getSharedPreferences("RNWallpapers", Context.MODE_PRIVATE) promise.resolve(Arguments.createMap().apply { putBoolean("isSetByApp", prefs.contains("last_set_target")) putString("target", target) putString("type", prefs.getString("last_set_type", "static")) putString("localPath", prefs.getString("last_set_path", null)) putDouble("setAt", prefs.getLong("last_set_at", 0).toDouble()) }) } // ─── getDeviceCapabilities ──────────────────────────────────────────────── @ReactMethod fun getDeviceCapabilities(promise: Promise) { val dm = reactContext.resources.displayMetrics promise.resolve(Arguments.createMap().apply { putBoolean("supportsLiveWallpaper", wallpaperManager.isWallpaperSupported) putBoolean("supportsVideoWallpaper", Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) putBoolean("supportsParallax", true) putBoolean("supportsLockScreen", Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) putInt("maxResolutionWidth", dm.widthPixels * 2) putInt("maxResolutionHeight", dm.heightPixels * 2) putInt("screenWidth", dm.widthPixels) putInt("screenHeight", dm.heightPixels) }) } // ─── Permissions ────────────────────────────────────────────────────────── @ReactMethod fun checkPermission(promise: Promise) { val granted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) reactContext.checkSelfPermission(android.Manifest.permission.SET_WALLPAPER) == android.content.pm.PackageManager.PERMISSION_GRANTED else true promise.resolve(granted) } @ReactMethod fun requestPermission(promise: Promise) { promise.resolve(true) } // ─── Image processing ───────────────────────────────────────────────────── private fun loadBitmapFromSource(source: String): Bitmap { return if (source.startsWith("http://") || source.startsWith("https://")) { val bytes = downloadBytes(source) BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: throw IOException("Could not decode image from URL") } else { val path = source.removePrefix("file://") BitmapFactory.decodeFile(path) ?: throw FileNotFoundException("Could not decode file: $path") } } private fun downloadBytes(url: String): ByteArray { val conn = URL(url).openConnection() as HttpURLConnection conn.connectTimeout = 15_000 conn.readTimeout = 30_000 conn.doInput = true conn.connect() if (conn.responseCode != HttpURLConnection.HTTP_OK) throw IOException("HTTP ${conn.responseCode}: ${conn.responseMessage}") return conn.inputStream.use { it.readBytes() } } private fun applyCrop(bitmap: Bitmap, crop: ReadableMap): Bitmap { val x = (crop.getDoubleOrDefault("x", 0.0) * bitmap.width).toInt().coerceAtLeast(0) val y = (crop.getDoubleOrDefault("y", 0.0) * bitmap.height).toInt().coerceAtLeast(0) val w = (crop.getDoubleOrDefault("width", 1.0) * bitmap.width).toInt().coerceAtMost(bitmap.width - x) val h = (crop.getDoubleOrDefault("height", 1.0) * bitmap.height).toInt().coerceAtMost(bitmap.height - y) return Bitmap.createBitmap(bitmap, x, y, w, h) } private fun applyFit(bitmap: Bitmap, fit: String): Bitmap { val sw = reactContext.resources.displayMetrics.widthPixels val sh = reactContext.resources.displayMetrics.heightPixels return when (fit) { "fill" -> { val s = maxOf(sw.toFloat() / bitmap.width, sh.toFloat() / bitmap.height); Bitmap.createScaledBitmap(bitmap, (bitmap.width * s).toInt(), (bitmap.height * s).toInt(), true) } "fit" -> { val s = minOf(sw.toFloat() / bitmap.width, sh.toFloat() / bitmap.height); Bitmap.createScaledBitmap(bitmap, (bitmap.width * s).toInt(), (bitmap.height * s).toInt(), true) } "stretch" -> Bitmap.createScaledBitmap(bitmap, sw, sh, true) else -> bitmap } } private fun applyColorFilter(bitmap: Bitmap, type: String, intensity: Float): Bitmap { val result = bitmap.copy(Bitmap.Config.ARGB_8888, true) val canvas = Canvas(result) val paint = Paint() val alpha = (intensity * 180).toInt() if (type.equals("grayscale", ignoreCase = true)) { val matrix = ColorMatrix().apply { setSaturation(1f - intensity) } paint.colorFilter = ColorMatrixColorFilter(matrix) canvas.drawBitmap(bitmap, 0f, 0f, paint) return result } val color = when (type.lowercase()) { "warm" -> Color.argb(alpha, 255, 150, 50) "cool" -> Color.argb(alpha, 50, 100, 255) "dark" -> Color.argb(alpha, 0, 0, 0) "light" -> Color.argb(alpha, 255, 255, 255) "sepia" -> Color.argb(alpha, 112, 66, 20) "vintage" -> Color.argb(alpha, 100, 80, 60) else -> return bitmap } paint.color = color canvas.drawRect(0f, 0f, result.width.toFloat(), result.height.toFloat(), paint) return result } private fun applyBlur(bitmap: Bitmap, radius: Float): Bitmap { val rs = android.renderscript.RenderScript.create(reactContext) val input = android.renderscript.Allocation.createFromBitmap(rs, bitmap) val output = android.renderscript.Allocation.createTyped(rs, input.type) val script = android.renderscript.ScriptIntrinsicBlur.create(rs, android.renderscript.Element.U8_4(rs)) script.setRadius(radius.coerceIn(1f, 25f)) script.setInput(input) script.forEach(output) val out = bitmap.copy(bitmap.config, true) output.copyTo(out) rs.destroy() return out } private fun applyBrightness(bitmap: Bitmap, brightness: Float): Bitmap { val v = (brightness * 255).toInt().toFloat() val matrix = ColorMatrix(floatArrayOf( 1f,0f,0f,0f,v, 0f,1f,0f,0f,v, 0f,0f,1f,0f,v, 0f,0f,0f,1f,0f )) val result = bitmap.copy(Bitmap.Config.ARGB_8888, true) Canvas(result).drawBitmap(bitmap, 0f, 0f, Paint().apply { colorFilter = ColorMatrixColorFilter(matrix) }) return result } private fun applyOpacity(bitmap: Bitmap, opacity: Float): Bitmap { val result = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888) Canvas(result).drawBitmap(bitmap, 0f, 0f, Paint().apply { alpha = (opacity * 255).toInt() }) return result } private fun saveToCache(name: String, bytes: ByteArray): String { val file = File(reactContext.cacheDir, name) FileOutputStream(file).use { it.write(bytes) } return file.absolutePath } private fun saveLastSetInfo(target: String, type: String, source: String) { reactContext.getSharedPreferences("RNWallpapers", Context.MODE_PRIVATE).edit().apply { putString("last_set_target", target) putString("last_set_type", type) putString("last_set_path", source) putLong("last_set_at", System.currentTimeMillis()) apply() } } private fun guessExtension(url: String): String = when { url.contains(".gif") -> "gif" url.contains(".png") -> "png" url.contains(".webp") -> "webp" url.contains(".mp4") -> "mp4" else -> "jpg" } private fun emitWallpaperChanged(target: String, type: String, source: String) { reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit("onWallpaperChanged", Arguments.createMap().apply { putString("target", target) putString("type", type) putString("source", source) putDouble("timestamp", System.currentTimeMillis().toDouble()) }) } override fun onCatalystInstanceDestroy() { scope.cancel(); super.onCatalystInstanceDestroy() } } // ─── ReadableMap extension helpers ──────────────────────────────────────────── private fun ReadableMap.getDoubleOrDefault(key: String, default: Double) = if (hasKey(key) && !isNull(key)) getDouble(key) else default private fun ReadableMap.getIntOrDefault(key: String, default: Int) = if (hasKey(key) && !isNull(key)) getInt(key) else default private fun ReadableMap.getBooleanOrDefault(key: String, default: Boolean) = if (hasKey(key) && !isNull(key)) getBoolean(key) else default private fun ReadableMap.getStringSafe(key: String) = if (hasKey(key) && !isNull(key)) getString(key) else null private fun ReadableMap.getMapSafe(key: String) = if (hasKey(key) && !isNull(key)) getMap(key) else null