org.qeo.android.service.QeoService.java Source code

Java tutorial

Introduction

Here is the source code for org.qeo.android.service.QeoService.java

Source

/*
 * Copyright (c) 2015 - Qeo LLC
 *
 * The source code form of this Qeo Open Source Project component is subject
 * to the terms of the Clear BSD license.
 *
 * You can redistribute it and/or modify it under the terms of the Clear BSD
 * License (http://directory.fsf.org/wiki/License:ClearBSD). See LICENSE file
 * for more details.
 *
 * The Qeo Open Source Project also includes third party Open Source Software.
 * See LICENSE file for more details.
 */

package org.qeo.android.service;

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.qeo.QeoFactory;
import org.qeo.android.internal.AidlConstants;
import org.qeo.android.internal.IServiceQeoCallback;
import org.qeo.android.service.db.DBHelper;
import org.qeo.deviceregistration.DeviceRegPref;
import org.qeo.deviceregistration.QeoManagementApp;
import org.qeo.deviceregistration.service.OAuthTokenService;
import org.qeo.deviceregistration.service.RegisterService;
import org.qeo.deviceregistration.service.RemoteDeviceRegistration;
import org.qeo.deviceregistration.service.RemoteDeviceRegistrationService;
import org.qeo.exception.QeoException;
import org.qeo.internal.BaseFactory;
import org.qeo.internal.java.EntityAccessorJava;
import org.qeo.java.QeoConnectionListener;
import org.qeo.java.QeoJava;
import org.qeo.jni.NativeQeo;
import org.qeo.system.RegistrationRequest;

import android.annotation.SuppressLint;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.sqlite.SQLiteDatabase;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.MulticastLock;
import android.os.Handler;
import android.os.IBinder;
import android.os.IInterface;
import android.os.Looper;
import android.os.PowerManager;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;

/**
 * This is the main Qeo service class. The Qeo service is responsible for communicating on DDS level.
 */
public class QeoService extends Service {
    /** Tag used for QeoService logging. */
    public static final String TAG = "QeoService";

    /** Broadcast action to be sent out if security setup is complete. */
    public static final String SECURITY_SETUP_FINISHED = "org.qeo.android.security.SETUP_FINISHED";
    /** flag (boolean) added to SECURITY_SETUP_FINISHED in case of success. */
    public static final String INTENT_EXTRA_SUCCESS = "success";
    /** String added to SECURITY_SETUP_FINISHED containing the otc code. */
    public static final String INTENT_EXTRA_OTC = "otc";
    /** String added to SECURITY_SETUP_FINISHED containing the url. */
    public static final String INTENT_EXTRA_URL = "url";
    /** String added to SECURITY_SETUP_FINISHED containing error message. */
    public static final String INTENT_EXTRA_ERRORMSG = "errorMsg";
    /** Broadcast that the qeo service is destroyed. */
    public static final String ACTION_SERVICE_DESTROYED = "actionServiceDestroyed";
    private static final Logger LOG = Logger.getLogger(TAG);
    private MulticastLock mMulticastLock;
    private BaseFactory mQeoClosed;
    private BaseFactory mQeoOpen;
    private boolean mQeoReady = false;
    private boolean mQeoInitCalled = false;
    private boolean mOtpDialogCanceled = false;
    private String mLatestStatusUpdate = "";

    private final Object mQeoReadyLock = new Object();
    private Handler mHandler;
    private SQLiteDatabase mDatabase;
    private ConfigurableSettings mSettingsExternal;
    private ConfigurableSettings mSettingsInternal;
    private boolean mOpenDomainDisabled;
    private QeoConnectionListener mQeoConnectionListenerOpen;
    private QeoConnectionListener mQeoConnectionListenerClosed;
    private int mInitDoneCounter = 0;
    private int mPositiveResultCounter = 0;
    private String mQeoErrorReason = "";
    private boolean mDestroyed = false;
    private QeoServiceImpl mQeoServiceImpl;
    private QeoServiceV1 mBinderV1;
    private BroadcastReceiver mOnNetworkConnectivityChanged;
    private QeoSuspendHelper mQeoSuspendHelper;

    /**
     * Create a RemoteCallbackList to be able to detect when a remote client dies so that we can cleanup all zombie
     * datareaders and datawriters.
     *
     * http://developer.android.com/reference/android/os/RemoteCallbackList.html
     */
    private interface IRemoteCallbackDied<E extends IInterface> {
        void remoteCallbackDied(E callback, Object cookie);
    }

