Home | History | Annotate | Download | only in android
      1 /*
      2  * Copyright 2013 Jaroslaw Wisniewski <j.wisniewski (at) appsisle.com>
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
      5  * License. You may obtain a copy of the License at
      6  *
      7  * http://www.apache.org/licenses/LICENSE-2.0
      8  *
      9  * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS"
     10  * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language
     11  * governing permissions and limitations under the License.
     12  *
     13  */
     14 
     15 package com.badlogic.gdx.backends.android;
     16 
     17 import android.content.Context;
     18 import android.os.Bundle;
     19 import android.service.wallpaper.WallpaperService;
     20 import android.util.Log;
     21 import android.view.MotionEvent;
     22 import android.view.SurfaceHolder;
     23 import android.view.WindowManager;
     24 
     25 import com.badlogic.gdx.Application;
     26 import com.badlogic.gdx.ApplicationListener;
     27 import com.badlogic.gdx.Gdx;
     28 import com.badlogic.gdx.Graphics;
     29 import com.badlogic.gdx.utils.GdxNativesLoader;
     30 
     31 /** An implementation of the {@link Application} interface dedicated for android live wallpapers.
     32  *
     33  * Derive from this class. In the {@link AndroidLiveWallpaperService#onCreateApplication} method call the
     34  * {@link AndroidLiveWallpaperService#initialize(ApplicationListener)} method specifying the configuration for the GLSurfaceView.
     35  * You can also use {@link AndroidWallpaperListener} along with {@link ApplicationListener} to respond for wallpaper specific
     36  * events in your app listener:
     37  *
     38  * MyAppListener implements ApplicationListener, AndroidWallpaperListener
     39  *
     40  * Notice: Following methods are not called for live wallpapers: {@link ApplicationListener#pause()}
     41  * {@link ApplicationListener#dispose()} TODO add callbacks to AndroidWallpaperListener allowing to notify app listener about
     42  * changed visibility state of live wallpaper but called from main thread, not from GL thread: for example:
     43  * AndroidWallpaperListener.visibilityChanged(boolean)
     44  *
     45  * //obsoleted: //Notice! //You have to kill all not daemon threads you created in {@link ApplicationListener#pause()} method. //
     46  * {@link ApplicationListener#dispose()} is never called! //If you leave live non daemon threads, wallpaper service wouldn't be
     47  * able to close, //this can cause problems with wallpaper lifecycle.
     48  *
     49  * Notice #2! On some devices wallpaper service is not killed immediately after exiting from preview. Service object is destroyed
     50  * (onDestroy called) but process on which it runs remains alive. When user comes back to wallpaper preview, new wallpaper service
     51  * object is created, but in the same process. It is important if you plan to use static variables / objects - they will be shared
     52  * between living instances of wallpaper services'! And depending on your implementation - it can cause problems you were not
     53  * prepared to.
     54  *
     55  * @author Jaroslaw Wisniewski <j.wisniewski (at) appsisle.com> */
     56 public abstract class AndroidLiveWallpaperService extends WallpaperService {
     57 	static {
     58 		GdxNativesLoader.load();
     59 	}
     60 
     61 	static final String TAG = "WallpaperService";
     62 	static boolean DEBUG = false; // TODO remember to disable this
     63 
     64 	// instance of libGDX Application, acts as singleton - one instance per application (per WallpaperService)
     65 	protected volatile AndroidLiveWallpaper app = null; // can be accessed from GL render thread
     66 	protected SurfaceHolder.Callback view = null;
     67 
     68 	// current format of surface (one GLSurfaceView is shared between all engines)
     69 	protected int viewFormat;
     70 	protected int viewWidth;
     71 	protected int viewHeight;
     72 
     73 	// app is initialized when engines == 1 first time, app is destroyed in WallpaperService.onDestroy, but
     74 // ApplicationListener.dispose is not called for wallpapers
     75 	protected int engines = 0;
     76 	protected int visibleEngines = 0;
     77 
     78 	// engine currently associated with app instance, linked engine serves surface handler for GLSurfaceView
     79 	protected volatile AndroidWallpaperEngine linkedEngine = null; // can be accessed from GL render thread by getSurfaceHolder
     80 
     81 	protected void setLinkedEngine (AndroidWallpaperEngine linkedEngine) {
     82 		synchronized (sync) {
     83 			this.linkedEngine = linkedEngine;
     84 		}
     85 	}
     86 
     87 	// if preview state notified ever
     88 	protected volatile boolean isPreviewNotified = false;
     89 
     90 	// the value of last preview state notified to app listener
     91 	protected volatile boolean notifiedPreviewState = false;
     92 
     93 	volatile int[] sync = new int[0];
     94 
     95 	// volatile ReentrantLock lock = new ReentrantLock();
     96 
     97 	// lifecycle methods - the order of calling (flow) is maintained ///////////////
     98 
     99 	public AndroidLiveWallpaperService () {
    100 		super();
    101 	}
    102 
    103 	/** Service is starting, libGDX application is shutdown now */
    104 	@Override
    105 	public void onCreate () {
    106 		if (DEBUG) Log.d(TAG, " > AndroidLiveWallpaperService - onCreate() " + hashCode());
    107 		Log.i(TAG, "service created");
    108 
    109 		super.onCreate();
    110 	}
    111 
    112 	/** One of wallpaper engines is starting. Do not override this method, service manages them internally. */
    113 	@Override
    114 	public Engine onCreateEngine () {
    115 		if (DEBUG) Log.d(TAG, " > AndroidLiveWallpaperService - onCreateEngine()");
    116 		Log.i(TAG, "engine created");
    117 
    118 		return new AndroidWallpaperEngine();
    119 	}
    120 
    121 	/** libGDX application is starting, it occurs after first wallpaper engine had started. Override this method an invoke
    122 	 * {@link AndroidLiveWallpaperService#initialize(ApplicationListener, AndroidApplicationConfiguration)} from there. */
    123 	public void onCreateApplication () {
    124 		if (DEBUG) Log.d(TAG, " > AndroidLiveWallpaperService - onCreateApplication()");
    125 	}
    126 
    127 	/** Look at {@link AndroidLiveWallpaperService#initialize(ApplicationListener, AndroidApplicationConfiguration)}
    128 	 * @param listener */
    129 	public void initialize (ApplicationListener listener) {
    130 		AndroidApplicationConfiguration config = new AndroidApplicationConfiguration();
    131 		initialize(listener, config);
    132 	}
    133 
    134 	/** This method has to be called in the {@link AndroidLiveWallpaperService#onCreateApplication} method. It sets up all the
    135 	 * things necessary to get input, render via OpenGL and so on. You can configure other aspects of the application with the rest
    136 	 * of the fields in the {@link AndroidApplicationConfiguration} instance.
    137 	 *
    138 	 * @param listener the {@link ApplicationListener} implementing the program logic
    139 	 * @param config the {@link AndroidApplicationConfiguration}, defining various settings of the application (use accelerometer,
    140 	 *           etc.). Do not change contents of this object after passing to this method! */
    141 	public void initialize (ApplicationListener listener, AndroidApplicationConfiguration config) {
    142 		if (DEBUG) Log.d(TAG, " > AndroidLiveWallpaperService - initialize()");
    143 
    144 		app.initialize(listener, config);
    145 
    146 		if (config.getTouchEventsForLiveWallpaper && Integer.parseInt(android.os.Build.VERSION.SDK) >= 7)
    147 			linkedEngine.setTouchEventsEnabled(true);
    148 
    149 		// onResume(); do not call it there
    150 	}
    151 
    152 	/** Getter for SurfaceHolder object, surface holder is required to restore gl context in GLSurfaceView */
    153 	public SurfaceHolder getSurfaceHolder () {
    154 		if (DEBUG) Log.d(TAG, " > AndroidLiveWallpaperService - getSurfaceHolder()");
    155 
    156 		synchronized (sync) {
    157 			if (linkedEngine == null)
    158 				return null;
    159 			else
    160 				return linkedEngine.getSurfaceHolder();
    161 		}
    162 	}
    163 
    164 	// engines live there
    165 
    166 	/** Called when the last engine is ending its live, it can occur when: 1. service is dying 2. service is switching from one
    167 	 * engine to another 3. [only my assumption] when wallpaper is not visible and system is going to restore some memory for
    168 	 * foreground processing by disposing not used wallpaper engine We can't destroy app there, because: 1. in won't work - gl
    169 	 * context is disposed right now and after app.onDestroy() app would stuck somewhere in gl thread synchronizing code 2. we
    170 	 * don't know if service create more engines, app is shared between them and should stay initialized waiting for new engines */
    171 	public void onDeepPauseApplication () {
    172 		if (DEBUG) Log.d(TAG, " > AndroidLiveWallpaperService - onDeepPauseApplication()");
    173 
    174 		// free native resources consuming runtime memory, note that it can cause some lag when resuming wallpaper
    175 		if (app != null) {
    176 			app.graphics.clearManagedCaches();
    177 		}
    178 	}
    179 
    180 	/** Service is dying, and will not be used again. You have to finish execution off all living threads there or short after
    181 	 * there, besides the new wallpaper service wouldn't be able to start. */
    182 	@Override
    183 	public void onDestroy () {
    184 		if (DEBUG) Log.d(TAG, " > AndroidLiveWallpaperService - onDestroy() " + hashCode());
    185 		Log.i(TAG, "service destroyed");
    186 
    187 		super.onDestroy(); // can call engine.onSurfaceDestroyed, must be before bellow code:
    188 
    189 		if (app != null) {
    190 			app.onDestroy();
    191 
    192 			app = null;
    193 			view = null;
    194 		}
    195 	}
    196 
    197 	@Override
    198 	protected void finalize () throws Throwable {
    199 		Log.i(TAG, "service finalized");
    200 		super.finalize();
    201 	}
    202 
    203 	// end of lifecycle methods ////////////////////////////////////////////////////////
    204 
    205 	public AndroidLiveWallpaper getLiveWallpaper () {
    206 		return app;
    207 	}
    208 
    209 	public WindowManager getWindowManager () {
    210 		return (WindowManager)getSystemService(Context.WINDOW_SERVICE);
    211 	}
    212 
    213 	/** Bridge between surface on which wallpaper is rendered and the wallpaper service. The problem is that there can be a group of
    214 	 * Engines at one time and we must share libGDX application between them.
    215 	 *
    216 	 * @author libGDX team and Jaroslaw Wisniewski <j.wisniewski (at) appsisle.com> */
    217 	public class AndroidWallpaperEngine extends Engine {
    218 
    219 		protected boolean engineIsVisible = false;
    220 
    221 		// destination format of surface when this engine is active (updated in onSurfaceChanged)
    222 		protected int engineFormat;
    223 		protected int engineWidth;
    224 		protected int engineHeight;
    225 
    226 		// lifecycle methods - the order of calling (flow) is maintained /////////////////
    227 
    228 		public AndroidWallpaperEngine () {
    229 			if (DEBUG) Log.d(TAG, " > AndroidWallpaperEngine() " + hashCode());
    230 		}
    231 
    232 		@Override
    233 		public void onCreate (final SurfaceHolder surfaceHolder) {
    234 			if (DEBUG)
    235 				Log.d(TAG, " > AndroidWallpaperEngine - onCreate() " + hashCode() + " running: " + engines + ", linked: "
    236 					+ (linkedEngine == this) + ", thread: " + Thread.currentThread().toString());
    237 			super.onCreate(surfaceHolder);
    238 		}
    239 
    240 		/** Called before surface holder callbacks (ex for GLSurfaceView)! This is called immediately after the surface is first
    241 		 * created. Implementations of this should start up whatever rendering code they desire. Note that only one thread can ever
    242 		 * draw into a Surface, so you should not draw into the Surface here if your normal rendering will be in another thread. */
    243 		@Override
    244 		public void onSurfaceCreated (final SurfaceHolder holder) {
    245 			engines++;
    246 			setLinkedEngine(this);
    247 
    248 			if (DEBUG)
    249 				Log.d(TAG, " > AndroidWallpaperEngine - onSurfaceCreated() " + hashCode() + ", running: " + engines + ", linked: "
    250 					+ (linkedEngine == this));
    251 			Log.i(TAG, "engine surface created");
    252 
    253 			super.onSurfaceCreated(holder);
    254 
    255 			if (engines == 1) {
    256 				// safeguard: recover attributes that could suffered by unexpected surfaceDestroy event
    257 				visibleEngines = 0;
    258 			}
    259 
    260 			if (engines == 1 && app == null) {
    261 				viewFormat = 0; // must be initialized with zeroes
    262 				viewWidth = 0;
    263 				viewHeight = 0;
    264 
    265 				app = new AndroidLiveWallpaper(AndroidLiveWallpaperService.this);
    266 
    267 				onCreateApplication();
    268 				if (app.graphics == null)
    269 					throw new Error(
    270 						"You must override 'AndroidLiveWallpaperService.onCreateApplication' method and call 'initialize' from its body.");
    271 			}
    272 
    273 			view = (SurfaceHolder.Callback)app.graphics.view;
    274 			this.getSurfaceHolder().removeCallback(view); // we are going to call this events manually
    275 
    276 			// inherit format from shared surface view
    277 			engineFormat = viewFormat;
    278 			engineWidth = viewWidth;
    279 			engineHeight = viewHeight;
    280 
    281 			if (engines == 1) {
    282 				view.surfaceCreated(holder);
    283 			} else {
    284 				// this combination of methods is described in AndroidWallpaperEngine.onResume
    285 				view.surfaceDestroyed(holder);
    286 				notifySurfaceChanged(engineFormat, engineWidth, engineHeight, false);
    287 				view.surfaceCreated(holder);
    288 			}
    289 
    290 			notifyPreviewState();
    291 			notifyOffsetsChanged();
    292 			if (!Gdx.graphics.isContinuousRendering()) {
    293 				Gdx.graphics.requestRendering();
    294 			}
    295 		}
    296 
    297 		/** This is called immediately after any structural changes (format or size) have been made to the surface. You should at
    298 		 * this point update the imagery in the surface. This method is always called at least once, after
    299 		 * surfaceCreated(SurfaceHolder). */
    300 		@Override
    301 		public void onSurfaceChanged (final SurfaceHolder holder, final int format, final int width, final int height) {
    302 			if (DEBUG)
    303 				Log.d(TAG, " > AndroidWallpaperEngine - onSurfaceChanged() isPreview: " + isPreview() + ", " + hashCode()
    304 					+ ", running: " + engines + ", linked: " + (linkedEngine == this) + ", sufcace valid: "
    305 					+ getSurfaceHolder().getSurface().isValid());
    306 			Log.i(TAG, "engine surface changed");
    307 
    308 			super.onSurfaceChanged(holder, format, width, height);
    309 
    310 			notifySurfaceChanged(format, width, height, true);
    311 
    312 			// it shouldn't be required there (as I understand android.service.wallpaper.WallpaperService impl)
    313 			// notifyPreviewState();
    314 		}
    315 
    316 		/** Notifies shared GLSurfaceView about changed surface format.
    317 		 * @param format
    318 		 * @param width
    319 		 * @param height
    320 		 * @param forceUpdate if false, surface view will be notified only if currently contains expired information */
    321 		private void notifySurfaceChanged (final int format, final int width, final int height, boolean forceUpdate) {
    322 			if (!forceUpdate && format == viewFormat && width == viewWidth && height == viewHeight) {
    323 				// skip if didn't changed
    324 				if (DEBUG) Log.d(TAG, " > surface is current, skipping surfaceChanged event");
    325 			} else {
    326 				// update engine desired surface format
    327 				engineFormat = format;
    328 				engineWidth = width;
    329 				engineHeight = height;
    330 
    331 				// update surface view if engine is linked with it already
    332 				if (linkedEngine == this) {
    333 					viewFormat = engineFormat;
    334 					viewWidth = engineWidth;
    335 					viewHeight = engineHeight;
    336 					view.surfaceChanged(this.getSurfaceHolder(), viewFormat, viewWidth, viewHeight);
    337 				} else {
    338 					if (DEBUG) Log.d(TAG, " > engine is not active, skipping surfaceChanged event");
    339 				}
    340 			}
    341 		}
    342 
    343 		/** Called to inform you of the wallpaper becoming visible or hidden. It is very important that a wallpaper only use CPU
    344 		 * while it is visible.. */
    345 		@Override
    346 		public void onVisibilityChanged (final boolean visible) {
    347 			boolean reportedVisible = isVisible();
    348 
    349 			if (DEBUG)
    350 				Log.d(TAG, " > AndroidWallpaperEngine - onVisibilityChanged(paramVisible: " + visible + " reportedVisible: "
    351 					+ reportedVisible + ") " + hashCode() + ", sufcace valid: " + getSurfaceHolder().getSurface().isValid());
    352 			super.onVisibilityChanged(visible);
    353 
    354 			// Android WallpaperService sends fake visibility changed events to force some buggy live wallpapers to shut down after
    355 // onSurfaceChanged when they aren't visible, it can cause problems in current implementation and it is not necessary
    356 			if (reportedVisible == false && visible == true) {
    357 				if (DEBUG) Log.d(TAG, " > fake visibilityChanged event! Android WallpaperService likes do that!");
    358 				return;
    359 			}
    360 
    361 			notifyVisibilityChanged(visible);
    362 		}
    363 
    364 		private void notifyVisibilityChanged (final boolean visible) {
    365 			if (this.engineIsVisible != visible) {
    366 				this.engineIsVisible = visible;
    367 
    368 				if (this.engineIsVisible)
    369 					onResume();
    370 				else
    371 					onPause();
    372 			} else {
    373 				if (DEBUG) Log.d(TAG, " > visible state is current, skipping visibilityChanged event!");
    374 			}
    375 		}
    376 
    377 		public void onResume () {
    378 			visibleEngines++;
    379 			if (DEBUG)
    380 				Log.d(TAG, " > AndroidWallpaperEngine - onResume() " + hashCode() + ", running: " + engines + ", linked: "
    381 					+ (linkedEngine == this) + ", visible: " + visibleEngines);
    382 			Log.i(TAG, "engine resumed");
    383 
    384 			if (linkedEngine != null) {
    385 				if (linkedEngine != this) {
    386 					setLinkedEngine(this);
    387 
    388 					// disconnect surface view from previous window
    389 					view.surfaceDestroyed(this.getSurfaceHolder()); // force gl surface reload, new instance will be created on current
    390 // surface holder
    391 
    392 					// resize surface to match window associated with current engine
    393 					notifySurfaceChanged(engineFormat, engineWidth, engineHeight, false);
    394 
    395 					// connect surface view to current engine
    396 					view.surfaceCreated(this.getSurfaceHolder());
    397 				} else {
    398 					// update if surface changed when engine wasn't active
    399 					notifySurfaceChanged(engineFormat, engineWidth, engineHeight, false);
    400 				}
    401 
    402 				if (visibleEngines == 1) app.onResume();
    403 
    404 				notifyPreviewState();
    405 				notifyOffsetsChanged();
    406 				if (!Gdx.graphics.isContinuousRendering()) {
    407 					Gdx.graphics.requestRendering();
    408 				}
    409 			}
    410 		}
    411 
    412 		public void onPause () {
    413 			visibleEngines--;
    414 			if (DEBUG)
    415 				Log.d(TAG, " > AndroidWallpaperEngine - onPause() " + hashCode() + ", running: " + engines + ", linked: "
    416 					+ (linkedEngine == this) + ", visible: " + visibleEngines);
    417 			Log.i(TAG, "engine paused");
    418 
    419 			// this shouldn't never happen, but if it will.. live wallpaper will not be stopped when device will pause and lwp will
    420 // drain battery.. shortly!
    421 			if (visibleEngines >= engines) {
    422 				Log.e(AndroidLiveWallpaperService.TAG, "wallpaper lifecycle error, counted too many visible engines! repairing..");
    423 				visibleEngines = Math.max(engines - 1, 0);
    424 			}
    425 
    426 			if (linkedEngine != null) {
    427 				if (visibleEngines == 0) app.onPause();
    428 			}
    429 
    430 			if (DEBUG) Log.d(TAG, " > AndroidWallpaperEngine - onPause() done!");
    431 		}
    432 
    433 		/** Called after surface holder callbacks (ex for GLSurfaceView)! This is called immediately before a surface is being
    434 		 * destroyed. After returning from this call, you should no longer try to access this surface. If you have a rendering
    435 		 * thread that directly accesses the surface, you must ensure that thread is no longer touching the Surface before returning
    436 		 * from this function.
    437 		 *
    438 		 * Attention! In some cases GL context may be shutdown right now! and SurfaceHolder.Surface.isVaild = false */
    439 		@Override
    440 		public void onSurfaceDestroyed (final SurfaceHolder holder) {
    441 			engines--;
    442 			if (DEBUG)
    443 				Log.d(TAG, " > AndroidWallpaperEngine - onSurfaceDestroyed() " + hashCode() + ", running: " + engines + " ,linked: "
    444 					+ (linkedEngine == this) + ", isVisible: " + engineIsVisible);
    445 			Log.i(TAG, "engine surface destroyed");
    446 
    447 			// application can be in resumed state at this moment if app surface had been lost just after it was created (wallpaper
    448 // selected too fast from preview mode etc)
    449 			// it is too late probably - calling on pause causes deadlock
    450 			// notifyVisibilityChanged(false);
    451 
    452 			// it is too late to call app.onDispose, just free native resources
    453 			if (engines == 0) onDeepPauseApplication();
    454 
    455 			// free surface if it belongs to this engine and if it was initialized
    456 			if (linkedEngine == this && view != null) view.surfaceDestroyed(holder);
    457 
    458 			// waitingSurfaceChangedEvent = null;
    459 			engineFormat = 0;
    460 			engineWidth = 0;
    461 			engineHeight = 0;
    462 
    463 			// safeguard for other engine callbacks
    464 			if (engines == 0) linkedEngine = null;
    465 
    466 			super.onSurfaceDestroyed(holder);
    467 		}
    468 
    469 		@Override
    470 		public void onDestroy () {
    471 			super.onDestroy();
    472 		}
    473 
    474 		// end of lifecycle methods ////////////////////////////////////////////////////////
    475 
    476 		// input
    477 
    478 		@Override
    479 		public Bundle onCommand (final String pAction, final int pX, final int pY, final int pZ, final Bundle pExtras,
    480 			final boolean pResultRequested) {
    481 			if (DEBUG)
    482 				Log.d(TAG, " > AndroidWallpaperEngine - onCommand(" + pAction + " " + pX + " " + pY + " " + pZ + " " + pExtras + " "
    483 					+ pResultRequested + ")" + ", linked: " + (linkedEngine == this));
    484 
    485 			return super.onCommand(pAction, pX, pY, pZ, pExtras, pResultRequested);
    486 		}
    487 
    488 		@Override
    489 		public void onTouchEvent (MotionEvent event) {
    490 			if (linkedEngine == this) {
    491 				app.input.onTouch(null, event);
    492 			}
    493 		}
    494 
    495 		// offsets from last onOffsetsChanged
    496 		boolean offsetsConsumed = true;
    497 		float xOffset = 0.0f;
    498 		float yOffset = 0.0f;
    499 		float xOffsetStep = 0.0f;
    500 		float yOffsetStep = 0.0f;
    501 		int xPixelOffset = 0;
    502 		int yPixelOffset = 0;
    503 
    504 		@Override
    505 		public void onOffsetsChanged (final float xOffset, final float yOffset, final float xOffsetStep, final float yOffsetStep,
    506 			final int xPixelOffset, final int yPixelOffset) {
    507 
    508 			// it spawns too frequent on some devices - its annoying!
    509 			// if (DEBUG)
    510 			// Log.d(TAG, " > AndroidWallpaperEngine - onOffsetChanged(" + xOffset + " " + yOffset + " " + xOffsetStep + " "
    511 			// + yOffsetStep + " " + xPixelOffset + " " + yPixelOffset + ") " + hashCode() + ", linkedApp: " + (linkedApp != null));
    512 
    513 			this.offsetsConsumed = false;
    514 			this.xOffset = xOffset;
    515 			this.yOffset = yOffset;
    516 			this.xOffsetStep = xOffsetStep;
    517 			this.yOffsetStep = yOffsetStep;
    518 			this.xPixelOffset = xPixelOffset;
    519 			this.yPixelOffset = yPixelOffset;
    520 
    521 			// can fail if linkedApp == null, so we repeat it in Engine.onResume
    522 			notifyOffsetsChanged();
    523 			if (!Gdx.graphics.isContinuousRendering()) {
    524 				Gdx.graphics.requestRendering();
    525 			}
    526 
    527 			super.onOffsetsChanged(xOffset, yOffset, xOffsetStep, yOffsetStep, xPixelOffset, yPixelOffset);
    528 		}
    529 
    530 		protected void notifyOffsetsChanged () {
    531 			if (linkedEngine == this && app.listener instanceof AndroidWallpaperListener) {
    532 				if (!offsetsConsumed) { // no need for more sophisticated synchronization - offsetsChanged can be called multiple
    533 // times and with various patterns on various devices - user application must be prepared for that
    534 					offsetsConsumed = true;
    535 
    536 					app.postRunnable(new Runnable() {
    537 						@Override
    538 						public void run () {
    539 							boolean isCurrent = false;
    540 							synchronized (sync) {
    541 								isCurrent = (linkedEngine == AndroidWallpaperEngine.this); // without this app can crash when fast
    542 // switching between engines (tested!)
    543 							}
    544 							if (isCurrent)
    545 								((AndroidWallpaperListener)app.listener).offsetChange(xOffset, yOffset, xOffsetStep, yOffsetStep,
    546 									xPixelOffset, yPixelOffset);
    547 						}
    548 					});
    549 				}
    550 			}
    551 		}
    552 
    553 		protected void notifyPreviewState () {
    554 			// notify preview state to app listener
    555 			if (linkedEngine == this && app.listener instanceof AndroidWallpaperListener) {
    556 				final boolean currentPreviewState = linkedEngine.isPreview();
    557 				app.postRunnable(new Runnable() {
    558 					@Override
    559 					public void run () {
    560 						boolean shouldNotify = false;
    561 						synchronized (sync) {
    562 							if (!isPreviewNotified || notifiedPreviewState != currentPreviewState) {
    563 								notifiedPreviewState = currentPreviewState;
    564 								isPreviewNotified = true;
    565 								shouldNotify = true;
    566 							}
    567 						}
    568 
    569 						if (shouldNotify) {
    570 							AndroidLiveWallpaper currentApp = app; // without this app can crash when fast switching between engines
    571 // (tested!)
    572 							if (currentApp != null)
    573 								((AndroidWallpaperListener)currentApp.listener).previewStateChange(currentPreviewState);
    574 						}
    575 					}
    576 				});
    577 			}
    578 		}
    579 	}
    580 }
    581