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

@gateian thank you for sharing your progress info! Apologies for being quiet… The end of 2020 was particularly hectic for me.

I'm glad you got the CPU usage sorted out… Your solution is exactly what I did as well.

If you can do Babylon native, that sounds like the best of both worlds. For my project, so much of it is already written using web/canvas that porting it over would have been a big stumbling block.

How goes the project? I'd love to see the finished product :-)

@gateian
Copy link

gateian commented May 11, 2021

No worries, i've been busy myself dealing with the pitter patter of tiny feet!

In the end, I went with threejs as my engine. Comparing it to babylonjs, I found threejs to be better performance. Possibly some will disagree with me here, but It's my preference anyway. Much faster loading times and has all the features I need.

I will keep chipping away at this, but needless to say I made some good progress in porting over the old version of the wallpaper and I love that I can run the wallpaper in my browser while developing without having to deploy to the phone all the time.

@iangilman
Copy link
Author

Awesome! Yes, being able to do the bulk of the development in the browser was a huge benefit for me 😃

And congratulations on the new arrival in your household! You're at the start of a BIG adventure... 🎉

@nibbida0
Copy link

nibbida0 commented Apr 13, 2023

@iangilman I tried your code and am able to launch a live wallpaper with webview.

Having 2 problem though...

  1. If I tried using WebGL with three.js, it throw exception: "THREE.WebGLRenderer: Error creating WebGL context". This error only happen when try to show the webview as live wallpaper. WebGL work perfect if I show the webview in normal android Activity.

  2. Also, I have tried the VirtualDisplay method, able to load live wallpaper with three.js for my Android 11 phone, but it throw "android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application" for my Android 7 phone, at the line when I call myPresentation.show()

Thanks!

@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