Skip to content

Instantly share code, notes, and snippets.

@iangilman
Last active August 10, 2023 18:36
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • 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();
@tabish075
Copy link

tabish075 commented Apr 2, 2019

Hey there thank you for your awesome work and research, I am also trying to use webview as a live wallpaper i saw your comments can you please give me a kick start Please help

I have added these two files in a new project but how to use them?

Update 1: i have used this tutorial https://www.vogella.com/tutorials/AndroidLiveWallpaper/article.html, it works fine then i've added your service and now it stuck on Loading live wallpaper...

Update 2: Finally got it working there is just one problem WebView is not updating on data changed in website

Update 3: Got it updating!

@iangilman
Copy link
Author

@tabish075 Apologies for not getting back to you sooner... I'm glad you were able to get it sorted out! Are there comments I can add to this Gist to make it clearer?

@pjc0247
Copy link

pjc0247 commented Jun 22, 2019

@iangilman How can I make a top-margin?
layout(top:100) does not work.

@iangilman
Copy link
Author

@pjc0247 it depends on what you mean by top-margin I guess. Do you want the wallpaper to be fullscreen, but to have some margin inside your webpage? If so, you could use CSS in your webpage. Or do you want the webpage to actually start lower on the screen? Then maybe you would change this line in the code above:

myWebView.layout(0, 0, width, height);

... to something like:

myWebView.layout(0, 100, width, height - 100);

@sundamissan
Copy link

Hi @https://gist.github.com/iangilman , is there a way to make this interactive?

@iangilman
Copy link
Author

Meaning you'd like the wallpaper itself to respond to the user's actions? Unfortunately I don't know enough about live wallpapers to know what's possible. I think maybe the wallpaper can know when you swipe from page to page in the home screen, and it can probably know about device orientation. Anyway, any of that stuff you can detect in Java you can send to the JavaScript via the myWebView.loadUrl mechanism.

Does that help answer your question?

@sundamissan
Copy link

sundamissan commented May 27, 2020

Yes, thank you very much.
The live wallpaper is a lot slower than compared to the webView. Is there something we can do for that?

@iangilman
Copy link
Author

Hmm… how slow? I don't think I actually did any FPS logging to see what the difference was, but for my app (http://pixfabrik.com/livingworlds/) it runs plenty fast enough for my needs.

BTW, I've been exploring another technique for web-based live wallpapers. Here's some info about that:

https://stackoverflow.com/questions/57452827/android-how-to-use-a-virtualdisplay-to-host-a-webview-in-a-wallpaperservice

I'm using both techniques in my app (allowing the user to switch), because they need to run better on some devices. Here's a little more on that:

https://medium.com/@iangilman/living-worlds-small-android-update-ef3fda3f10ca

I know that any of that is relevant to your slow frame rate, but I thought it was worth mentioning here anyway.

@gateian
Copy link

gateian commented Nov 13, 2020

Just to say thanks for the help on this. It's good to know it's possible. However sadly it seems the code needs updating as LocalBroadcastManager is now deprecated.

I tried your stack overflow presentation technique code and placed that code onSurfaceChanged function as mentioned, but keep getting a white screen. Weirdly, it seems that the 3D engine is functioning perfectly as I can see it firing it's functions in the chrome debugger tools, it's just not drawing anything to screen strangely.

@iangilman
Copy link
Author

I'm glad it's of help! Hopefully you can get these last details sorted out.

However sadly it seems the code needs updating as LocalBroadcastManager is now deprecated.

Bummer. I'm still using LocalBroadcastManager, but presumably eventually I'll need to switch. I haven't looked into it, yet, though. If you figure out the correct new technique for passing the events to the right thread, I'd love to hear about it!

I tried your stack overflow presentation technique code and placed that code onSurfaceChanged function as mentioned, but keep getting a white screen. Weirdly, it seems that the 3D engine is functioning perfectly as I can see it firing it's functions in the chrome debugger tools, it's just not drawing anything to screen strangely.

Are you calling show like it suggests in the Stack Overflow? If not, that's what fixed the white screen for me. Otherwise, can you share that function so I can see how you're doing it?

@gateian
Copy link

gateian commented Nov 21, 2020

I eventually got this working using the Presentation and virtual display technique you posted on Stack Overflow. Originally I was converting the above gist to work with this technique so I think I had some residual code left in that was causing an issue. I started a clean Wallpaper Engine class and voila! Works like a charm.

I am using Babylonjs on the webview to display my live wallpaper and want to do some testing to see how efficient that set up is. I'm toying with using BabylonJS native though, as I would expect that to run more efficiently than javascript in a webview. I also have some concerns over users having different browsers installed and any compatability issues.

Did you have any issues with high cpu use? I notice that it is quite high for my project at the moment. Too high at 10% and it doesn't seem to drop when the live wallpaper is not visible. I expected it to be the webview and babylonjs scene running, but I did a quick test to destroy the webview when the wallpaper became invisible and the cpu usage seemed to remain constant in the background. Perhaps the webview has remained in the background somehow. My android development knowledge is rusty, so need to investigate and work out if it is webview, presentation or virtual display. Hopefully it's the webview as I can do something about that.

EDIT: It WAS the webview causing high cpu usage. I now call a javascript function and pass through the visible state of wallpaper to pause and resume rendering. CPU resources now not being used when wallpaper is not visible.

I'd love convert my old Unity Based wallpaper https://play.google.com/store/apps/details?id=com.RockCakeGames.ToonTown3DFree which was basically killed by the unity plugin I was using not being updated and i've fallen in love with webgl recently and love how quick and fast it is to develop with.

Lots to consider.

@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