fix: include native module sources in git tracking

Fix .gitignore to only exclude root android/ and ios/
(Expo prebuild output) while keeping modules/ native code.
Add Android WallpaperService Kotlin sources, GLSL shaders,
manifest, and Gradle config.
This commit is contained in:
Mathis Pruvot
2026-05-28 11:50:19 +00:00
parent 84809378d1
commit 9b90e6a4a1
7 changed files with 744 additions and 2 deletions

View File

@@ -0,0 +1,27 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.lively.wallpaper"
compileSdk = 35
defaultConfig {
minSdk = 24
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
implementation(project(":expo-modules-core"))
implementation("org.jetbrains.kotlin:kotlin-stdlib")
}

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.SET_WALLPAPER" />
<uses-feature
android:name="android.software.live_wallpaper"
android:required="false" />
<application>
<service
android:name=".LiveWallpaperService"
android:label="Lively Wallpaper"
android:permission="android.permission.BIND_WALLPAPER"
android:exported="true">
<intent-filter>
<action android:name="android.service.wallpaper.WallpaperService" />
</intent-filter>
<meta-data
android:name="android.service.wallpaper"
android:resource="@xml/wallpaper" />
</service>
</application>
</manifest>

View File

@@ -0,0 +1,189 @@
package com.lively.wallpaper
import android.content.Context
import android.content.SharedPreferences
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.opengl.GLSurfaceView
import android.os.Handler
import android.os.Looper
import android.service.wallpaper.WallpaperService
import android.view.SurfaceHolder
import org.json.JSONObject
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10
/**
* Live Wallpaper service that renders animated shaders via OpenGL ES 2.0.
*
* Reads wallpaper config from SharedPreferences (written by WallpaperAndroidModule).
* Config format: { shaderId, imagePath, depthMapPath?, uniforms: { intensity, speed, direction } }
*/
class LiveWallpaperService : WallpaperService() {
override fun onCreateEngine(): Engine = LiveEngine()
inner class LiveEngine : WallpaperService.Engine(), SensorEventListener {
private var renderer: ShaderRenderer? = null
private var glSurface: WallpaperGLSurfaceView? = null
// Config
private var shaderId = "ken-burns"
private var imagePath = ""
private var depthMapPath: String? = null
private var intensity = 0.5f
private var speed = 0.3f
private var direction = 0f
private var targetFps = 24
// State
private var startTime = 0L
private var gyroX = 0f
private var gyroY = 0f
private var isVisible = false
// Sensors
private var sensorManager: SensorManager? = null
private var gyroscope: Sensor? = null
// Render loop
private val handler = Handler(Looper.getMainLooper())
private val renderRunnable = object : Runnable {
override fun run() {
if (isVisible) {
glSurface?.requestRender()
handler.postDelayed(this, (1000L / targetFps))
}
}
}
override fun onCreate(surfaceHolder: SurfaceHolder) {
super.onCreate(surfaceHolder)
setTouchEventsEnabled(false)
loadConfig()
sensorManager = getSystemService(Context.SENSOR_SERVICE) as? SensorManager
gyroscope = sensorManager?.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
}
override fun onSurfaceCreated(holder: SurfaceHolder) {
super.onSurfaceCreated(holder)
glSurface = WallpaperGLSurfaceView(this@LiveWallpaperService).also { view ->
view.setEGLContextClientVersion(2)
view.setRenderer(WallpaperRenderer())
view.renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY
}
startTime = System.currentTimeMillis()
}
override fun onVisibilityChanged(visible: Boolean) {
super.onVisibilityChanged(visible)
isVisible = visible
if (visible) {
handler.post(renderRunnable)
gyroscope?.let {
sensorManager?.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME)
}
} else {
handler.removeCallbacks(renderRunnable)
sensorManager?.unregisterListener(this)
}
}
override fun onSurfaceDestroyed(holder: SurfaceHolder) {
super.onSurfaceDestroyed(holder)
isVisible = false
handler.removeCallbacks(renderRunnable)
sensorManager?.unregisterListener(this)
renderer?.release()
renderer = null
}
override fun onSensorChanged(event: SensorEvent?) {
event?.let {
if (it.sensor.type == Sensor.TYPE_GYROSCOPE) {
// Accumulate with damping
gyroX += it.values[0] * 0.05f
gyroY += it.values[1] * 0.05f
// Damping toward zero
gyroX *= 0.95f
gyroY *= 0.95f
}
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
private fun loadConfig() {
val prefs = getSharedPreferences("lively_wallpaper", Context.MODE_PRIVATE)
val configStr = prefs.getString("config", null) ?: return
try {
val json = JSONObject(configStr)
shaderId = json.optString("animation", "ken-burns")
imagePath = json.optString("sourceUri", "")
depthMapPath = json.optString("depthMapUri", null)
targetFps = json.optInt("fps", 24)
val uniforms = json.optJSONObject("uniforms")
if (uniforms != null) {
intensity = uniforms.optDouble("intensity", 0.5).toFloat()
speed = uniforms.optDouble("speed", 0.3).toFloat()
direction = uniforms.optDouble("direction", 0.0).toFloat()
}
} catch (e: Exception) {
// Fallback to defaults on parse error
}
}
inner class WallpaperRenderer : GLSurfaceView.Renderer {
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
renderer = ShaderRenderer().also { r ->
r.initialize(shaderId)
if (imagePath.isNotEmpty()) {
r.loadTexture(imagePath)
}
depthMapPath?.let { path ->
if (path.isNotEmpty()) r.loadDepthTexture(path)
}
}
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
android.opengl.GLES20.glViewport(0, 0, width, height)
}
override fun onDrawFrame(gl: GL10?) {
val elapsed = (System.currentTimeMillis() - startTime) / 1000f
val holder = surfaceHolder
val width = holder.surfaceFrame.width()
val height = holder.surfaceFrame.height()
renderer?.draw(
width = width,
height = height,
time = elapsed,
intensity = intensity,
speed = speed,
direction = direction,
gyroX = gyroX,
gyroY = gyroY
)
}
}
/**
* Custom GLSurfaceView that uses the WallpaperService's SurfaceHolder.
*/
inner class WallpaperGLSurfaceView(context: Context) : GLSurfaceView(context) {
override fun getHolder(): SurfaceHolder = this@LiveEngine.surfaceHolder
}
}
}

