Skip to content

Instantly share code, notes, and snippets.

@iangilman
Last active August 10, 2023 18:36
Show Gist options
  • Save iangilman/71650d46384a2d4ae6387f2d4087cc37 to your computer and use it in GitHub Desktop.
Save iangilman/71650d46384a2d4ae6387f2d4087cc37 to your computer and use it in GitHub Desktop.
Android WallpaperService with WebView
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());
}
}
}
}
}
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();
@iangilman
Copy link
Author

@nibbida0 I'm glad my code has helped get you this far! Unfortunately, I know very little about Android beyond what I managed to make work here. I never tried WebGL and I haven't seen those errors.

Perhaps someone else here has suggestions?

At any rate, my recommendation would be to post on Stack Overflow and/or find Android development communities (I know they exist on Reddit and Discord, at least) and ask there.

Good luck! If you do find the answer and you think of it, I'd love to know what it turned out to be.

@creativedrewy
Copy link

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!

@iangilman
Copy link
Author

@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