com.badlogic.gdx.backends.android.AndroidLiveWallpaperService.java Source code

Java tutorial

Introduction

Here is the source code for com.badlogic.gdx.backends.android.AndroidLiveWallpaperService.java

Source

/*
 * Copyright 2013 Jaroslaw Wisniewski <j.wisniewski@appsisle.com>
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
 * License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS"
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 *
 */

package com.badlogic.gdx.backends.android;

import android.content.Context;
import android.os.Bundle;
import android.service.wallpaper.WallpaperService;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.WindowManager;

import com.badlogic.gdx.Application;
import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Graphics;
import com.badlogic.gdx.utils.GdxNativesLoader;

/** An implementation of the {@link Application} interface dedicated for android live wallpapers.
 * 
 * Derive from this class. In the {@link AndroidLiveWallpaperService#onCreateApplication} method call the
 * {@link AndroidLiveWallpaperService#initialize(ApplicationListener)} method specifying the configuration for the GLSurfaceView.
 * You can also use {@link AndroidWallpaperListener} along with {@link ApplicationListener} to respond for wallpaper specific
 * events in your app listener:
 * 
 * MyAppListener implements ApplicationListener, AndroidWallpaperListener
 * 
 * Notice: Following methods are not called for live wallpapers: {@link ApplicationListener#pause()}
 * {@link ApplicationListener#dispose()} TODO add callbacks to AndroidWallpaperListener allowing to notify app listener about
 * changed visibility state of live wallpaper but called from main thread, not from GL thread: for example:
 * AndroidWallpaperListener.visibilityChanged(boolean)
 * 
 * //obsoleted: //Notice! //You have to kill all not daemon threads you created in {@link ApplicationListener#pause()} method. //
 * {@link ApplicationListener#dispose()} is never called! //If you leave live non daemon threads, wallpaper service wouldn't be
 * able to close, //this can cause problems with wallpaper lifecycle.
 * 
 * Notice #2! On some devices wallpaper service is not killed immediately after exiting from preview. Service object is destroyed
 * (onDestroy called) but process on which it runs remains alive. When user comes back to wallpaper preview, new wallpaper service
 * object is created, but in the same process. It is important if you plan to use static variables / objects - they will be shared
 * between living instances of wallpaper services'! And depending on your implementation - it can cause problems you were not
 * prepared to.
 * 
 * @author Jaroslaw Wisniewski <j.wisniewski@appsisle.com> */
public abstract class AndroidLiveWallpaperService extends WallpaperService {
    static {
        GdxNativesLoader.load();
    }

    static final String TAG = "WallpaperService";
    static boolean DEBUG = false; // TODO remember to disable this

    // instance of libGDX Application, acts as singleton - one instance per application (per WallpaperService)
    protected volatile AndroidLiveWallpaper app = null; // can be accessed from GL render thread
    protected SurfaceHolder.Callback view = null;

    // current format of surface (one GLSurfaceView is shared between all engines)
    protected int viewFormat;
    protected int viewWidth;
    protected int viewHeight;

    // app is initialized when engines == 1 first time, app is destroyed in WallpaperService.onDestroy, but
    // ApplicationListener.dispose is not called for wallpapers
    protected int engines = 0;
    protected int visibleEngines = 0;

    // engine currently associated with app instance, linked engine serves surface handler for GLSurfaceView
    protected volatile AndroidWallpaperEngine linkedEngine = null; // can be accessed from GL render thread by getSurfaceHolder

    protected void setLinkedEngine(AndroidWallpaperEngine linkedEngine) {
        synchronized (sync) {
            this.linkedEngine = linkedEngine;
        }
    }

    // if preview state notified ever
    protected volatile boolean isPreviewNotified = false;

    // the value of last preview state notified to app listener
    protected volatile boolean notifiedPreviewState = false;

    volatile int[] sync = new int[0];

    // volatile ReentrantLock lock = new ReentrantLock();

    // lifecycle methods - the order of calling (flow) is maintained ///////////////

    public AndroidLiveWallpaperService() {
        super();
    }

    /** Service is starting, libGDX application is shutdown now */
    @Override
    public void onCreate() {
        if (DEBUG)
            Log.d(TAG, " > AndroidLiveWallpaperService - onCreate() " + hashCode());
        Log.i(TAG, "service created");

        super.onCreate();
    }

    /** One of wallpaper engines is starting. Do not override this method, service manages them internally. */
    @Override
    public Engine onCreateEngine() {
        if (DEBUG)
            Log.d(TAG, " > AndroidLiveWallpaperService - onCreateEngine()");
        Log.i(TAG, "engine created");

        return new AndroidWallpaperEngine();
    }