View File

@@ -0,0 +1,438 @@
package com.lively.wallpaper
import android.opengl.GLES20
import android.opengl.GLUtils
import android.graphics.BitmapFactory
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer
/**
* OpenGL ES 2.0 renderer that applies fragment shaders to a texture.
* Each animation type maps to a GLSL fragment shader equivalent to the SkSL version.
*/
class ShaderRenderer {
private var programId = 0
private var textureId = 0
private var depthTextureId = 0
private var vertexBuffer: FloatBuffer? = null
// Uniform locations
private var uResolution = -1
private var uTime = -1
private var uIntensity = -1
private var uSpeed = -1
private var uDirection = -1
private var uImage = -1
private var uDepthMap = -1
private var uGyroX = -1
private var uGyroY = -1
private var uParticleType = -1
private val vertexShaderCode = """
attribute vec4 aPosition;
attribute vec2 aTexCoord;
varying vec2 vTexCoord;
void main() {
gl_Position = aPosition;
vTexCoord = aTexCoord;
}
""".trimIndent()
// Fullscreen quad vertices (position + texcoord)
private val quadVertices = floatArrayOf(
// X, Y, U, V
-1.0f, -1.0f, 0.0f, 1.0f,
1.0f, -1.0f, 1.0f, 1.0f,
-1.0f, 1.0f, 0.0f, 0.0f,
1.0f, 1.0f, 1.0f, 0.0f,
)
fun initialize(shaderId: String) {
val bb = ByteBuffer.allocateDirect(quadVertices.size * 4)
bb.order(ByteOrder.nativeOrder())
vertexBuffer = bb.asFloatBuffer().apply {
put(quadVertices)
position(0)
}
val fragmentShaderCode = getFragmentShader(shaderId)
programId = createProgram(vertexShaderCode, fragmentShaderCode)
GLES20.glUseProgram(programId)
uResolution = GLES20.glGetUniformLocation(programId, "uResolution")
uTime = GLES20.glGetUniformLocation(programId, "uTime")
uIntensity = GLES20.glGetUniformLocation(programId, "uIntensity")
uSpeed = GLES20.glGetUniformLocation(programId, "uSpeed")
uDirection = GLES20.glGetUniformLocation(programId, "uDirection")
uImage = GLES20.glGetUniformLocation(programId, "uImage")
uDepthMap = GLES20.glGetUniformLocation(programId, "uDepthMap")
uGyroX = GLES20.glGetUniformLocation(programId, "uGyroX")
uGyroY = GLES20.glGetUniformLocation(programId, "uGyroY")
uParticleType = GLES20.glGetUniformLocation(programId, "uParticleType")
}
fun loadTexture(imagePath: String): Boolean {
val bitmap = BitmapFactory.decodeFile(imagePath) ?: return false
val textures = IntArray(1)
GLES20.glGenTextures(1, textures, 0)
textureId = textures[0]
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0)
bitmap.recycle()
return true
}
fun loadDepthTexture(depthMapPath: String): Boolean {
val bitmap = BitmapFactory.decodeFile(depthMapPath) ?: return false
val textures = IntArray(1)
GLES20.glGenTextures(1, textures, 0)
depthTextureId = textures[0]
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, depthTextureId)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0)
bitmap.recycle()
return true
}
fun draw(
width: Int,
height: Int,
time: Float,
intensity: Float,
speed: Float,
direction: Float,
gyroX: Float = 0f,
gyroY: Float = 0f,
particleType: Float = 0f
) {
GLES20.glUseProgram(programId)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
// Bind image texture to unit 0
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)
if (uImage >= 0) GLES20.glUniform1i(uImage, 0)
// Bind depth map to unit 1 (if available)
if (depthTextureId != 0) {
GLES20.glActiveTexture(GLES20.GL_TEXTURE1)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, depthTextureId)
if (uDepthMap >= 0) GLES20.glUniform1i(uDepthMap, 1)
}
// Set uniforms
if (uResolution >= 0) GLES20.glUniform2f(uResolution, width.toFloat(), height.toFloat())
if (uTime >= 0) GLES20.glUniform1f(uTime, time)
if (uIntensity >= 0) GLES20.glUniform1f(uIntensity, intensity)
if (uSpeed >= 0) GLES20.glUniform1f(uSpeed, speed)
if (uDirection >= 0) GLES20.glUniform1f(uDirection, direction)
if (uGyroX >= 0) GLES20.glUniform1f(uGyroX, gyroX)
if (uGyroY >= 0) GLES20.glUniform1f(uGyroY, gyroY)
if (uParticleType >= 0) GLES20.glUniform1f(uParticleType, particleType)
// Draw fullscreen quad
val posHandle = GLES20.glGetAttribLocation(programId, "aPosition")
val texHandle = GLES20.glGetAttribLocation(programId, "aTexCoord")
vertexBuffer?.let { buf ->
buf.position(0)
GLES20.glEnableVertexAttribArray(posHandle)
GLES20.glVertexAttribPointer(posHandle, 2, GLES20.GL_FLOAT, false, 16, buf)
buf.position(2)
GLES20.glEnableVertexAttribArray(texHandle)
GLES20.glVertexAttribPointer(texHandle, 2, GLES20.GL_FLOAT, false, 16, buf)
}
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
GLES20.glDisableVertexAttribArray(posHandle)
GLES20.glDisableVertexAttribArray(texHandle)
}
fun release() {
if (programId != 0) {
GLES20.glDeleteProgram(programId)
programId = 0
}
if (textureId != 0) {
GLES20.glDeleteTextures(1, intArrayOf(textureId), 0)
textureId = 0
}
if (depthTextureId != 0) {
GLES20.glDeleteTextures(1, intArrayOf(depthTextureId), 0)
depthTextureId = 0
}
}
private fun createProgram(vertexSource: String, fragmentSource: String): Int {
val vertexShader = compileShader(GLES20.GL_VERTEX_SHADER, vertexSource)
val fragmentShader = compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource)
val program = GLES20.glCreateProgram()
GLES20.glAttachShader(program, vertexShader)
GLES20.glAttachShader(program, fragmentShader)
GLES20.glLinkProgram(program)
val linkStatus = IntArray(1)
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0)
if (linkStatus[0] == 0) {
val log = GLES20.glGetProgramInfoLog(program)
GLES20.glDeleteProgram(program)
throw RuntimeException("Program link failed: $log")
}
GLES20.glDeleteShader(vertexShader)
GLES20.glDeleteShader(fragmentShader)
return program
}
private fun compileShader(type: Int, source: String): Int {
val shader = GLES20.glCreateShader(type)
GLES20.glShaderSource(shader, source)
GLES20.glCompileShader(shader)
val compileStatus = IntArray(1)
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0)
if (compileStatus[0] == 0) {
val log = GLES20.glGetShaderInfoLog(shader)
GLES20.glDeleteShader(shader)
throw RuntimeException("Shader compile failed: $log")
}
return shader
}
companion object {
/**
* Maps animation IDs to GLSL fragment shaders.
* These are GLSL ES 2.0 equivalents of the SkSL shaders in src/shaders/.
*/
fun getFragmentShader(shaderId: String): String = when (shaderId) {
"ken-burns" -> """
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uImage;
uniform vec2 uResolution;
uniform float uTime;
uniform float uIntensity;
uniform float uSpeed;
uniform float uDirection;
void main() {
float t = uTime * uSpeed * 0.1;
float zoom = 1.0 + uIntensity * 0.15 * (0.5 + 0.5 * sin(t));
float panX = cos(uDirection) * uIntensity * 0.05 * sin(t * 0.7);
float panY = sin(uDirection) * uIntensity * 0.05 * cos(t * 0.7);
vec2 center = vec2(0.5);
vec2 uv = (vTexCoord - center) / zoom + center;
uv += vec2(panX, panY);
uv = clamp(uv, 0.0, 1.0);
gl_FragColor = texture2D(uImage, uv);
}
""".trimIndent()
"color-shift" -> """
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uImage;
uniform float uTime;
uniform float uIntensity;
uniform float uSpeed;
vec3 rgb2hsl(vec3 c) {
float mx = max(c.r, max(c.g, c.b));
float mn = min(c.r, min(c.g, c.b));
float l = (mx + mn) * 0.5;
float s = 0.0, h = 0.0;
if (mx != mn) {
float d = mx - mn;
s = l > 0.5 ? d / (2.0 - mx - mn) : d / (mx + mn);
if (mx == c.r) h = (c.g - c.b) / d + (c.g < c.b ? 6.0 : 0.0);
else if (mx == c.g) h = (c.b - c.r) / d + 2.0;
else h = (c.r - c.g) / d + 4.0;
h /= 6.0;
}
return vec3(h, s, l);
}
float hue2rgb(float p, float q, float t) {
float tt = t;
if (tt < 0.0) tt += 1.0;
if (tt > 1.0) tt -= 1.0;
if (tt < 1.0/6.0) return p + (q - p) * 6.0 * tt;
if (tt < 1.0/2.0) return q;
if (tt < 2.0/3.0) return p + (q - p) * (2.0/3.0 - tt) * 6.0;
return p;
}
vec3 hsl2rgb(vec3 hsl) {
if (hsl.y == 0.0) return vec3(hsl.z);
float q = hsl.z < 0.5 ? hsl.z * (1.0 + hsl.y) : hsl.z + hsl.y - hsl.z * hsl.y;
float p = 2.0 * hsl.z - q;
return vec3(hue2rgb(p,q,hsl.x+1.0/3.0), hue2rgb(p,q,hsl.x), hue2rgb(p,q,hsl.x-1.0/3.0));
}
void main() {
vec4 color = texture2D(uImage, vTexCoord);
vec3 hsl = rgb2hsl(color.rgb);
float t = uTime * uSpeed * 0.15;
hsl.x = fract(hsl.x + uIntensity * 0.1 * sin(t));
hsl.y = clamp(hsl.y + uIntensity * 0.15 * sin(t * 1.3), 0.0, 1.0);
hsl.z = clamp(hsl.z + uIntensity * 0.03 * sin(t * 0.8), 0.0, 1.0);
gl_FragColor = vec4(hsl2rgb(hsl), color.a);
}
""".trimIndent()
"particles" -> """
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uImage;
uniform vec2 uResolution;
uniform float uTime;
uniform float uIntensity;
uniform float uSpeed;
uniform float uParticleType;
float hash(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
float snow(vec2 uv, float layer) {
float t = uTime * uSpeed * 0.4 + layer * 12.345;
vec2 grid = floor(uv * (8.0 + layer * 6.0));
vec2 f = fract(uv * (8.0 + layer * 6.0));
float rnd = hash(grid);
vec2 center = vec2(0.5 + 0.3*sin(t+rnd*6.28), fract(rnd+t*(0.1+rnd*0.08)));
float d = length(f - center);
float size = 0.04 + rnd * 0.06;
return smoothstep(size, size*0.3, d) * (0.4+0.6*rnd);
}
float bokeh(vec2 uv, float layer) {
float t = uTime * uSpeed * 0.2 + layer * 5.67;
vec2 grid = floor(uv * (3.0 + layer * 2.0));
vec2 f = fract(uv * (3.0 + layer * 2.0));
float rnd = hash(grid);
vec2 center = vec2(0.5+0.2*sin(t*0.5+rnd*6.28), 0.5+0.2*cos(t*0.3+rnd*6.28));
float d = length(f - center);
float size = 0.1 + rnd * 0.15;
float ring = smoothstep(size,size-0.02,d) - smoothstep(size-0.03,size-0.06,d);
float fill = smoothstep(size,size*0.2,d) * 0.3;
return (ring+fill) * (0.3+0.7*rnd) * step(0.3, rnd);
}
void main() {
vec4 color = texture2D(uImage, vTexCoord);
float particles = 0.0;
for (float i = 0.0; i < 3.0; i += 1.0) {
if (uParticleType < 0.5) particles += snow(vTexCoord, i);
else particles += bokeh(vTexCoord, i);
}
particles *= uIntensity;
gl_FragColor = vec4(clamp(color.rgb + vec3(particles), 0.0, 1.0), color.a);
}
""".trimIndent()
"vignette-pulse" -> """
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uImage;
uniform float uTime;
uniform float uIntensity;
uniform float uSpeed;
void main() {
vec4 color = texture2D(uImage, vTexCoord);
vec2 center = vTexCoord - 0.5;
float dist = length(center);
float t = uTime * uSpeed * 0.3;
float strength = uIntensity * (0.6 + 0.4 * sin(t));
float vignette = 1.0 - smoothstep(0.2, 0.85, dist * (1.0 + strength));
vec3 tinted = color.rgb * vignette;
float tint = (1.0 - vignette) * 0.15 * uIntensity;
tinted.b += tint * 0.3;
gl_FragColor = vec4(clamp(tinted, 0.0, 1.0), color.a);
}
""".trimIndent()
"glitch" -> """
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uImage;
uniform vec2 uResolution;
uniform float uTime;
uniform float uIntensity;
uniform float uSpeed;
float hash(float n) { return fract(sin(n) * 43758.5453); }
void main() {
float t = uTime * uSpeed * 0.5;
float trigger = step(0.92, sin(t*2.0)*sin(t*7.3));
float amount = uIntensity * trigger;
float shift = uIntensity * 0.003 + amount * 0.015;
vec2 uvR = vec2(vTexCoord.x + shift, vTexCoord.y);
vec2 uvB = vec2(vTexCoord.x - shift, vTexCoord.y);
uvR = clamp(uvR, 0.0, 1.0);
uvB = clamp(uvB, 0.0, 1.0);
float r = texture2D(uImage, uvR).r;
float g = texture2D(uImage, vTexCoord).g;
float b = texture2D(uImage, uvB).b;
gl_FragColor = vec4(r, g, b, 1.0);
}
""".trimIndent()
"depth-parallax" -> """
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uImage;
uniform sampler2D uDepthMap;
uniform vec2 uResolution;
uniform float uTime;
uniform float uIntensity;
uniform float uSpeed;
uniform float uGyroX;
uniform float uGyroY;
void main() {
float depth = texture2D(uDepthMap, vTexCoord).r;
float t = uTime * uSpeed * 0.2;
vec2 offset = vec2(
uGyroX * 0.8 + sin(t) * 0.3,
uGyroY * 0.8 + cos(t * 0.7) * 0.3
);
vec2 displacement = offset * depth * uIntensity * 0.04;
vec2 uv = clamp(vTexCoord + displacement, 0.0, 1.0);
gl_FragColor = texture2D(uImage, uv);
}
""".trimIndent()
else -> """
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uImage;
void main() { gl_FragColor = texture2D(uImage, vTexCoord); }
""".trimIndent()
}
}
}

