1/*
2 * Copyright 2013 Jaroslaw Wisniewski <j.wisniewski@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
15package com.badlogic.gdx.backends.android;
16
17import android.content.Context;
18import android.os.Bundle;
19import android.service.wallpaper.WallpaperService;
20import android.util.Log;
21import android.view.MotionEvent;
22import android.view.SurfaceHolder;
23import android.view.WindowManager;
24
25import com.badlogic.gdx.Application;
26import com.badlogic.gdx.ApplicationListener;
27import com.badlogic.gdx.Gdx;
28import com.badlogic.gdx.Graphics;
29import 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@appsisle.com> */
56public 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@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