    /** libGDX application is starting, it occurs after first wallpaper engine had started. Override this method an invoke
     * {@link AndroidLiveWallpaperService#initialize(ApplicationListener, AndroidApplicationConfiguration)} from there. */
    public void onCreateApplication() {
        if (DEBUG)
            Log.d(TAG, " > AndroidLiveWallpaperService - onCreateApplication()");
    }

    /** Look at {@link AndroidLiveWallpaperService#initialize(ApplicationListener, AndroidApplicationConfiguration)}
     * @param listener */
    public void initialize(ApplicationListener listener) {
        AndroidApplicationConfiguration config = new AndroidApplicationConfiguration();
        initialize(listener, config);
    }

    /** This method has to be called in the {@link AndroidLiveWallpaperService#onCreateApplication} method. It sets up all the
     * things necessary to get input, render via OpenGL and so on. You can configure other aspects of the application with the rest
     * of the fields in the {@link AndroidApplicationConfiguration} instance.
     * 
     * @param listener the {@link ApplicationListener} implementing the program logic
     * @param config the {@link AndroidApplicationConfiguration}, defining various settings of the application (use accelerometer,
     *           etc.). Do not change contents of this object after passing to this method! */
    public void initialize(ApplicationListener listener, AndroidApplicationConfiguration config) {
        if (DEBUG)
            Log.d(TAG, " > AndroidLiveWallpaperService - initialize()");

        app.initialize(listener, config);

        if (config.getTouchEventsForLiveWallpaper && Integer.parseInt(android.os.Build.VERSION.SDK) >= 7)
            linkedEngine.setTouchEventsEnabled(true);

        // onResume(); do not call it there
    }

    /** Getter for SurfaceHolder object, surface holder is required to restore gl context in GLSurfaceView */
    public SurfaceHolder getSurfaceHolder() {
        if (DEBUG)
            Log.d(TAG, " > AndroidLiveWallpaperService - getSurfaceHolder()");

        synchronized (sync) {
            if (linkedEngine == null)
                return null;
            else
                return linkedEngine.getSurfaceHolder();
        }
    }

    // engines live there

    /** Called when the last engine is ending its live, it can occur when: 1. service is dying 2. service is switching from one
     * engine to another 3. [only my assumption] when wallpaper is not visible and system is going to restore some memory for
     * foreground processing by disposing not used wallpaper engine We can't destroy app there, because: 1. in won't work - gl
     * context is disposed right now and after app.onDestroy() app would stuck somewhere in gl thread synchronizing code 2. we
     * don't know if service create more engines, app is shared between them and should stay initialized waiting for new engines */
    public void onDeepPauseApplication() {
        if (DEBUG)
            Log.d(TAG, " > AndroidLiveWallpaperService - onDeepPauseApplication()");

        // free native resources consuming runtime memory, note that it can cause some lag when resuming wallpaper
        if (app != null) {
            app.graphics.clearManagedCaches();
        }
    }

    /** Service is dying, and will not be used again. You have to finish execution off all living threads there or short after
     * there, besides the new wallpaper service wouldn't be able to start. */
    @Override
    public void onDestroy() {
        if (DEBUG)
            Log.d(TAG, " > AndroidLiveWallpaperService - onDestroy() " + hashCode());
        Log.i(TAG, "service destroyed");

        super.onDestroy(); // can call engine.onSurfaceDestroyed, must be before bellow code:

        if (app != null) {
            app.onDestroy();

            app = null;
            view = null;
        }
    }

    @Override
    protected void finalize() throws Throwable {
        Log.i(TAG, "service finalized");
        super.finalize();
    }

    // end of lifecycle methods ////////////////////////////////////////////////////////

    public AndroidLiveWallpaper getLiveWallpaper() {
        return app;
    }

    public WindowManager getWindowManager() {
        return (WindowManager) getSystemService(Context.WINDOW_SERVICE);
    }

    /** Bridge between surface on which wallpaper is rendered and the wallpaper service. The problem is that there can be a group of
     * Engines at one time and we must share libGDX application between them.
     * 
     * @author libGDX team and Jaroslaw Wisniewski <j.wisniewski@appsisle.com> */
    public class AndroidWallpaperEngine extends Engine {

        protected boolean engineIsVisible = false;

        // destination format of surface when this engine is active (updated in onSurfaceChanged)
        protected int engineFormat;
        protected int engineWidth;
        protected int engineHeight;

