2 * Copyright 2013 Jaroslaw Wisniewski <j.wisniewski@appsisle.com>
\r
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
\r
5 * License. You may obtain a copy of the License at
\r
7 * http://www.apache.org/licenses/LICENSE-2.0
\r
9 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS"
\r
10 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language
\r
11 * governing permissions and limitations under the License.
\r
15 package com.badlogic.gdx.backends.android;
\r
17 import java.lang.reflect.Method;
\r
18 import java.util.concurrent.locks.ReentrantLock;
\r
20 import com.badlogic.gdx.Application;
\r
21 import com.badlogic.gdx.ApplicationListener;
\r
22 import com.badlogic.gdx.Gdx;
\r
23 import com.badlogic.gdx.Graphics;
\r
24 import com.badlogic.gdx.android.AndroidWallpaperListener;
\r
25 import com.badlogic.gdx.backends.android.surfaceview.FillResolutionStrategy;
\r
26 import com.badlogic.gdx.backends.android.surfaceview.GLBaseSurfaceViewLW;
\r
27 import com.badlogic.gdx.backends.android.surfaceview.GLSurfaceViewCupcake;
\r
28 import com.badlogic.gdx.graphics.GL10;
\r
29 import com.badlogic.gdx.graphics.GL11;
\r
30 import com.badlogic.gdx.utils.GdxNativesLoader;
\r
32 import android.app.Activity;
\r
33 import android.app.WallpaperManager;
\r
34 import android.content.Context;
\r
35 import android.opengl.GLSurfaceView;
\r
36 import android.os.Bundle;
\r
37 import android.os.Handler;
\r
38 import android.provider.LiveFolders;
\r
39 import android.service.wallpaper.WallpaperService;
\r
40 import android.service.wallpaper.WallpaperService.Engine;
\r
41 import android.util.Log;
\r
42 import android.view.MotionEvent;
\r
43 import android.view.SurfaceHolder;
\r
44 import android.view.WindowManager;
\r
48 * An implementation of the {@link Application} interface dedicated for android live wallpapers.
\r
50 * Derive from this class. In the {@link AndroidLiveWallpaperService#onCreateApplication} method call the {@link AndroidLiveWallpaperService#initialize(ApplicationListener, boolean)}
\r
51 * method specifying the configuration for the GLSurfaceView. You can also use {@link AndroidWallpaperListener}
\r
52 * along with {@link ApplicationListener} to respond for wallpaper specific events in your app listener:
\r
54 * MyAppListener implements ApplicationListener, AndroidWallpaperListener
\r
57 * Following methods are not called for live wallpapers:
\r
58 * {@link ApplicationListener#pause()}
\r
59 * {@link ApplicationListener#dispose()}
\r
60 * TODO add callbacks to AndroidWallpaperListener allowing to notify app listener about changed visibility
\r
61 * state of live wallpaper but called from main thread, not from GL thread:
\r
63 * AndroidWallpaperListener.visibilityChanged(boolean)
\r
67 * //You have to kill all not daemon threads you created in {@link ApplicationListener#pause()} method.
\r
68 * //{@link ApplicationListener#dispose()} is never called!
\r
69 * //If you leave live non daemon threads, wallpaper service wouldn't be able to close,
\r
70 * //this can cause problems with wallpaper lifecycle.
\r
73 * On some devices wallpaper service is not killed immediately after exiting from preview. Service object
\r
74 * is destroyed (onDestroy called) but process on which it runs remains alive. When user comes back to wallpaper
\r
75 * preview, new wallpaper service object is created, but in the same process. It is important if you plan to
\r
76 * use static variables / objects - they will be shared between living instances of wallpaper services'!
\r
77 * And depending on your implementation - it can cause problems you were not prepared to.
\r
79 * @author Jaroslaw Wisniewski <j.wisniewski@appsisle.com>
\r
81 public abstract class AndroidLiveWallpaperService extends WallpaperService {
\r
83 GdxNativesLoader.load();
\r
86 static final String TAG = "WallpaperService";
\r
87 static boolean DEBUG = false; // TODO remember to disable this
\r
90 // instance of libGDX Application, acts as singleton - one instance per application (per WallpaperService)
\r
91 protected volatile AndroidLiveWallpaper app = null; // can be accessed from GL render thread
\r
92 protected SurfaceHolder.Callback view = null;
\r
94 // current format of surface (one GLSurfaceView is shared between all engines)
\r
95 protected int viewFormat;
\r
96 protected int viewWidth;
\r
97 protected int viewHeight;
\r
99 // app is initialized when engines == 1 first time, app is destroyed in WallpaperService.onDestroy, but ApplicationListener.dispose is not called for wallpapers
\r
100 protected int engines = 0;
\r
101 protected int visibleEngines = 0;
\r
103 // engine currently associated with app instance, linked engine serves surface handler for GLSurfaceView
\r
104 protected volatile AndroidWallpaperEngine linkedEngine = null; // can be accessed from GL render thread by getSurfaceHolder
\r
106 protected void setLinkedEngine (AndroidWallpaperEngine linkedEngine) {
\r
107 synchronized (sync) {
\r
108 this.linkedEngine = linkedEngine;
\r
113 // if preview state notified ever
\r
114 protected volatile boolean isPreviewNotified = false;
\r
116 // the value of last preview state notified to app listener
\r
117 protected volatile boolean notifiedPreviewState = false;
\r
120 volatile int[] sync = new int[0];
\r
121 //volatile ReentrantLock lock = new ReentrantLock();
\r
124 // lifecycle methods - the order of calling (flow) is maintained ///////////////
\r
126 public AndroidLiveWallpaperService () {
\r
132 * Service is starting, libGDX application is shutdown now
\r
135 public void onCreate () {
\r
136 if (DEBUG) Log.d(TAG, " > AndroidLiveWallpaperService - onCreate() " + hashCode());
\r
137 Log.i(TAG, "service created");
\r
144 * One of wallpaper engines is starting.
\r
145 * Do not override this method, service manages them internally.
\r
148 public Engine onCreateEngine () {
\r
149 if (DEBUG) Log.d(TAG, " > AndroidLiveWallpaperService - onCreateEngine()");
\r
150 Log.i(TAG, "engine created");
\r
152 return new AndroidWallpaperEngine();
\r
157 * libGDX application is starting, it occurs after first wallpaper engine had started.
\r
158 * Override this method an invoke {@link AndroidLiveWallpaperService#initialize(ApplicationListener, AndroidApplicationConfiguration)} from there.
\r
160 public void onCreateApplication () {
\r
161 if (DEBUG) Log.d(TAG, " > AndroidLiveWallpaperService - onCreateApplication()");
\r
166 * Look at {@link AndroidLiveWallpaperService#initialize(ApplicationListener, AndroidApplicationConfiguration)}}
\r
168 * @param useGL2IfAvailable
\r
170 public void initialize (ApplicationListener listener, boolean useGL2IfAvailable) {
\r
171 AndroidApplicationConfiguration config = new AndroidApplicationConfiguration();
\r
172 config.useGL20 = useGL2IfAvailable;
\r
173 initialize(listener, config);
\r
177 * This method has to be called in the {@link AndroidLiveWallpaperService#onCreateApplication} method. It sets up all the things necessary to get
\r
178 * input, render via OpenGL and so on. If config.useGL20 is set the AndroidApplication will try to create an OpenGL ES 2.0
\r
179 * context which can then be used via {@link Graphics#getGL20()}. The {@link GL10} and {@link GL11} interfaces should not be
\r
180 * used when OpenGL ES 2.0 is enabled. To query whether enabling OpenGL ES 2.0 was successful use the
\r
181 * {@link Graphics#isGL20Available()} method. You can configure other aspects of the application with the rest of the fields in
\r
182 * the {@link AndroidApplicationConfiguration} instance.
\r
184 * @param listener the {@link ApplicationListener} implementing the program logic
\r
185 * @param config the {@link AndroidApplicationConfiguration}, defining various settings of the application (use accelerometer,
\r
186 * etc.). Do not change contents of this object after passing to this method!
\r
188 public void initialize (ApplicationListener listener, AndroidApplicationConfiguration config) {
\r
189 if (DEBUG) Log.d(TAG, " > AndroidLiveWallpaperService - initialize()");
\r
191 app.initialize(listener, config);
\r
193 if (config.getTouchEventsForLiveWallpaper && Integer.parseInt(android.os.Build.VERSION.SDK) >= 7)
\r
194 linkedEngine.setTouchEventsEnabled(true);
\r
196 //onResume(); do not call it there
\r
201 * Getter for SurfaceHolder object, surface holder is required to restore gl context in GLSurfaceView
\r
203 public SurfaceHolder getSurfaceHolder() {
\r
204 if (DEBUG) Log.d(TAG, " > AndroidLiveWallpaperService - getSurfaceHolder()");
\r
206 synchronized (sync) {
\r
207 if (linkedEngine == null)
\r
210 return linkedEngine.getSurfaceHolder();
\r
215 // engines live there
\r
219 * Called when the last engine is ending its live, it can occur when:
\r
220 * 1. service is dying
\r
221 * 2. service is switching from one engine to another
\r
222 * 3. [only my assumption] when wallpaper is not visible and system is going to restore some memory
\r
223 * for foreground processing by disposing not used wallpaper engine
\r
224 * We can't destroy app there, because:
\r
225 * 1. in won't work - gl context is disposed right now and after app.onDestroy() app would stuck somewhere in gl thread synchronizing code
\r
226 * 2. we don't know if service create more engines, app is shared between them and should stay initialized waiting for new engines
\r
228 public void onDeepPauseApplication () {
\r
229 if (DEBUG) Log.d(TAG, " > AndroidLiveWallpaperService - onDeepPauseApplication()");
\r
231 // free native resources consuming runtime memory, note that it can cause some lag when resuming wallpaper
\r
233 app.graphics.clearManagedCaches();
\r
239 * Service is dying, and will not be used again.
\r
240 * You have to finish execution off all living threads there or short after there,
\r
241 * besides the new wallpaper service wouldn't be able to start.
\r
244 public void onDestroy () {
\r
245 if (DEBUG) Log.d(TAG, " > AndroidLiveWallpaperService - onDestroy() " + hashCode());
\r
246 Log.i(TAG, "service destroyed");
\r
248 super.onDestroy(); // can call engine.onSurfaceDestroyed, must be before bellow code:
\r
260 protected void finalize () throws Throwable {
\r
261 Log.i(TAG, "service finalized");
\r
265 // end of lifecycle methods ////////////////////////////////////////////////////////
\r
269 public AndroidLiveWallpaper getLiveWallpaper() {
\r
274 public WindowManager getWindowManager() {
\r
275 return (WindowManager)getSystemService(Context.WINDOW_SERVICE);
\r
280 * Bridge between surface on which wallpaper is rendered and the wallpaper service.
\r
281 * The problem is that there can be a group of Engines at one time and we must share libGDX application between them.
\r
283 * @author libGDX team and Jaroslaw Wisniewski <j.wisniewski@appsisle.com>
\r
286 public class AndroidWallpaperEngine extends Engine {
\r
288 protected boolean engineIsVisible = false;
\r
290 // destination format of surface when this engine is active (updated in onSurfaceChanged)
\r
291 protected int engineFormat;
\r
292 protected int engineWidth;
\r
293 protected int engineHeight;
\r
296 // lifecycle methods - the order of calling (flow) is maintained /////////////////
\r
298 public AndroidWallpaperEngine () {
\r
299 if (DEBUG) Log.d(TAG, " > AndroidWallpaperEngine() " + hashCode());
\r
304 public void onCreate (final SurfaceHolder surfaceHolder) {
\r
305 if (DEBUG) Log.d(TAG, " > AndroidWallpaperEngine - onCreate() " + hashCode() + " running: " + engines + ", linked: " + (linkedEngine == this) + ", thread: " + Thread.currentThread().toString());
\r
306 super.onCreate(surfaceHolder);
\r
311 * Called before surface holder callbacks (ex for GLSurfaceView)!
\r
312 * This is called immediately after the surface is first created. Implementations of this should start
\r
313 * up whatever rendering code they desire. Note that only one thread can ever draw into a Surface,
\r
314 * so you should not draw into the Surface here if your normal rendering will be in another thread.
\r
317 public void onSurfaceCreated (final SurfaceHolder holder) {
\r
319 setLinkedEngine(this);
\r
321 if (DEBUG) Log.d(TAG, " > AndroidWallpaperEngine - onSurfaceCreated() " + hashCode() + ", running: " + engines + ", linked: " + (linkedEngine == this));
\r
322 Log.i(TAG, "engine surface created");
\r
324 super.onSurfaceCreated(holder);
\r
326 if (engines == 1) {
\r
327 // safeguard: recover attributes that could suffered by unexpected surfaceDestroy event
\r
328 visibleEngines = 0;
\r
331 if (engines == 1 && app == null) {
\r
332 viewFormat = 0; // must be initialized with zeroes
\r
336 app = new AndroidLiveWallpaper(AndroidLiveWallpaperService.this);
\r
338 onCreateApplication();
\r
339 if (app.graphics == null)
\r
340 throw new Error("You must override 'AndroidLiveWallpaperService.onCreateApplication' method and call 'initialize' from its body.");
\r
343 view = (SurfaceHolder.Callback)app.graphics.view;
\r
344 this.getSurfaceHolder().removeCallback(view); // we are going to call this events manually
\r
346 // inherit format from shared surface view
\r
347 engineFormat = viewFormat;
\r
348 engineWidth = viewWidth;
\r
349 engineHeight = viewHeight;
\r
351 if (engines == 1) {
\r
352 view.surfaceCreated(holder);
\r
355 // this combination of methods is described in AndroidWallpaperEngine.onResume
\r
356 view.surfaceDestroyed(holder);
\r
357 notifySurfaceChanged(engineFormat, engineWidth, engineHeight, false);
\r
358 view.surfaceCreated(holder);
\r
361 notifyPreviewState();
\r
362 notifyOffsetsChanged();
\r
367 * This is called immediately after any structural changes (format or size) have been made to the surface.
\r
368 * You should at this point update the imagery in the surface. This method is always called at least once,
\r
369 * after surfaceCreated(SurfaceHolder).
\r
372 public void onSurfaceChanged (final SurfaceHolder holder, final int format, final int width, final int height) {
\r
373 if (DEBUG) Log.d(TAG, " > AndroidWallpaperEngine - onSurfaceChanged() isPreview: " + isPreview() + ", " + hashCode() + ", running: " + engines + ", linked: " + (linkedEngine == this) + ", sufcace valid: " + getSurfaceHolder().getSurface().isValid());
\r
374 Log.i(TAG, "engine surface changed");
\r
376 super.onSurfaceChanged(holder, format, width, height);
\r
378 notifySurfaceChanged(format, width, height, true);
\r
380 // it shouldn't be required there (as I understand android.service.wallpaper.WallpaperService impl)
\r
381 //notifyPreviewState();
\r
386 * Notifies shared GLSurfaceView about changed surface format.
\r
390 * @param forceUpdate if false, surface view will be notified only if currently contains expired information
\r
392 private void notifySurfaceChanged(final int format, final int width, final int height, boolean forceUpdate)
\r
394 if (!forceUpdate && format == viewFormat && width == viewWidth && height == viewHeight) {
\r
395 // skip if didn't changed
\r
396 if (DEBUG) Log.d(TAG, " > surface is current, skipping surfaceChanged event");
\r
399 // update engine desired surface format
\r
400 engineFormat = format;
\r
401 engineWidth = width;
\r
402 engineHeight = height;
\r
404 // update surface view if engine is linked with it already
\r
405 if (linkedEngine == this) {
\r
406 viewFormat = engineFormat;
\r
407 viewWidth = engineWidth;
\r
408 viewHeight = engineHeight;
\r
409 view.surfaceChanged(this.getSurfaceHolder(), viewFormat, viewWidth, viewHeight);
\r
412 if (DEBUG) Log.d(TAG, " > engine is not active, skipping surfaceChanged event");
\r
419 * Called to inform you of the wallpaper becoming visible or hidden. It is very important that
\r
420 * a wallpaper only use CPU while it is visible..
\r
423 public void onVisibilityChanged (final boolean visible) {
\r
424 boolean reportedVisible = isVisible();
\r
426 if (DEBUG) Log.d(TAG, " > AndroidWallpaperEngine - onVisibilityChanged(paramVisible: " + visible + " reportedVisible: " + reportedVisible + ") " + hashCode() + ", sufcace valid: " + getSurfaceHolder().getSurface().isValid());
\r
427 super.onVisibilityChanged(visible);
\r
429 // Android WallpaperService sends fake visibility changed events to force some buggy live wallpapers to shut down after onSurfaceChanged when they aren't visible, it can cause problems in current implementation and it is not necessary
\r
430 if (reportedVisible == false && visible == true) {
\r
431 if (DEBUG) Log.d(TAG, " > fake visibilityChanged event! Android WallpaperService likes do that!");
\r
435 notifyVisibilityChanged(visible);
\r
439 private void notifyVisibilityChanged(final boolean visible)
\r
441 if (this.engineIsVisible != visible) {
\r
442 this.engineIsVisible = visible;
\r
444 if (this.engineIsVisible)
\r
450 if (DEBUG) Log.d(TAG, " > visible state is current, skipping visibilityChanged event!");
\r
455 public void onResume () {
\r
457 if (DEBUG) Log.d(TAG, " > AndroidWallpaperEngine - onResume() " + hashCode() + ", running: " + engines + ", linked: " + (linkedEngine == this) + ", visible: " + visibleEngines);
\r
458 Log.i(TAG, "engine resumed");
\r
460 if (linkedEngine != null) {
\r
461 if (linkedEngine != this) {
\r
462 setLinkedEngine(this);
\r
464 // disconnect surface view from previous window
\r
465 view.surfaceDestroyed(this.getSurfaceHolder()); // force gl surface reload, new instance will be created on current surface holder
\r
467 // resize surface to match window associated with current engine
\r
468 notifySurfaceChanged(engineFormat, engineWidth, engineHeight, false);
\r
470 // connect surface view to current engine
\r
471 view.surfaceCreated(this.getSurfaceHolder());
\r
474 // update if surface changed when engine wasn't active
\r
475 notifySurfaceChanged(engineFormat, engineWidth, engineHeight, false);
\r
478 if (visibleEngines == 1)
\r
481 notifyPreviewState();
\r
482 notifyOffsetsChanged();
\r
487 public void onPause () {
\r
489 if (DEBUG) Log.d(TAG, " > AndroidWallpaperEngine - onPause() " + hashCode() + ", running: " + engines + ", linked: " + (linkedEngine == this) + ", visible: " + visibleEngines);
\r
490 Log.i(TAG, "engine paused");
\r
492 // this shouldn't never happen, but if it will.. live wallpaper will not be stopped when device will pause and lwp will drain battery.. shortly!
\r
493 if (visibleEngines >= engines) {
\r
494 Log.e(AndroidLiveWallpaperService.TAG, "wallpaper lifecycle error, counted too many visible engines! repairing..");
\r
495 visibleEngines = Math.max(engines - 1, 0);
\r
498 if (linkedEngine != null) {
\r
499 if (visibleEngines == 0)
\r
503 if (DEBUG) Log.d(TAG, " > AndroidWallpaperEngine - onPause() done!");
\r
508 * Called after surface holder callbacks (ex for GLSurfaceView)!
\r
509 * This is called immediately before a surface is being destroyed. After returning from this call,
\r
510 * you should no longer try to access this surface. If you have a rendering thread that directly
\r
511 * accesses the surface, you must ensure that thread is no longer touching the Surface before
\r
512 * returning from this function.
\r
515 * In some cases GL context may be shutdown right now! and SurfaceHolder.Surface.isVaild = false
\r
518 public void onSurfaceDestroyed (final SurfaceHolder holder) {
\r
520 if (DEBUG) Log.d(TAG, " > AndroidWallpaperEngine - onSurfaceDestroyed() " + hashCode() + ", running: " + engines + " ,linked: " + (linkedEngine == this) + ", isVisible: " + engineIsVisible);
\r
521 Log.i(TAG, "engine surface destroyed");
\r
523 // application can be in resumed state at this moment if app surface had been lost just after it was created (wallpaper selected too fast from preview mode etc)
\r
524 // it is too late probably - calling on pause causes deadlock
\r
525 //notifyVisibilityChanged(false);
\r
527 // it is too late to call app.onDispose, just free native resources
\r
529 onDeepPauseApplication();
\r
531 // free surface if it belongs to this engine and if it was initialized
\r
532 if (linkedEngine == this && view != null)
\r
533 view.surfaceDestroyed(holder);
\r
535 //waitingSurfaceChangedEvent = null;
\r
540 // safeguard for other engine callbacks
\r
542 linkedEngine = null;
\r
544 super.onSurfaceDestroyed(holder);
\r
549 public void onDestroy () {
\r
553 // end of lifecycle methods ////////////////////////////////////////////////////////
\r
559 public Bundle onCommand (final String pAction, final int pX, final int pY, final int pZ, final Bundle pExtras,
\r
560 final boolean pResultRequested) {
\r
561 if (DEBUG) Log.d(TAG, " > AndroidWallpaperEngine - onCommand(" + pAction + " " + pX + " " + pY + " " + pZ + " " + pExtras + " " + pResultRequested + ")" + ", linked: " + (linkedEngine == this));
\r
563 return super.onCommand(pAction, pX, pY, pZ, pExtras, pResultRequested);
\r
568 public void onTouchEvent (MotionEvent event) {
\r
569 if (linkedEngine == this) {
\r
570 app.input.onTouch(null, event);
\r
575 // offsets from last onOffsetsChanged
\r
576 boolean offsetsConsumed = true;
\r
577 float xOffset = 0.0f;
\r
578 float yOffset = 0.0f;
\r
579 float xOffsetStep = 0.0f;
\r
580 float yOffsetStep = 0.0f;
\r
581 int xPixelOffset = 0;
\r
582 int yPixelOffset = 0;
\r
585 public void onOffsetsChanged (final float xOffset, final float yOffset, final float xOffsetStep, final float yOffsetStep, final int xPixelOffset,
\r
586 final int yPixelOffset) {
\r
588 // it spawns too frequent on some devices - its annoying!
\r
590 // Log.d(TAG, " > AndroidWallpaperEngine - onOffsetChanged(" + xOffset + " " + yOffset + " " + xOffsetStep + " "
\r
591 // + yOffsetStep + " " + xPixelOffset + " " + yPixelOffset + ") " + hashCode() + ", linkedApp: " + (linkedApp != null));
\r
593 this.offsetsConsumed = false;
\r
594 this.xOffset = xOffset;
\r
595 this.yOffset = yOffset;
\r
596 this.xOffsetStep = xOffsetStep;
\r
597 this.yOffsetStep = yOffsetStep;
\r
598 this.xPixelOffset = xPixelOffset;
\r
599 this.yPixelOffset = yPixelOffset;
\r
601 // can fail if linkedApp == null, so we repeat it in Engine.onResume
\r
602 notifyOffsetsChanged();
\r
604 super.onOffsetsChanged(xOffset, yOffset, xOffsetStep, yOffsetStep, xPixelOffset, yPixelOffset);
\r
608 protected void notifyOffsetsChanged()
\r
610 if (linkedEngine == this && app.listener instanceof AndroidWallpaperListener) {
\r
611 if (!offsetsConsumed) { // no need for more sophisticated synchronization - offsetsChanged can be called multiple times and with various patterns on various devices - user application must be prepared for that
\r
612 offsetsConsumed = true;
\r
614 app.postRunnable(new Runnable() {
\r
616 public void run () {
\r
617 boolean isCurrent = false;
\r
618 synchronized (sync) {
\r
619 isCurrent = (linkedEngine == AndroidWallpaperEngine.this); // without this app can crash when fast switching between engines (tested!)
\r
622 ((AndroidWallpaperListener)app.listener).offsetChange(xOffset, yOffset, xOffsetStep, yOffsetStep, xPixelOffset, yPixelOffset);
\r
630 protected void notifyPreviewState()
\r
632 // notify preview state to app listener
\r
633 if (linkedEngine == this && app.listener instanceof AndroidWallpaperListener) {
\r
634 final boolean currentPreviewState = linkedEngine.isPreview();
\r
635 app.postRunnable(new Runnable() {
\r
637 public void run () {
\r
638 boolean shouldNotify = false;
\r
639 synchronized (sync) {
\r
640 if (!isPreviewNotified || notifiedPreviewState != currentPreviewState) {
\r
641 notifiedPreviewState = currentPreviewState;
\r
642 isPreviewNotified = true;
\r
643 shouldNotify = true;
\r
647 if (shouldNotify) {
\r
648 AndroidLiveWallpaper currentApp = app; // without this app can crash when fast switching between engines (tested!)
\r
649 if (currentApp != null)
\r
650 ((AndroidWallpaperListener)currentApp.listener).previewStateChange(currentPreviewState);
\r