    /**
     * Represents the remote callback list.
     *
     * @param <E> The callback list type
     */
    private static final class RemoteCallbackListWithDiedCB<E extends IInterface> extends RemoteCallbackList<E> {
        private final IRemoteCallbackDied<E> mRemoteCallbackDied;

        public RemoteCallbackListWithDiedCB(IRemoteCallbackDied<E> remoteCallbackDied) {
            mRemoteCallbackDied = remoteCallbackDied;
        }

        @Override
        public void onCallbackDied(E callback, Object cookie) {
            super.onCallbackDied(callback, cookie);
            mRemoteCallbackDied.remoteCallbackDied(callback, cookie);
        }
    }

    private static class RemoteCallbackDied implements IRemoteCallbackDied<IServiceQeoCallback> {
        @Override
        public void remoteCallbackDied(IServiceQeoCallback iServiceQeo, Object cookie) {
            LOG.fine("Lost connection with client: " + iServiceQeo.asBinder());
            ConnectedWriter.removeWriters(iServiceQeo.asBinder());
            ConnectedReader.removeReaders(iServiceQeo.asBinder());
        }
    }

    /**
     * Check if all qeo factories needed are created.
     *
     * @param success If the native creation was success or not.
     */
    private synchronized void checkQeoReady(boolean success) {
        if (mDestroyed) {
            // Qeo is already destroyed, don't count this as positive
            success = false;
        }
        mInitDoneCounter++;
        if (success) {
            mPositiveResultCounter++;
        }
        int numNeeded = (mOpenDomainDisabled ? 1 : 2); // number of domains needed
        if (mInitDoneCounter == numNeeded) {
            if (mPositiveResultCounter == mInitDoneCounter) {
                // register receiver to monitor for network changes. Do this here since it's mandatory for DDS to be
                // started before calls from this listeners can work. Otherwise it would result in a deadlock.
                enableNetworkSettings();

                // start the Headless registration listener daemon if needed
                startHeadlessRegistrationListener();
            } else {
                mQeoInitCalled = false; // qeo init failed
            }
            if (mHandler != null) {
                mHandler.post(new Runnable() {

                    @Override
                    public void run() {
                        /* For now: only continue if ALL factories could be created */
                        if (mPositiveResultCounter == mInitDoneCounter) {
                            mQeoServiceImpl.init(mQeoClosed, mQeoOpen);

                            // Done
                            LOG.info("Qeo init completed");

                            // notify peers that we're ready
                            LOG.fine("Informing Qeo Clients of Qeo Ready == true");
                            notifyClientsReady(true);
                        } else {
                            // Failed
                            LOG.info("Qeo init failed");
                            notifyClientsReady(false);
                        }

                        LOG.fine("onQeoInitDone Runnable finished!");
                    }
                });
            }
        }
    }

    private class QeoServiceConnectionListener extends QeoConnectionListener {

        @Override
        public void onStatusUpdate(final String status, final String reason) {
            mLatestStatusUpdate = reason;
            LOG.fine("QeoService.onStatusUpdate(): [status=" + status + ", reason=" + reason + "]");
        }

        @Override
        public boolean onStartAuthentication() {
            LOG.fine("onStartAuthentication() start");

            synchronized (QeoService.this) {
                // start qeoService as a started service. This to ensure that qeoservice does not gets destroyed if
                // nobody
                // is bound anymore during registration.
                mHandler.removeCallbacks(mStopServiceAfterTimeoutRunnable);
                startService(new Intent(QeoService.this, QeoService.class));
            }

            // register broadcastreceiver to get notified of completion.
            IntentFilter iff = new IntentFilter();
            iff.addAction(QeoService.SECURITY_SETUP_FINISHED);
            LocalBroadcastManager.getInstance(getBaseContext()).registerReceiver(mOnSecurityNotice, iff);

            if (!checkClientsAuthentication()) {
                startRegisterService();
            }
            LOG.fine("onStartAuthentication() done");
            return true;
        }

        @Override
        public synchronized void onQeoReady(QeoFactory qeo) {
            BaseFactory factory = (BaseFactory) qeo;
            LOG.fine("onQeoReady start - domain: " + factory.getDomainId());

            if (factory.getDomainId() == QeoFactory.OPEN_ID) {
                mQeoOpen = factory;
            } else {
                mQeoClosed = factory;
            }

            checkQeoReady(true);
        }