        // lifecycle methods - the order of calling (flow) is maintained /////////////////

        public AndroidWallpaperEngine() {
            if (DEBUG)
                Log.d(TAG, " > AndroidWallpaperEngine() " + hashCode());
        }

        @Override
        public void onCreate(final SurfaceHolder surfaceHolder) {
            if (DEBUG)
                Log.d(TAG, " > AndroidWallpaperEngine - onCreate() " + hashCode() + " running: " + engines
                        + ", linked: " + (linkedEngine == this) + ", thread: " + Thread.currentThread().toString());
            super.onCreate(surfaceHolder);
        }

        /** Called before surface holder callbacks (ex for GLSurfaceView)! This is called immediately after the surface is first
         * created. Implementations of this should start up whatever rendering code they desire. Note that only one thread can ever
         * draw into a Surface, so you should not draw into the Surface here if your normal rendering will be in another thread. */
        @Override
        public void onSurfaceCreated(final SurfaceHolder holder) {
            engines++;
            setLinkedEngine(this);

            if (DEBUG)
                Log.d(TAG, " > AndroidWallpaperEngine - onSurfaceCreated() " + hashCode() + ", running: " + engines
                        + ", linked: " + (linkedEngine == this));
            Log.i(TAG, "engine surface created");

            super.onSurfaceCreated(holder);

            if (engines == 1) {
                // safeguard: recover attributes that could suffered by unexpected surfaceDestroy event
                visibleEngines = 0;
            }

            if (engines == 1 && app == null) {
                viewFormat = 0; // must be initialized with zeroes
                viewWidth = 0;
                viewHeight = 0;

                app = new AndroidLiveWallpaper(AndroidLiveWallpaperService.this);

                onCreateApplication();
                if (app.graphics == null)
                    throw new Error(
                            "You must override 'AndroidLiveWallpaperService.onCreateApplication' method and call 'initialize' from its body.");
            }

            view = (SurfaceHolder.Callback) app.graphics.view;
            this.getSurfaceHolder().removeCallback(view); // we are going to call this events manually

            // inherit format from shared surface view
            engineFormat = viewFormat;
            engineWidth = viewWidth;
            engineHeight = viewHeight;

            if (engines == 1) {
                view.surfaceCreated(holder);
            } else {
                // this combination of methods is described in AndroidWallpaperEngine.onResume
                view.surfaceDestroyed(holder);
                notifySurfaceChanged(engineFormat, engineWidth, engineHeight, false);
                view.surfaceCreated(holder);
            }

            notifyPreviewState();
            notifyOffsetsChanged();
            if (!Gdx.graphics.isContinuousRendering()) {
                Gdx.graphics.requestRendering();
            }
        }

        /** This is called immediately after any structural changes (format or size) have been made to the surface. You should at
         * this point update the imagery in the surface. This method is always called at least once, after
         * surfaceCreated(SurfaceHolder). */
        @Override
        public void onSurfaceChanged(final SurfaceHolder holder, final int format, final int width,
                final int height) {
            if (DEBUG)
                Log.d(TAG,
                        " > AndroidWallpaperEngine - onSurfaceChanged() isPreview: " + isPreview() + ", "
                                + hashCode() + ", running: " + engines + ", linked: " + (linkedEngine == this)
                                + ", sufcace valid: " + getSurfaceHolder().getSurface().isValid());
            Log.i(TAG, "engine surface changed");

            super.onSurfaceChanged(holder, format, width, height);

            notifySurfaceChanged(format, width, height, true);

            // it shouldn't be required there (as I understand android.service.wallpaper.WallpaperService impl)
            // notifyPreviewState();
        }

        /** Notifies shared GLSurfaceView about changed surface format.
         * @param format
         * @param width
         * @param height
         * @param forceUpdate if false, surface view will be notified only if currently contains expired information */
        private void notifySurfaceChanged(final int format, final int width, final int height,
                boolean forceUpdate) {
            if (!forceUpdate && format == viewFormat && width == viewWidth && height == viewHeight) {
                // skip if didn't changed
                if (DEBUG)
                    Log.d(TAG, " > surface is current, skipping surfaceChanged event");
            } else {
                // update engine desired surface format
                engineFormat = format;
                engineWidth = width;
                engineHeight = height;

                // update surface view if engine is linked with it already
                if (linkedEngine == this) {
                    viewFormat = engineFormat;
                    viewWidth = engineWidth;
                    viewHeight = engineHeight;
                    view.surfaceChanged(this.getSurfaceHolder(), viewFormat, viewWidth, viewHeight);
                } else {
                    if (DEBUG)
                        Log.d(TAG, " > engine is not active, skipping surfaceChanged event");
                }
            }
        }

