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