        @Override
        public synchronized void onQeoError(QeoException ex) {
            LOG.log(Level.FINE, "got onQeoError", ex);
            if (ex != null) {
                mQeoErrorReason = ex.getMessage();
            } else {
                mQeoErrorReason = "";
            }
            checkQeoReady(false);
        }

        @Override
        public void onWakeUp(String typeName) {
            if (typeName == null) {
                LOG.warning("Suspending failed, sending empty wakeup");
                notifyClientsWakeUp(null);
                return;
            }

            LOG.log(Level.FINE, "got onWakeUp: " + typeName);
            if (QeoDefaults.isRemoteRegistrationListenerAvailable() && QeoManagementApp.isRealmAdmin()
                    && typeName.equals(RegistrationRequest.class.getName())) {
                // Resume because we received something on the registration request topic.
                // TODO use plain resume + delayedSuspend
                mQeoSuspendHelper.autoResume();
            } else {
                notifyClientsWakeUp(typeName);
            }
        }

        @Override
        public void onBgnsConnectionChange(boolean connected) {
            LOG.log(Level.FINE, "got onBgnsConnected: " + connected);
            notifyClientsBgnsConnected(connected);
        }
    }

    /** Callback called whenever a connection is lost. */
    private final RemoteCallbackDied mRemoteCallbackDied = new RemoteCallbackDied();

    /** The actual callback list. */
    private final RemoteCallbackListWithDiedCB<IServiceQeoCallback> mClients = new RemoteCallbackListWithDiedCB<IServiceQeoCallback>(
            mRemoteCallbackDied);

    private String getTcpServer() {
        // For use as a regular client
        // This option will disable location service and connect to QeoForwarder at the given address.
        String tcpServer;
        // Check for shared preferences of the org.qeo.android.debugconfig package.
        tcpServer = mSettingsExternal.getString("tcpServer", null);
        if (tcpServer != null && !tcpServer.isEmpty()) {
            LOG.warning("Custom DDS tcpServer: " + tcpServer);
            return tcpServer;
        } else {
            return null;
        }
    }