        /** Called to inform you of the wallpaper becoming visible or hidden. It is very important that a wallpaper only use CPU
         * while it is visible.. */
        @Override
        public void onVisibilityChanged(final boolean visible) {
            boolean reportedVisible = isVisible();

            if (DEBUG)
                Log.d(TAG,
                        " > AndroidWallpaperEngine - onVisibilityChanged(paramVisible: " + visible
                                + " reportedVisible: " + reportedVisible + ") " + hashCode() + ", sufcace valid: "
                                + getSurfaceHolder().getSurface().isValid());
            super.onVisibilityChanged(visible);

            // 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
            if (reportedVisible == false && visible == true) {
                if (DEBUG)
                    Log.d(TAG, " > fake visibilityChanged event! Android WallpaperService likes do that!");
                return;
            }

            notifyVisibilityChanged(visible);
        }

        private void notifyVisibilityChanged(final boolean visible) {
            if (this.engineIsVisible != visible) {
                this.engineIsVisible = visible;

                if (this.engineIsVisible)
                    onResume();
                else
                    onPause();
            } else {
                if (DEBUG)
                    Log.d(TAG, " > visible state is current, skipping visibilityChanged event!");
            }
        }

        public void onResume() {
            visibleEngines++;
            if (DEBUG)
                Log.d(TAG, " > AndroidWallpaperEngine - onResume() " + hashCode() + ", running: " + engines
                        + ", linked: " + (linkedEngine == this) + ", visible: " + visibleEngines);
            Log.i(TAG, "engine resumed");

            if (linkedEngine != null) {
                if (linkedEngine != this) {
                    setLinkedEngine(this);

                    // disconnect surface view from previous window
                    view.surfaceDestroyed(this.getSurfaceHolder()); // force gl surface reload, new instance will be created on current
                    // surface holder

                    // resize surface to match window associated with current engine
                    notifySurfaceChanged(engineFormat, engineWidth, engineHeight, false);

                    // connect surface view to current engine
                    view.surfaceCreated(this.getSurfaceHolder());
                } else {
                    // update if surface changed when engine wasn't active
                    notifySurfaceChanged(engineFormat, engineWidth, engineHeight, false);
                }

                if (visibleEngines == 1)
                    app.onResume();

                notifyPreviewState();
                notifyOffsetsChanged();
                if (!Gdx.graphics.isContinuousRendering()) {
                    Gdx.graphics.requestRendering();
                }
            }
        }

        public void onPause() {
            visibleEngines--;
            if (DEBUG)
                Log.d(TAG, " > AndroidWallpaperEngine - onPause() " + hashCode() + ", running: " + engines
                        + ", linked: " + (linkedEngine == this) + ", visible: " + visibleEngines);
            Log.i(TAG, "engine paused");

            // 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!
            if (visibleEngines >= engines) {
                Log.e(AndroidLiveWallpaperService.TAG,
                        "wallpaper lifecycle error, counted too many visible engines! repairing..");
                visibleEngines = Math.max(engines - 1, 0);
            }

            if (linkedEngine != null) {
                if (visibleEngines == 0)
                    app.onPause();
            }

            if (DEBUG)
                Log.d(TAG, " > AndroidWallpaperEngine - onPause() done!");
        }

        /** Called after surface holder callbacks (ex for GLSurfaceView)! This is called immediately before a surface is being
         * destroyed. After returning from this call, you should no longer try to access this surface. If you have a rendering
         * thread that directly accesses the surface, you must ensure that thread is no longer touching the Surface before returning
         * from this function.
         * 
         * Attention! In some cases GL context may be shutdown right now! and SurfaceHolder.Surface.isVaild = false */
        @Override
        public void onSurfaceDestroyed(final SurfaceHolder holder) {
            engines--;
            if (DEBUG)
                Log.d(TAG, " > AndroidWallpaperEngine - onSurfaceDestroyed() " + hashCode() + ", running: "
                        + engines + " ,linked: " + (linkedEngine == this) + ", isVisible: " + engineIsVisible);
            Log.i(TAG, "engine surface destroyed");

            // 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)
            // it is too late probably - calling on pause causes deadlock
            // notifyVisibilityChanged(false);

            // it is too late to call app.onDispose, just free native resources
            if (engines == 0)
                onDeepPauseApplication();

            // free surface if it belongs to this engine and if it was initialized
            if (linkedEngine == this && view != null)
                view.surfaceDestroyed(holder);

            // waitingSurfaceChangedEvent = null;
            engineFormat = 0;
            engineWidth = 0;
            engineHeight = 0;

            // safeguard for other engine callbacks
            if (engines == 0)
                linkedEngine = null;

            super.onSurfaceDestroyed(holder);
        }