View File

@@ -0,0 +1,59 @@
package com.lively.wallpaper
import android.app.WallpaperManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import org.json.JSONObject
class WallpaperAndroidModule : Module() {
override fun definition() = ModuleDefinition {
Name("WallpaperAndroid")
/**
* Save wallpaper configuration to SharedPreferences.
* The LiveWallpaperService reads this on start.
*/
AsyncFunction("saveConfig") { configJson: String ->
val context = appContext.reactContext ?: throw Exception("Context unavailable")
val prefs = context.getSharedPreferences("lively_wallpaper", Context.MODE_PRIVATE)
prefs.edit().putString("config", configJson).apply()
}
/**
* Launch the system live wallpaper picker with our service pre-selected.
*/
AsyncFunction("setLiveWallpaper") {
val context = appContext.reactContext ?: throw Exception("Context unavailable")
val intent = Intent(WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER).apply {
putExtra(
WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT,
ComponentName(context.packageName, LiveWallpaperService::class.java.name)
)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
}
/**
* Check if live wallpapers are supported on this device.
*/
Function("isLiveWallpaperSupported") {
val context = appContext.reactContext ?: return@Function false
val pm = context.packageManager
pm.hasSystemFeature("android.software.live_wallpaper")
}
/**
* Get the currently active wallpaper config (if it's ours).
*/
Function("getCurrentConfig") {
val context = appContext.reactContext ?: return@Function null
val prefs = context.getSharedPreferences("lively_wallpaper", Context.MODE_PRIVATE)
prefs.getString("config", null)
}
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<wallpaper
xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/app_name"
android:thumbnail="@mipmap/ic_launcher" />