    private synchronized void startRegisterService() {
        if (mHandler != null) {
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    String mRealm = QeoManagementApp.getRealmName();
                    String mUserName = QeoManagementApp.getUserName();
                    String mDeviceName = QeoManagementApp.getDeviceName();

                    // Launch easy-install registration
                    Intent intent = new Intent(QeoService.this, RegisterService.class);
                    if (mRealm != null) {
                        LOG.info("Specific realm " + mRealm);
                        intent.putExtra(RegisterService.INTENT_EXTRA_REALMNAME, mRealm);
                    }
                    if (mUserName != null) {
                        LOG.info("Specific user name " + mUserName);
                        intent.putExtra(RegisterService.INTENT_EXTRA_USERNAME, mUserName);
                    }
                    if (mDeviceName != null) {
                        LOG.info("Specific device name " + mDeviceName);
                        intent.putExtra(RegisterService.INTENT_EXTRA_DEVICENAME, mDeviceName);
                    }
                    LOG.fine("Starting registerservice: " + intent);
                    startService(intent);
                }
            });
        }
    }

    private final BroadcastReceiver mOnSecurityNotice = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (action.equals(SECURITY_SETUP_FINISHED)) {
                boolean success = intent.getBooleanExtra(INTENT_EXTRA_SUCCESS, false);

                if (success) {
                    final String otc = intent.getStringExtra(INTENT_EXTRA_OTC);
                    if (otc == null || otc.isEmpty()) {
                        throw new IllegalStateException("OTC missing");
                    }
                    String url = intent.getStringExtra(INTENT_EXTRA_URL);
                    if (url == null || url.isEmpty()) {
                        url = QeoDefaults.getPublicUrl();
                    }
                    LOG.fine("OTC received, passing it to native");
                    NativeQeo.setRegistrationCredentials(otc, url);
                    RemoteDeviceRegistrationService.checkStartStop(QeoService.this);
                } else {
                    String errorMsg = intent.getStringExtra(INTENT_EXTRA_ERRORMSG);
                    mOtpDialogCanceled = false;
                    LOG.warning("Device registration failed: " + errorMsg);
                    try {
                        // close factories.
                        if (mQeoConnectionListenerClosed != null) {
                            QeoJava.closeQeo(mQeoConnectionListenerClosed);
                        }
                        if (mQeoConnectionListenerOpen != null) {
                            QeoJava.closeQeo(mQeoConnectionListenerOpen);
                        }
                    } catch (RuntimeException rte) {
                        LOG.log(Level.WARNING, "Failed to cancel registration", rte);
                    }
                    notifyClientsReady(false);
                }
            } else {
                throw new IllegalStateException("unhandled action: " + action);
            }

            synchronized (QeoService.this) {
                LOG.fine("Stopping started service after 10 sec");
                // stop started qeo service after 10 seconds. This will have no effect if somebody is bound
                mHandler.postDelayed(mStopServiceAfterTimeoutRunnable, 10000); // 10 sec
            }

        }
    };

    private final Runnable mStopServiceAfterTimeoutRunnable = new Runnable() {
        @Override
        public void run() {
            LOG.fine("Stop started qeoservice");
            stopService(new Intent(QeoService.this, QeoService.class));
        }
    };

    // disable or enable UDP
    private void configureNetworkSettings(NetworkInfo activeNetworkInfo) {
        if (activeNetworkInfo != null) {
            switch (activeNetworkInfo.getType()) {
            case ConnectivityManager.TYPE_ETHERNET:
            case ConnectivityManager.TYPE_WIFI:
                // only enable UDP on WIFI or ETHERNET
                LOG.fine("enable UDP");
                NativeQeo.setUdpMode(true);
                break;
            default:
                // disable UDP
                LOG.fine("disable UDP");
                NativeQeo.setUdpMode(false);
                break;
            }
        }
    }

    private void enableNetworkSettings() {
        LOG.fine("Enable network change monitoring");
        mOnNetworkConnectivityChanged = new BroadcastReceiver() {
            @SuppressLint("Wakelock")
            @Override
            public void onReceive(Context context, Intent intent) {

                /*
                 * Check if there is still a connected active network interface. If not, take a wakelock to make sure
                 * DDS has the time to let all of it's timers time out properly. This is especially needed when the
                 * network goes down due to the device going in sleep mode.
                 */
                ConnectivityManager connMgr = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
                NetworkInfo activeNetworkInf = connMgr.getActiveNetworkInfo();
                if (((activeNetworkInf != null) && (!activeNetworkInf.isConnected()))
                        || (activeNetworkInf == null)) {
                    PowerManager powerMgr = (PowerManager) getSystemService(POWER_SERVICE);
                    PowerManager.WakeLock wakeLock = powerMgr.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);

                    LOG.info("Take wakelock for 40 seconds");
                    wakeLock.acquire(40 * 1000);
                }

                configureNetworkSettings(activeNetworkInf);
            }

        };
        registerReceiver(mOnNetworkConnectivityChanged, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
        ConnectivityManager connMgr = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
        NetworkInfo activeNetworkInfo = connMgr.getActiveNetworkInfo();
        configureNetworkSettings(activeNetworkInfo);
    }

    /* Check if there is a client that wants to override authentication. */
    private boolean checkClientsAuthentication() {
        boolean result = false;
        synchronized (mQeoReadyLock) {
            int i = mClients.beginBroadcast();

            while (i > 0) {
                i--;
                try {
                    if (mClients.getBroadcastItem(i).onStartAuthentication()) {
                        LOG.info("Client implements its own authentication mechanism");
                        result = true;
                        break;
                    }
                } catch (final RemoteException e) {
                    // The RemoteCallbackList will take care of removing
                    // the dead object for us.
                }
            }
            mClients.finishBroadcast();
        }
        return result;
    }

    /* Notify all clients that Qeo is now ready or not. */
    private void notifyClientsReady(boolean success) {
        synchronized (this) {
            if (!success) {
                mQeoInitCalled = false; // qeo init failed.
                // make sure to stop headless registration service
                RegisterService.stopHeadlessRegistration(this);
            }
        }
        synchronized (mQeoReadyLock) {
            mQeoReady = success;

            int i = mClients.beginBroadcast();
            while (i > 0) {
                i--;
                try {
                    if (success) {
                        LOG.fine("Notify client: " + mClients.getBroadcastItem(i));
                        mClients.getBroadcastItem(i).onRegistered();
                    } else {
                        if (mOtpDialogCanceled) {
                            LOG.warning("OTP dialog cancelled");
                            mClients.getBroadcastItem(i).onOtpDialogCanceled();
                        } else {
                            LOG.warning("Security init failed: " + mLatestStatusUpdate);
                            if (mQeoErrorReason.equals("Invalid OTC")) {
                                mClients.getBroadcastItem(i).onSecurityInitFailed(mQeoErrorReason);
                            } else {
                                mClients.getBroadcastItem(i).onSecurityInitFailed(mLatestStatusUpdate);
                            }
                        }
                    }
                } catch (final Exception e) {
                    // The RemoteCallbackList will take care of removing
                    // the dead object for us.
                }
            }
            mClients.finishBroadcast();
        }
    }

    /* Notify all clients of the waking up. */
    private void notifyClientsWakeUp(String typeName) {
        synchronized (mQeoReadyLock) {
            int i = mClients.beginBroadcast();

            while (i > 0) {
                i--;
                try {
                    mClients.getBroadcastItem(i).onWakeUp(typeName);
                } catch (final RemoteException e) {
                    // The RemoteCallbackList will take care of removing
                    // the dead object for us.
                }
            }
            mClients.finishBroadcast();
        }
    }

    private void notifyClientsBgnsConnected(boolean state) {
        synchronized (mQeoReadyLock) {
            int i = mClients.beginBroadcast();

            while (i > 0) {
                i--;
                try {
                    mClients.getBroadcastItem(i).onBgnsConnected(state);
                } catch (final RemoteException e) {
                    // The RemoteCallbackList will take care of removing
                    // the dead object for us.
                }
            }
            mClients.finishBroadcast();
        }
    }

    private Map<String, String> parseFlags(String flags) {
        Map<String, String> out = new HashMap<String, String>();
        for (String flag : flags.split(";")) {
            String[] flagSplit = flag.split("=");
            if (flagSplit.length == 2) {
                // ignore if not valid
                out.put(flagSplit[0], flagSplit[1]);
            }
        }
        return out;
    }

    // Set optional DDS flags from config app
    // this will only work if qeo-c is built with DEBUG=1
    private void setDdsFlags() {
        String flags = mSettingsExternal.getString("ddsFlags", null);
        if (flags != null) {
            for (Entry<String, String> e : parseFlags(flags).entrySet()) {
                NativeQeo.setDssFlag(e.getKey(), e.getValue());
            }
        }
    }

    private void setJunitConfigParameters() {
        String flags = mSettingsInternal.getString("qeoFlags", null);
        if (flags != null) {
            for (Entry<String, String> e : parseFlags(flags).entrySet()) {
                LOG.info("Set junit parameter " + e.getKey() + " = " + e.getValue());
                NativeQeo.setQeoParameter(e.getKey(), e.getValue());
            }
        }
    }

    private synchronized void checkOpenDomainDisabled() {
        // check if open domain is disabled in config app
        mOpenDomainDisabled = mSettingsExternal.getBoolean("openDomainDisabled", false);
    }

    private void checkDdsSecurityDisabled() {
        // check if dds security is disabled in config app
        boolean noSecurity = mSettingsExternal.getBoolean("ddsSecurityDisabled", false);
        if (noSecurity) {
            LOG.warning("Disabled security!!");
            NativeQeo.setSecurityDisabled(true);
        }
    }

    private void checkLocationServiceDisabled() {
        boolean noLocSrv = mSettingsExternal.getBoolean("locationServiceDisabled", false);
        if (noLocSrv) {
            LOG.warning("Disabled location service!!");
            NativeQeo.setQeoParameter("FWD_DISABLE_LOCATION_SERVICE", "1");
        }
    }

    private void checkDomainId() {
        int id;
        int defaultClosed = 128;
        int defaultOpen = 1;

        LOG.fine("getDomainId() start");
        id = mSettingsExternal.getInt("domainId", defaultClosed);
        if (id != defaultClosed) {
            NativeQeo.setQeoParameter("DDS_DOMAIN_ID_CLOSED", Integer.toString(id));
        }
        id = mSettingsExternal.getInt("domainIdOpen", defaultOpen);
        if (id != defaultOpen) {
            NativeQeo.setQeoParameter("DDS_DOMAIN_ID_OPEN", Integer.toString(id));
        }
        LOG.fine("getDomainId() done");
    }

    /**
     * return whether the manifest pop-up needs to be disabled according to the fact that an Oauth token is present or
     * if not, according to to the internal preferences.
     *
     * @return True if the manifest pop-up should be disabled, false otherwise.
     */
    public boolean isManifestPopupDisabled() {
        if (!QeoDefaults.isManifestEnabled()) {
            // manifest globally disabled
            return true;
        }
        String refreshToken = DeviceRegPref.getRefreshToken();

        if (refreshToken != null && !refreshToken.isEmpty()) {
            return true;
        }
        return mSettingsInternal.getBoolean("manifestPopupDisabled", false);
    }

    /**
     * Initialize nativeQeo before it can be used.
     *
     * @param ctx android context.
     */
    public static synchronized void initNative(Context ctx) {
        NativeQeo.setStoragePath(getStorageDir(ctx));

        NativeQeo.setDeviceInfo(DeviceInfoAndroid.getInstance().getDeviceInfo());
        // don't publish deviceInfo the the open domain
        NativeQeo.setQeoParameter("CMN_PUB_DEVICEINFO_OPEN_DOMAIN", "0");
    }

    /**
     * Get the qeo storage directory.
     *
     * @param ctx The android context.
     * @return The path.
     */
    public static String getStorageDir(Context ctx) {
        if (QeoDefaults.getQeoStorageDir() == null) {
            return ctx.getFilesDir().getAbsolutePath();
        } else {
            return QeoDefaults.getQeoStorageDir();
        }
    }

    private synchronized void initQeo() {
        if (mQeoInitCalled || mDestroyed) {
            return;
        }

        // initialize Qeo
        LOG.fine("Qeo.initQeo");

        // open sqlite database
        initDatabase();

        /**
         * Initialize Qeo (domain participant) >> this is blocking now!!!! Will be unblocked from native thread thanks
         * to calling setRegistrationCredentials or cancelRegistration. Those calls are done from within
         * BroadcastReceiver.onReceive()
         */
        Thread qeoInitThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    // Use Android's Log to make sure the version is always logged independent of the level.
                    Log.i(TAG, "Qeo-java version is " + QeoJava.getVersionString());

                    NativeQeo.clearState();

                    initNative(QeoService.this);

                    checkOpenDomainDisabled();
                    checkLocationServiceDisabled();
                    checkDdsSecurityDisabled();
                    checkDomainId();

                    setDdsFlags();
                    setJunitConfigParameters();

                    LOG.fine("Creating Native Factory CLOSED");

                    NativeQeo.setTcpServer(QeoFactory.DEFAULT_ID, getTcpServer());
                    mQeoConnectionListenerClosed = new QeoServiceConnectionListener();
                    if (!mDestroyed) {
                        QeoJava.initQeo(QeoFactory.DEFAULT_ID, mQeoConnectionListenerClosed);
                    } else {
                        LOG.fine("Skipping initQeo for closed factory as service is destroyed");
                        mQeoConnectionListenerClosed.onQeoError(new QeoException("Service already stopped"));
                    }
                    if (!mOpenDomainDisabled) {
                        if (!mDestroyed) {
                            LOG.fine("Creating Native Factory OPEN");
                            NativeQeo.setTcpServer(QeoFactory.OPEN_ID, getTcpServer());
                            mQeoConnectionListenerOpen = new QeoServiceConnectionListener();
                            QeoJava.initQeo(QeoFactory.OPEN_ID, mQeoConnectionListenerOpen);
                        } else {
                            LOG.fine("Skipping initQeo for open factory as service is destroyed");
                            mQeoConnectionListenerClosed.onQeoError(new QeoException("Service already stopped"));
                        }
                    }
                    LOG.fine("Creating Native Factory DONE");
                } catch (Exception e) {
                    LOG.log(Level.SEVERE, "Qeo native init failed", e);
                    synchronized (QeoService.this) {
                        // mark as not called again.
                        mQeoInitCalled = false;
                    }
                }
            }
        }, "QeoInitThread");

        qeoInitThread.start();
        mQeoInitCalled = true;
    }

    private synchronized void initDatabase() {
        if (mDatabase == null) {
            // open sqlite database
            DBHelper dbHelper = new DBHelper(this);
            mDatabase = dbHelper.getWritableDatabase();
        }
    }

    /**
     * Get SQlite database handle for the service.
     *
     * @return database handle
     */
    public SQLiteDatabase getDatabase() {
        initDatabase();
        return mDatabase;
    }

    @Override
    public void onCreate() {
        // initialize QeoSuspendHelper singleton
        mQeoSuspendHelper = QeoSuspendHelper.getInstance(this);
        // don't do any calls to native here as this will init the native libraries on the UI thread!
        LOG.info("Creating service");
        if (mDestroyed) {
            throw new IllegalStateException("This service is already destroyed");
        }

        // Init different version of the AIDL interfaces
        mQeoServiceImpl = new QeoServiceImpl(this);
        mBinderV1 = new QeoServiceV1(mQeoServiceImpl);

        // init ServiceApplication. Only really needed if the service is embedded, but won't do anything if running
        // standalone so it's not harm in calling it here.
        ServiceApplication.initServiceApp(getApplicationContext());

        mSettingsExternal = ServiceApplication.getProperties().getExternalSettings();
        mSettingsInternal = new ConfigurableSettings(this, ConfigurableSettings.FILE_QEO_PREFS, false);
        synchronized (this) {
            mHandler = new Handler(Looper.getMainLooper());
        }

        /* Uncomment the next line if you want to debug the Qeo service */
        // android.os.Debug.waitForDebugger();

        final WifiManager wifi = (WifiManager) getSystemService(Context.WIFI_SERVICE);
        if (wifi != null) {
            // take multicast lock. Not required on all devices as some have
            // this by default enabled in their drivers.
            mMulticastLock = wifi.createMulticastLock("mQeo");
            mMulticastLock.acquire();
        }
        mOnNetworkConnectivityChanged = null;
    }

    @Override
    public void onDestroy() {
        LOG.info("onDestroy(destroying service)");
        mDestroyed = true;

        synchronized (this) {
            // clear everything that would still be pending
            mHandler.removeCallbacksAndMessages(null);
            mHandler = null;
        }

        LocalBroadcastManager.getInstance(this).unregisterReceiver(mOnSecurityNotice);
        if (mOnNetworkConnectivityChanged != null) {
            unregisterReceiver(mOnNetworkConnectivityChanged);
        }

        LocalBroadcastManager.getInstance(this).sendBroadcastSync(new Intent(ACTION_SERVICE_DESTROYED));

        // remove all dangling reader/writers
        ConnectedReader.closeAll();
        ConnectedWriter.closeAll();

        stopHeadlessRegistrationListener();

        // pre destroy
        NativeQeo.preDestroy();

        if (mQeoConnectionListenerClosed != null) {
            QeoJava.closeQeo(mQeoConnectionListenerClosed);
        }
        if (mQeoConnectionListenerOpen != null) {
            QeoJava.closeQeo(mQeoConnectionListenerOpen);
        }

        if (mDatabase != null) {
            mDatabase.close();
            mDatabase = null;
        }

        // post destroy
        NativeQeo.postDestroy();
        NativeQeo.clearState();

        // Tell the user we stopped.
        if (mMulticastLock != null) {
            mMulticastLock.release();
            mMulticastLock = null;
        }
        QeoSuspendHelper.stop();
        mQeoSuspendHelper = null;
        LOG.info("onDestroy(done)");
    }

    private void startHeadlessRegistrationListener() {
        if (QeoDefaults.isRemoteRegistrationListenerAvailable() && QeoManagementApp.isRealmAdmin()) {
            // only start if the user is admin
            RemoteDeviceRegistration deviceRegisterQeoService = RemoteDeviceRegistration.getInstance();
            deviceRegisterQeoService.start(this, mQeoOpen, mQeoClosed);
        }
    }

    private void stopHeadlessRegistrationListener() {
        RemoteDeviceRegistration deviceRegisterQeoService = RemoteDeviceRegistration.getInstance();
        deviceRegisterQeoService.stop();
    }

    @Override
    public IBinder onBind(Intent intent) {
        // binder should return the correct version of the requested aidl interface. Make sure to update
        // QeoServiceVersion too if new version is created.
        mOtpDialogCanceled = false;
        String action = intent.getAction();
        if (action != null) {
            LOG.info("Binding to Qeo Service (action: " + action + ", type: " + intent.getType() + ")");
        }
        if (AidlConstants.AIDL_SERVICE_ACTION_V1.equals(action)) {
            return mBinderV1;
        } else {
            // default
            return mBinderV1;
        }

    }

    @Override
    public boolean onUnbind(Intent intent) {
        LOG.fine("unbinding to Qeo Service");
        mBinderV1.onUnbind();
        return true;
    }

    private void checkDelayedStop() {
        QeoServiceTimeout.bindToService(getApplicationContext());
    }

    /**
     * Register service callback and init qeo if needed.
     *
     * @param cb The callback
     * @throws RemoteException if an error occurs.
     */
    void register(IServiceQeoCallback cb) throws RemoteException {
        LOG.fine("OnRegister");
        synchronized (mQeoReadyLock) {
            mClients.register(cb);
            if (mQeoReady) {
                /* Qeo is already ready, so we can notify the peer directly from here */
                cb.onRegistered();
            }
        }
        // initialize Qeo
        initQeo();
    }

    /**
     * Unregister service callback.
     *
     * @param cb The callback.
     * @throws RemoteException if an error occurs.
     */
    void unregister(IServiceQeoCallback cb) throws RemoteException {
        LOG.fine("OnUnregister");
        synchronized (mQeoReadyLock) {
            mClients.unregister(cb);
        }
        cb.onUnregistered();

        // start timeout service if client stops.
        checkDelayedStop();
    }

    /**
     * Trigger a policy refresh.
     */
    void refreshPolicy() {
        try {
            if (mQeoClosed == null) {
                LOG.severe("Qeo not yet initialized while trying to refresh policy");
                throw new IllegalStateException("Qeo not yet initialized");
            }
            LOG.fine("Refresh policy");
            ((EntityAccessorJava) mQeoClosed.getEntityAccessor()).getNativeFactory().refreshPolicy();
        } catch (Throwable e) {
            // catch everything, they can't be sent over AIDL
            LOG.log(Level.WARNING, "Unable to refresh policy", e);
        }
    }

    private void registerOauthReceiver() {
        // register broadcastreceiver to get notified when OAUthTokenService is ready.
        IntentFilter ifo = new IntentFilter();
        ifo.addAction(OAuthTokenService.ACTION_OAUTH_TOKEN_READY);

        BroadcastReceiver onOAuthTokenReady = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                LOG.fine("mOnOAuthTokenReady onReceive: " + intent.getAction());
                String action = intent.getAction();
                if (action.equals(OAuthTokenService.ACTION_OAUTH_TOKEN_READY)) {
                    LocalBroadcastManager.getInstance(context).unregisterReceiver(this);
                    LOG.fine("Start registerService");
                    startRegisterService();
                } else {
                    throw new IllegalStateException("unhandled action: " + action);
                }
            }
        };
        LocalBroadcastManager.getInstance(getBaseContext()).registerReceiver(onOAuthTokenReady, ifo);
    }

    /**
     * Continue authentication.
     *
     * @param type code type
     * @param data the authentication data
     */
    void continueAuthentication(int type, String data) {
        LOG.fine("Got OAuth/JWT code from application, starting OAuthTokenService");
        // start the oauth service
        Intent i = new Intent(this, OAuthTokenService.class);
        switch (type) {
        case AidlConstants.AUTHENTICATION_DATA_CODE:
            i.putExtra(OAuthTokenService.INTENT_EXTRA_OAUTH_CODE, data);
            registerOauthReceiver();
            startService(i);
            break;
        case AidlConstants.AUTHENTICATION_DATA_JWT:
            i.putExtra(OAuthTokenService.INTENT_EXTRA_JWT, data);
            registerOauthReceiver();
            startService(i);
            break;
        case AidlConstants.AUTHENTICATION_DATA_CANCEL:
            LOG.info("Cancel registration");
            NativeQeo.cancelRegistration();
            stopService(new Intent(QeoService.this, QeoService.class));
            break;
        case AidlConstants.AUTHENTICATION_DATA_OTC: {
            String[] dataSplit = data.split("\\|");
            if (dataSplit.length != 2) {
                LOG.warning("Invalid OTC data: " + data);
                break;
            }
            i = new Intent(SECURITY_SETUP_FINISHED);
            i.putExtra(INTENT_EXTRA_SUCCESS, true);
            i.putExtra(INTENT_EXTRA_OTC, dataSplit[0]);
            i.putExtra(INTENT_EXTRA_URL, dataSplit[1]);
            LocalBroadcastManager.getInstance(this).sendBroadcast(i);
            break;
        }
        default:
            LOG.warning("Unknown type: " + type);
        }
    }

    /**
     * Suspend Qeo operations. All non-vital network connections are closed and timers are stopped.
     *
     * @see org.qeo.Notifiable
     * @see org.qeo.java.QeoConnectionListener#onWakeUp(java.lang.String)
     * @see #resume()
     */
    void suspend() {
        LOG.fine("Suspend operations");
        mQeoSuspendHelper.suspend();
    }

    /**
     * Resume Qeo operations. Re-establishes all connections as if nothing happened.
     *
     * @see #suspend()
     */
    void resume() {
        LOG.fine("Resume operations");
        mQeoSuspendHelper.resume();
    }
}