        @Override
        public void onDestroy() {
            super.onDestroy();
        }

        // end of lifecycle methods ////////////////////////////////////////////////////////

        // input

        @Override
        public Bundle onCommand(final String pAction, final int pX, final int pY, final int pZ,
                final Bundle pExtras, final boolean pResultRequested) {
            if (DEBUG)
                Log.d(TAG, " > AndroidWallpaperEngine - onCommand(" + pAction + " " + pX + " " + pY + " " + pZ + " "
                        + pExtras + " " + pResultRequested + ")" + ", linked: " + (linkedEngine == this));

            return super.onCommand(pAction, pX, pY, pZ, pExtras, pResultRequested);
        }

        @Override
        public void onTouchEvent(MotionEvent event) {
            if (linkedEngine == this) {
                app.input.onTouch(null, event);
            }
        }

        // offsets from last onOffsetsChanged
        boolean offsetsConsumed = true;
        float xOffset = 0.0f;
        float yOffset = 0.0f;
        float xOffsetStep = 0.0f;
        float yOffsetStep = 0.0f;
        int xPixelOffset = 0;
        int yPixelOffset = 0;

        @Override
        public void onOffsetsChanged(final float xOffset, final float yOffset, final float xOffsetStep,
                final float yOffsetStep, final int xPixelOffset, final int yPixelOffset) {

            // it spawns too frequent on some devices - its annoying!
            // if (DEBUG)
            // Log.d(TAG, " > AndroidWallpaperEngine - onOffsetChanged(" + xOffset + " " + yOffset + " " + xOffsetStep + " "
            // + yOffsetStep + " " + xPixelOffset + " " + yPixelOffset + ") " + hashCode() + ", linkedApp: " + (linkedApp != null));

            this.offsetsConsumed = false;
            this.xOffset = xOffset;
            this.yOffset = yOffset;
            this.xOffsetStep = xOffsetStep;
            this.yOffsetStep = yOffsetStep;
            this.xPixelOffset = xPixelOffset;
            this.yPixelOffset = yPixelOffset;

            // can fail if linkedApp == null, so we repeat it in Engine.onResume
            notifyOffsetsChanged();
            if (!Gdx.graphics.isContinuousRendering()) {
                Gdx.graphics.requestRendering();
            }

            super.onOffsetsChanged(xOffset, yOffset, xOffsetStep, yOffsetStep, xPixelOffset, yPixelOffset);
        }

        protected void notifyOffsetsChanged() {
            if (linkedEngine == this && app.listener instanceof AndroidWallpaperListener) {
                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
                    offsetsConsumed = true;

                    app.postRunnable(new Runnable() {
                        @Override
                        public void run() {
                            boolean isCurrent = false;
                            synchronized (sync) {
                                isCurrent = (linkedEngine == AndroidWallpaperEngine.this); // without this app can crash when fast
                                // switching between engines (tested!)
                            }
                            if (isCurrent)
                                ((AndroidWallpaperListener) app.listener).offsetChange(xOffset, yOffset,
                                        xOffsetStep, yOffsetStep, xPixelOffset, yPixelOffset);
                        }
                    });
                }
            }
        }

        protected void notifyPreviewState() {
            // notify preview state to app listener
            if (linkedEngine == this && app.listener instanceof AndroidWallpaperListener) {
                final boolean currentPreviewState = linkedEngine.isPreview();
                app.postRunnable(new Runnable() {
                    @Override
                    public void run() {
                        boolean shouldNotify = false;
                        synchronized (sync) {
                            if (!isPreviewNotified || notifiedPreviewState != currentPreviewState) {
                                notifiedPreviewState = currentPreviewState;
                                isPreviewNotified = true;
                                shouldNotify = true;
                            }
                        }

                        if (shouldNotify) {
                            AndroidLiveWallpaper currentApp = app; // without this app can crash when fast switching between engines
                            // (tested!)
                            if (currentApp != null)
                                ((AndroidWallpaperListener) currentApp.listener)
                                        .previewStateChange(currentPreviewState);
                        }
                    }
                });
            }
        }
    }
}