Last active
August 10, 2023 18:36
-
-
Save iangilman/71650d46384a2d4ae6387f2d4087cc37 to your computer and use it in GitHub Desktop.
Android WallpaperService with WebView
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.content.BroadcastReceiver; | |
import android.content.Intent; | |
import android.content.IntentFilter; | |
import android.content.SharedPreferences; | |
import android.graphics.Canvas; | |
import android.graphics.Point; | |
import android.preference.PreferenceManager; | |
import android.service.wallpaper.WallpaperService; | |
import android.content.Context; | |
import android.support.v4.content.LocalBroadcastManager; | |
import android.util.Log; | |
import android.view.Display; | |
import android.view.SurfaceHolder; | |
import android.view.View; | |
import android.view.WindowManager; | |
import android.webkit.JavascriptInterface; | |
import android.webkit.WebResourceRequest; | |
import android.webkit.WebSettings; | |
import android.webkit.WebView; | |
import android.webkit.WebViewClient; | |
public class MyLWPService extends WallpaperService { | |
// We can have multiple engines running at once (since you might have one on your home screen | |
// and another in the settings panel, for instance), so for debugging it's useful to keep track | |
// of which one is which. We give each an id based on this nextEngineId. | |
static int nextEngineId = 1; | |
@Override | |
public void onCreate() { | |
super.onCreate(); | |
} | |
@Override | |
public Engine onCreateEngine() { | |
return new MyEngine(this); | |
} | |
// This JSInterface allows us to get messages from the WebView. | |
public class JSInterface { | |
private MyEngine myActivity; | |
public JSInterface (MyEngine activity) { | |
myActivity = activity; | |
} | |
@JavascriptInterface | |
public void drawFrame(){ | |
myActivity.incomingMessage(); | |
} | |
} | |
public class MyEngine extends Engine { | |
private Context myContext; | |
private WebView myWebView; | |
private SurfaceHolder myHolder; | |
private int myId; | |
private JSInterface myJSInterface; | |
private BroadcastReceiver myMessageReceiver; | |
public MyEngine(Context context) { | |
myId = nextEngineId; | |
nextEngineId++; | |
myContext = context; | |
myWebView = null; | |
myMessageReceiver = null; | |
log("Engine created."); | |
} | |
private void log(String message) { | |
Log.d("MyLWP " + myId, message); | |
} | |
private void logError(String message) { | |
Log.e("MyLWP " + myId, message); | |
} | |
public void incomingMessage() { | |
// The message from the WebView might not be on the right thread, so we use a broadcast | |
// to fix that. | |
Intent intent = new Intent("draw-frame"); | |
LocalBroadcastManager.getInstance(myContext).sendBroadcast(intent); | |
} | |
@Override | |
public void onCreate(SurfaceHolder surfaceHolder) { | |
log("On Create"); | |
super.onCreate(surfaceHolder); | |
} | |
@Override | |
public void onDestroy() { | |
log("On Destroy"); | |
super.onDestroy(); | |
} | |
@Override | |
public void onVisibilityChanged(boolean visible) { | |
log("On Visibility Changed " + String.valueOf(visible)); | |
super.onVisibilityChanged(visible); | |
if (myWebView == null) { | |
return; | |
} | |
// To save battery, when we're not visible we want the WebView to stop processing, | |
// so we use the loadUrl mechanism to call some JavaScript to tell it to pause. | |
if (visible) { | |
myWebView.loadUrl("javascript:resumeWallpaper()"); | |
} else { | |
myWebView.loadUrl("javascript:pauseWallpaper()"); | |
} | |
} | |
@Override | |
public void onSurfaceCreated(SurfaceHolder holder) { | |
log("On Surface Create"); | |
super.onSurfaceCreated(holder); | |
myHolder = holder; | |
// Create WebView | |
if (myWebView != null) { | |
myWebView.destroy(); | |
} | |
WebViewClient client = new WebViewClient() { | |
@Override | |
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { | |
return false; | |
} | |
}; | |
myWebView = new WebView(myContext); | |
myWebView.setWebViewClient(client); | |
WebView.setWebContentsDebuggingEnabled(true); | |
WebSettings webSettings = myWebView.getSettings(); | |
webSettings.setJavaScriptEnabled(true); | |
webSettings.setDomStorageEnabled(true); | |
myJSInterface = new JSInterface(this); | |
myWebView.addJavascriptInterface(myJSInterface, "androidWallpaperInterface"); | |
myWebView.loadUrl("file:///android_asset/index.html"); | |
// Create message receiver | |
myMessageReceiver = new BroadcastReceiver() { | |
@Override | |
public void onReceive(Context context, Intent intent) { | |
drawFrame(); | |
} | |
}; | |
LocalBroadcastManager.getInstance(myContext).registerReceiver(myMessageReceiver, | |
new IntentFilter("draw-frame") | |
); | |
} | |
@Override | |
public void onSurfaceDestroyed(SurfaceHolder holder) { | |
log("On Surface Destroy"); | |
if (myMessageReceiver != null) { | |
LocalBroadcastManager.getInstance(myContext).unregisterReceiver(myMessageReceiver); | |
myMessageReceiver = null; | |
} | |
if (myWebView != null) { | |
myWebView.destroy(); | |
myWebView = null; | |
} | |
super.onSurfaceDestroyed(holder); | |
} | |
@Override | |
public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) { | |
log("On Surface Changed " + String.valueOf(format) + ", " + String.valueOf(width) + ", " + String.valueOf(height)); | |
super.onSurfaceChanged(holder, format, width, height); | |
if (myWebView != null) { | |
int widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY); | |
int heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY); | |
myWebView.measure(widthSpec, heightSpec); | |
myWebView.layout(0, 0, width, height); | |
} | |
} | |
public void drawFrame() { | |
if (myWebView != null) { | |
try { | |
Canvas canvas = myHolder.lockCanvas(); | |
if (canvas == null) { | |
logError("Can't lock canvas"); | |
} else { | |
myWebView.draw(canvas); | |
myHolder.unlockCanvasAndPost(canvas); | |
} | |
} catch (Exception e) { | |
logError("drawing exception " + e.toString()); | |
} | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var animationFrame; | |
function frame() { | |
animationFrame = requestAnimationFrame(frame); | |
// Actually draw | |
// Let the host app know | |
window.androidWallpaperInterface.drawFrame(); | |
} | |
function pauseWallpaper() { | |
cancelAnimationFrame(animationFrame); | |
} | |
function resumeWallpaper() { | |
frame(); | |
} | |
frame(); |
@creativedrewy awesome! Thank you for sharing this project!
I wonder why it's so performance intensive. How does the Living Worlds app compare? I think it's relatively light. It's using canvas directly, plus a bunch of custom code. One difference is that its canvas is very small (345 by 455).
Thanks for the Heaven & Earth love! I assume you've seen https://www.clockworkgoldfish.com/? I still have on my back burner to do the remaining 11 puzzle types, but clearly I haven't gotten around to it yet.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hey everyone! I've put together a repo that pulls all of this together in a reference implementation. You can find it here:
https://github.com/creativedrewy/ArtWallpapers
The goal was to create an app that could render p5js sketches as live wallpapers. That codebase currently does this, but I haven't taken it much farther because performance is quite bad. As in, when rendering a live p5 sketch on a modern processor, it will still eat up two full cores. Not good for battery drain.
Either way I hope it can help someone!
Also @iangilman I grew up playing Heaven & Earth. I was completely tickled when I was looking for a solution like this and I found you talking about this very thing! No lie, I have wanted all of the Heaven & Earth illusion puzzles on mobile for a long time!