de.dfki.iui.opentok.cordova.plugin.OpenTokPlugin.java Source code

Java tutorial

Introduction

Here is the source code for de.dfki.iui.opentok.cordova.plugin.OpenTokPlugin.java

Source

package de.dfki.iui.opentok.cordova.plugin;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.cordova.CordovaWebView;
import org.apache.cordova.api.CallbackContext;
import org.apache.cordova.api.CordovaInterface;
import org.apache.cordova.api.CordovaPlugin;
import org.apache.cordova.api.LOG;
import org.apache.cordova.api.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.provider.Settings.Secure;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.AbsoluteLayout;
import android.widget.ImageView;

import com.opentok.android.Connection;
import com.opentok.android.OpentokException;
import com.opentok.android.OpentokException.ErrorCode;
import com.opentok.android.Publisher;
import com.opentok.android.Session;
import com.opentok.android.Stream;
import com.opentok.android.Subscriber;

import de.dfki.iui.opentok.R;

/*
 *    Copyright (C) 2012-2014 DFKI GmbH
 *    Deutsches Forschungszentrum fuer Kuenstliche Intelligenz
 *    German Research Center for Artificial Intelligence
 *    http://www.dfki.de
 * 
 *    Permission is hereby granted, free of charge, to any person obtaining a 
 *    copy of this software and associated documentation files (the 
 *    "Software"), to deal in the Software without restriction, including 
 *    without limitation the rights to use, copy, modify, merge, publish, 
 *    distribute, sublicense, and/or sell copies of the Software, and to 
 *    permit persons to whom the Software is furnished to do so, subject to 
 *    the following conditions:
 * 
 *    The above copyright notice and this permission notice shall be included 
 *    in all copies or substantial portions of the Software.
 * 
 *    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
 *    OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
 *    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
 *    IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
 *    CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
 *    TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
 *    SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

public class OpenTokPlugin extends CordovaPlugin {

    private static final String AUDIO_ICON_ID_POSTFIX = "__audioIcon";

    private static final String ID_PUBLISHER = "TBPublisher";

    private final static String PLUGIN_NAME = "OpenTokPlugin";

    private final static String ACTION_UPDATE_VIEW = "updateView";
    private final static String ACTION_EXEPTION_HANDLER = "exceptionHandler";
    private final static String ACTION_TB_TESTING = "TBTesting";
    private final static String ACTION_INIT_PUBLISHER = "initPublisher";
    private final static String ACTION_DESTROY_PUBLISHER = "destroyPublisher";
    private final static String ACTION_INIT_SESSION = "initSession";
    private final static String ACTION_STREAM_CREATED_HANDLER = "streamCreatedHandler";
    private final static String ACTION_CONNECT = "connect";
    private final static String ACTION_STREAM_DISCONNECT_HANDLER = "streamDisconnectedHandler";
    private final static String ACTION_SESSION_DISCONNECT_HANDLER = "sessionDisconnectedHandler";
    private final static String ACTION_DISCONNECT = "disconnect";
    private final static String ACTION_PUBLISH = "publish";
    private final static String ACTION_UNPUBLISH = "unpublish";
    private final static String ACTION_UNSUBSCRIBE = "unsubscribe";
    private final static String ACTION_SUBSCRIBE = "subscribe";

    private final static String ACTION_SESSION_CONNECTION_CREATED_HANDLER = "sessionConnectionCreatedHandler";
    private final static String ACTION_SESSION_CONNECTION_DESTROYED_HANDLER = "sessionConnectionDestroyedHandler";

    private final static String ACTION_GET_SESSION_CONNECTION = "getSessionConnection";
    private final static String ACTION_TOGGLE_AUDIO = "toggleAudio";
    private final static String ACTION_REFRESH = "refresh";

    private Session _session;
    private Publisher _publisher;

    private boolean statusIsDeviceOnPause;

    //TODO make things thread-safe
    private Map<String, Subscriber> subscriberDictionary;
    private Map<String, ImageView> subscriberAudioIconDictionary;
    private Map<String, Stream> streamDictionary;

    private PauseStrategy _pauseMode = PauseStrategy.PAUSE_TRANSMISSION;

    /**
     * It <b>recommended</b> to explicitly stop the session before Activity is paused,
     * of disconnect all subscribers and un-publish.
     * 
     * Use this in combination with REMOVE_NATIVE_VIEWS_ON_RESUME
     *  which will "clean up" any remnants of the session upon
     *  resuming the Activity.
     * 
     * With PAUSE_TRANSMISSION, the plugin will try to automatically pause all
     * audio/video transmission during the Activity's pause-state.
     * 
     * NOTE that currently (vers. 2.0beta2) will fail to resume from PAUSE_TRANSMISSION
     * cleanly due to (silent) errors when "pausing" subscribers.
     * 
     * 
     * @author russa
     */
    public static enum PauseStrategy {
        PAUSE_TRANSMISSION, // pause/resume publisher/subscribers
        REMOVE_NATIVE_VIEWS_ON_RESUME, // do remove all views on resume (use this if your app automatically disconnects on pause: ensures that all native overlays will get removed on resume)
        DISCONNECT_ON_PAUSE
    }

    //for debugging
    private enum DebugLevel {
        DEBUG, INFO, WARN, ERROR, FATAL
    }

    //field for debug-level:
    private DebugLevel _debug = DebugLevel.INFO;

    private class ViewParams {
        int top;
        int left;
        int width;
        int height;
        int zIndex;

        public ViewParams(int top, int left, int width, int height, int zIndex) {
            super();
            this.top = top;
            this.left = left;
            this.width = width;
            this.height = height;
            this.zIndex = zIndex;
        }

        public LayoutParams create() {
            return createLayoutParams(left, top, width, height);
        }
    }

    private class SynchronizedViewAdministrator {

        private List<View> ViewList;

        public SynchronizedViewAdministrator() {
            ViewList = new LinkedList<View>();
        }

        /**
         *
         * @param activity this.cordova.getActivity()
         * @param parentView
         * @param view
         * @param params
         */
        // TODO: should add a parent-view-layer for opentok-views instead  of using the webview
        public synchronized void addView(Activity activity, final CordovaWebView parentView, final View view,
                final LayoutParams params) {
            activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    parentView.addView(view, params);
                    ViewList.add(view);
                }
            });
        }

        public synchronized void removeView(Activity activity, final View view) {
            activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    if (view != null && view.getParent() != null) {

                        // remove its child views first
                        //                                if (!(view instanceof ImageView)) {
                        //                                    ((ViewGroup) view).removeAllViews();
                        //                                }

                        // to be safe - check that view again
                        //                                if(view != null && view.getParent() != null) {
                        ((ViewGroup) view.getParent()).removeView(view);
                        //                                }

                        Log.d(PLUGIN_NAME, String.format("Removed view for from Layout."));

                        if (ViewList.indexOf(view) > -1) {
                            //                                    ViewList.remove(ViewList.indexOf(view));
                            ViewList.remove(view);
                        }
                    } else {
                        // not in layout -> remove it from list
                        if (ViewList.indexOf(view) > -1) {
                            //                                    ViewList.remove(ViewList.indexOf(view));
                            ViewList.remove(view);
                        }
                    }
                }
            });
        }

        public synchronized void removeAllViews(Activity activity) {
            activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {

                    Object[] viewListArray = ViewList.toArray();

                    for (Object viewObject : viewListArray) {
                        View view = (View) viewObject;
                        if (view != null && (view.getParent() != null)) {
                            // remove its child views first
                            //                                    ((ViewGroup)view).removeAllViews();

                            // to be safe - check that view again
                            //                                    if(view != null && view.getParent() != null) {
                            ((ViewGroup) view.getParent()).removeView(view);
                            //                                    }

                            Log.d(PLUGIN_NAME, String.format("Removed view for from Layout."));

                            if (ViewList.indexOf(view) > -1) {
                                //                                    ViewList.remove(ViewList.indexOf(view));
                                ViewList.remove(view);
                            }
                        } else {
                            // not in layout -> remove it from list
                            if (ViewList.indexOf(view) > -1) {
                                //                                    ViewList.remove(ViewList.indexOf(view));
                                ViewList.remove(view);
                            }
                        }
                    }
                }
            });
        }
    }

    //   private ViewParams publisherViewParams;
    private Map<String, ViewParams> subscriberViewParams;

    private OpenTokPlugin.Listener mListener = new OpenTokPlugin.Listener();

    private CallbackContext _sessionConnectedCallback;

    private CallbackContext _streamCreatedCallback;

    private CallbackContext _exceptionCallback;
    private CallbackContext _streamDisconnectedCallback;
    private CallbackContext _sessionDisconnectedCallback;

    private CallbackContext _sessionConnectionCreatedCallback;
    private CallbackContext _sessionConnectionDestroyedCallback;

    static CordovaInterface _cordova;
    static CordovaWebView _webView;

    //    protected static List<View> TokViewList;
    private SynchronizedViewAdministrator viewAdministrator;

    @Override
    public void initialize(CordovaInterface cordova, CordovaWebView webView) {
        _cordova = cordova;
        _webView = webView;
        statusIsDeviceOnPause = false;
        viewAdministrator = new SynchronizedViewAdministrator();
        Log.d(PLUGIN_NAME, "Initialize Plugin");
        super.initialize(cordova, webView);
    }

    private boolean isPublishing = false;
    private boolean isPausedPublishing = false;
    private boolean isPausedPublishingAudio = false;
    private boolean isPausedPublishingVideo = false;

    private List<PausedSubscriber> pausedSubscribers = new LinkedList<OpenTokPlugin.PausedSubscriber>();

    private ImageView _publisherAudioIcon;

    private class PausedSubscriber {
        Subscriber s;
        boolean isAudio;
        boolean isVideo;

        public PausedSubscriber(Subscriber subs) {
            this.isAudio = subs.getSubscribeToAudio();
            this.isVideo = subs.getSubscribeToVideo();
            this.s = subs;
            pause();
        }

        public void pause() {
            this.s.setSubscribeToAudio(false);
            this.s.setSubscribeToVideo(false);
        }

        public void resume() {
            this.s.setSubscribeToAudio(this.isAudio);
            this.s.setSubscribeToVideo(this.isVideo);
        }

        public boolean isValid() {
            return s != null && s.getStream() != null && s.getStream().getStreamId() != null;
        }
    }

    @Override
    public void onPause(boolean multitasking) {

        statusIsDeviceOnPause = true;

        //      Log.d("OPENTOK", "pausing...");

        if (this._pauseMode == PauseStrategy.DISCONNECT_ON_PAUSE) {
            if (_session != null) {
                _session.disconnect();
            }
            this.onDestroy();
        } else if (this._pauseMode == PauseStrategy.PAUSE_TRANSMISSION) {
            this.doPause();
        }

        super.onPause(multitasking);
        //        Log.d("OPENTOK", "paused...");
    }

    private void doPause() {

        Log.d("OPENTOK", "pausing publisher and all subscribers...");

        this.isPausedPublishingAudio = this._publisher.getPublishAudio();
        this.isPausedPublishingVideo = this._publisher.getPublishVideo();
        this.isPausedPublishing = true;

        this._publisher.setPublishAudio(false);
        this._publisher.setPublishVideo(false);

        this.pausedSubscribers.clear();
        for (Subscriber s : this.subscriberDictionary.values()) {

            this.pausedSubscribers.add(new PausedSubscriber(s));
        }

    }

    @Override
    public void onResume(boolean multitasking) {

        statusIsDeviceOnPause = false;

        //        Log.d("OPENTOK", "Start resuming...");
        if (this._pauseMode == PauseStrategy.REMOVE_NATIVE_VIEWS_ON_RESUME
                || this._pauseMode == PauseStrategy.DISCONNECT_ON_PAUSE) {
            //           Log.d("OPENTOK", "removing views - if any");
            // remove all views used by opentok - better safe than sorry
            viewAdministrator.removeAllViews(this.cordova.getActivity());
            //           Log.d("OPENTOK", "removed views.");
        } else if (this._pauseMode == PauseStrategy.PAUSE_TRANSMISSION) {
            this.doResume();
        }

        super.onResume(multitasking);
        //        Log.d("OPENTOK", "resumed.");
    }

    private void doResume() {

        Log.d("OPENTOK", "resuming publisher and all subscribers...");

        this.isPausedPublishing = false;

        this._publisher.setPublishAudio(this.isPausedPublishingAudio);
        this._publisher.setPublishVideo(this.isPausedPublishingVideo);

        for (PausedSubscriber ps : this.pausedSubscribers) {
            if (ps.isValid())
                ps.resume();
            else {

                //cleanup: if subscriber is still in subscriber-map --> do remove it (i.e. un-subscribe)
                String sid = null;
                for (Entry<String, Subscriber> e : this.subscriberDictionary.entrySet()) {
                    if (e.getValue() == ps.s) {
                        sid = e.getKey();
                        break;
                    }
                }

                if (sid != null)
                    this.doUnsubscribe(sid, true);
            }

        }
        this.pausedSubscribers.clear();

    }

    @Override
    public void onDestroy() {
        statusIsDeviceOnPause = true;
        if (_publisher != null) {
            if (isPublishing) {
                this.doUnpublish();
            }
            this.doDestroyPublisher();
        }

        if (subscriberDictionary != null) {
            String[] sids = new String[subscriberDictionary.size()];
            sids = subscriberDictionary.keySet().toArray(sids);

            for (String sid : sids) {
                this.doUnsubscribe(sid);
            }
        }

        if (_session != null) {
            this.doDisconnect();
        }

        super.onDestroy();
    }

    @Override
    public boolean execute(String action, JSONArray args, CallbackContext callbackContext) {
        boolean isHandled = false;

        doDebug(args != null ? args.toString()
                : "NO_ARGS" + (callbackContext.isFinished() ? " CALLBACK_FINISHED!" : ""), "execute-" + action);//FIXME debug

        if (ACTION_UPDATE_VIEW.equals(action)) {
            isHandled = true;

            PluginResult result;
            try {
                String sid = args.getString(0); //[command.arguments objectAtIndex:0];
                int top = extractDp(args, 1);//args.getInt(1); //[[command.arguments objectAtIndex:1] intValue];
                int left = extractDp(args, 2);//args.getInt(2); //[[command.arguments objectAtIndex:2] intValue];
                int width = extractDp(args, 3);//args.getInt(3); //[[command.arguments objectAtIndex:3] intValue];
                int height = extractDp(args, 4);//args.getInt(4); //[[command.arguments objectAtIndex:4] intValue];
                int zIndex = args.getInt(5); //[[command.arguments objectAtIndex:5] intValue];

                result = doUpdateView(sid, top, left, width, height, zIndex);

            } catch (JSONException e) {
                e.printStackTrace();
                String msg = String.format("Error processing arguments for %s: %s - arguments: %s", action, e,
                        args.toString());
                result = new PluginResult(PluginResult.Status.ERROR, msg);
            }

            callbackContext.sendPluginResult(result);
        } else if (ACTION_EXEPTION_HANDLER.equals(action)) {
            isHandled = true;

            PluginResult result = this.setExceptionHandler(callbackContext);

            callbackContext.sendPluginResult(result);
        } else if (ACTION_STREAM_DISCONNECT_HANDLER.equals(action)) {
            isHandled = true;

            PluginResult result = this.setStreamDisconnectHandler(callbackContext);

            callbackContext.sendPluginResult(result);
        } else if (ACTION_SESSION_DISCONNECT_HANDLER.equals(action)) {
            isHandled = true;

            PluginResult result = this.setSessionDisconnectHandler(callbackContext);

            callbackContext.sendPluginResult(result);
        } else if (ACTION_TB_TESTING.equals(action)) {
            isHandled = true;

            this.doTBTesting(callbackContext);
        } else if (ACTION_INIT_SESSION.equals(action)) {
            isHandled = true;

            PluginResult result;
            try {

                String sessionId = args.getString(0);

                result = doInitSession(sessionId);

            } catch (JSONException e) {
                e.printStackTrace();
                String msg = String.format("Error processing arguments for %s: %s - arguments: %s", action, e,
                        args.toString());
                result = new PluginResult(PluginResult.Status.ERROR, msg);
            }

            callbackContext.sendPluginResult(result);
        } else if (ACTION_INIT_PUBLISHER.equals(action)) {
            isHandled = true;

            PluginResult result;
            try {

                LOG.i(PLUGIN_NAME, "creating Publisher");
                boolean bpubAudio = true;
                boolean bpubVideo = true;

                // Get Parameters

                int top = args.getInt(0); //[[command.arguments objectAtIndex:0] intValue];
                int left = args.getInt(1); //[[command.arguments objectAtIndex:1] intValue];
                int width = args.getInt(2); //[[command.arguments objectAtIndex:2] intValue];
                int height = args.getInt(3); //[[command.arguments objectAtIndex:3] intValue];

                String name = args.getString(4); //[command.arguments objectAtIndex:4];
                if (name.equals("TBNameHolder")) {

                    //TODO this usually only works for tab-devices... need to provide a fallback in case this is a phone...
                    name = Secure.getString(this.cordova.getActivity().getContentResolver(), Secure.ANDROID_ID);
                    //                 name = [[UIDevice currentDevice] name];
                }

                String publishAudio = args.getString(5); //[command.arguments objectAtIndex:5];
                if (publishAudio.equals("false")) {
                    bpubAudio = false;
                }
                String publishVideo = args.getString(6); //[command.arguments objectAtIndex:6];
                if (publishVideo.equals("false")) {
                    bpubVideo = false;
                }
                int zIndex = args.getInt(7); //[[command.arguments objectAtIndex:7] intValue];

                result = doInitPublisher(top, left, width, height, name, bpubAudio, bpubVideo, zIndex);

            } catch (JSONException e) {
                e.printStackTrace();
                String msg = String.format("Error processing arguments for %s: %s - arguments: %s", action, e,
                        args.toString());
                result = new PluginResult(PluginResult.Status.ERROR, msg);
            }

            callbackContext.sendPluginResult(result);
        } else if (ACTION_PUBLISH.equals(action)) {
            isHandled = true;

            PluginResult result = doPublish();

            callbackContext.sendPluginResult(result);
        } else if (ACTION_UNPUBLISH.equals(action)) {
            isHandled = true;

            PluginResult result = doUnpublish();

            callbackContext.sendPluginResult(result);
        } else if (ACTION_DESTROY_PUBLISHER.equals(action)) {
            isHandled = true;

            PluginResult result = doDestroyPublisher();

            callbackContext.sendPluginResult(result);
        } else if (ACTION_STREAM_CREATED_HANDLER.equals(action)) {
            isHandled = true;

            PluginResult result = this.setStreamCreatedHandler(callbackContext);

            callbackContext.sendPluginResult(result);
        } else if (ACTION_CONNECT.equals(action)) {
            isHandled = true;

            //         PluginResult result;
            try {

                //             NSString* tbKey = [command.arguments objectAtIndex:0];
                //             NSString* tbToken = [command.arguments objectAtIndex:1];
                String tbKey = args.getString(0);
                String tbToken = args.getString(1);

                //            result = 
                doConnect(tbKey, tbToken, callbackContext);

            } catch (JSONException e) {
                e.printStackTrace();
                String msg = String.format("Error processing arguments for %s: %s - arguments: %s", action, e,
                        args.toString());
                //            result =
                callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, msg));
            }

            //          callbackContext.sendPluginResult(result);
        } else if (ACTION_SUBSCRIBE.equals(action)) {
            isHandled = true;

            PluginResult result;
            try {

                String sid = args.getString(0);//[command.arguments objectAtIndex:0];

                int top = args.getInt(1);// [[command.arguments objectAtIndex:1] intValue];
                int left = args.getInt(2);// [[command.arguments objectAtIndex:2] intValue];
                int width = args.getInt(3);// [[command.arguments objectAtIndex:3] intValue];
                int height = args.getInt(4);// [[command.arguments objectAtIndex:4] intValue];
                String tmp = args.getString(5);// [command.arguments objectAtIndex:5];
                int zIndex = args.getInt(6);// [[command.arguments objectAtIndex:6] intValue];

                boolean isSubscribeToVideo = true;
                if (tmp != null) {
                    tmp = tmp.trim();
                    if (tmp.length() > 0)
                        isSubscribeToVideo = Boolean.parseBoolean(tmp);
                }

                result = doSubscribe(sid, top, left, width, height, isSubscribeToVideo, zIndex);

            } catch (JSONException e) {
                e.printStackTrace();
                String msg = String.format("Error processing arguments for %s: %s - arguments: %s", action, e,
                        args.toString());
                result = new PluginResult(PluginResult.Status.ERROR, msg);
            }

            callbackContext.sendPluginResult(result);
        } else if (ACTION_UNSUBSCRIBE.equals(action)) {
            isHandled = true;

            PluginResult result;
            try {

                String sid = args.getString(0);//[command.arguments objectAtIndex:0];

                result = doUnsubscribe(sid);

            } catch (JSONException e) {
                e.printStackTrace();
                String msg = String.format("Error processing arguments for %s: %s - arguments: %s", action, e,
                        args.toString());
                result = new PluginResult(PluginResult.Status.ERROR, msg);
            }

            callbackContext.sendPluginResult(result);
        } else if (ACTION_DISCONNECT.equals(action)) {
            isHandled = true;

            PluginResult result = doDisconnect();

            callbackContext.sendPluginResult(result);
        } else if (ACTION_SESSION_CONNECTION_CREATED_HANDLER.equals(action)) {
            isHandled = true;

            PluginResult result = this.setSessionConnectionCreatedHandler(callbackContext);

            callbackContext.sendPluginResult(result);
        } else if (ACTION_SESSION_CONNECTION_DESTROYED_HANDLER.equals(action)) {
            isHandled = true;

            PluginResult result = this.setSessionConnectionDestroyedHandler(callbackContext);

            callbackContext.sendPluginResult(result);
        } else if (ACTION_GET_SESSION_CONNECTION.equals(action)) {
            isHandled = true;

            doGetSessionConnection(callbackContext);
        } else if (ACTION_TOGGLE_AUDIO.equals(action)) {
            isHandled = true;

            PluginResult result;
            try {

                String sid = args.getString(0);

                result = doToggleAudio(sid);

            } catch (JSONException e) {
                e.printStackTrace();
                String msg = String.format("Error processing arguments for %s: %s - arguments: %s", action, e,
                        args.toString());
                result = new PluginResult(PluginResult.Status.ERROR, msg);
            }

            callbackContext.sendPluginResult(result);
        } else if (ACTION_REFRESH.equals(action)) {
            isHandled = true;

            PluginResult result;
            try {

                String sid = args.getString(0);

                result = doRefresh(sid);

            } catch (JSONException e) {
                e.printStackTrace();
                String msg = String.format("Error processing arguments for %s: %s - arguments: %s", action, e,
                        args.toString());
                result = new PluginResult(PluginResult.Status.ERROR, msg);
            }

            callbackContext.sendPluginResult(result);
        }

        return isHandled;
    }

    //TODO add generic sendError-method: takes PluginResult -> sends via _exceptionCallback if present (otherwise LOG output)
    private PluginResult setExceptionHandler(CallbackContext callbackContext) {
        this._exceptionCallback = callbackContext;

        PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
        result.setKeepCallback(true);
        return result;
    }

    @SuppressWarnings("serial")
    private class OpenTokPluginError extends Exception {
        public OpenTokPluginError(String errorMessage) {
            super(errorMessage);
        }
    }

    private boolean sendException(String errorMessage) {

        Exception exc = new OpenTokPluginError(errorMessage);

        //remove first element form stack trace, since we want the 
        //  calling method as first entry in the stack trace
        //  (and not this method itself)
        StackTraceElement[] stack = exc.getStackTrace();
        exc.setStackTrace(getStackTrace(stack, 1, stack.length - 1));

        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        exc.printStackTrace(pw);
        errorMessage = sw.toString();

        if (this._exceptionCallback != null) {

            PluginResult excResult = new PluginResult(PluginResult.Status.ERROR, errorMessage);
            excResult.setKeepCallback(true);
            OpenTokPlugin.this._exceptionCallback.sendPluginResult(excResult);

            return true;
        } else {
            LOG.e(PLUGIN_NAME, errorMessage, exc);
            return false;
        }
    }

    private StackTraceElement[] getStackTrace(StackTraceElement[] original, int startIndex, int endIndex) {
        StackTraceElement[] newStack = new StackTraceElement[endIndex - startIndex + 1];
        for (int i = startIndex, j = 0; i <= endIndex; ++i, ++j) {
            newStack[j] = original[i];
        }
        return newStack;
    };

    private PluginResult setStreamDisconnectHandler(CallbackContext callbackContext) {

        if (isDebug())
            LOG.d(PLUGIN_NAME, "Adding Stream Destroyed Event Listener");

        this._streamDisconnectedCallback = callbackContext;

        PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
        result.setKeepCallback(true);
        return result;
    }

    private PluginResult setSessionDisconnectHandler(CallbackContext callbackContext) {
        this._sessionDisconnectedCallback = callbackContext;

        PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
        result.setKeepCallback(true);
        return result;
    }

    private PluginResult setStreamCreatedHandler(CallbackContext callbackContext) {

        if (isDebug())
            LOG.d(PLUGIN_NAME, "Adding Stream Created Event Listener");

        this._streamCreatedCallback = callbackContext;

        PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
        result.setKeepCallback(true);
        return result;
    }

    private PluginResult setSessionConnectionCreatedHandler(CallbackContext callbackContext) {
        LOG.d(PLUGIN_NAME, "Adding Connection Created (in Session) Event Listener");
        this._sessionConnectionCreatedCallback = callbackContext;

        PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
        result.setKeepCallback(true);
        return result;
    }

    private PluginResult setSessionConnectionDestroyedHandler(CallbackContext callbackContext) {
        LOG.d(PLUGIN_NAME, "Adding Connection Destroyed (in Session) Event Listener");
        this._sessionConnectionDestroyedCallback = callbackContext;

        PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
        result.setKeepCallback(true);
        return result;
    }

    @SuppressLint("DefaultLocale")
    private PluginResult doUpdateView(String sid, int top, int left, int width, int height, int zIndex) {

        try {//FIXME TEST

            LOG.i(PLUGIN_NAME,
                    String.format(
                            "updateView with arguments: sid %s, top %d, left %d, width %d, height %d, zIndex %d",
                            sid, top, left, width, height, zIndex));

            if (_publisher != null && sid.equals(ID_PUBLISHER)) {
                LOG.i(PLUGIN_NAME, String.format("The Width is: %d", width));
                //         LayoutParams layoutParams = createLayoutParams(left, top, width, height);
                ////         _publisher.getView().setLeft(left);
                ////         _publisher.getView().setTop(top);
                //         _publisher.getView().setX(left);
                //         _publisher.getView().setY(top);
                //            publisherViewContainer.addView(mPublisher.getView(), layoutParams);

                //         this.publisherViewParams = new ViewParams(left, top, width, height, zIndex);
                //         LayoutParams params = this.publisherViewParams.create();//this.createLayoutParams(left, top, width, height);
                LayoutParams params = this.createLayoutParams(left, top, width, height);
                //           _publisher.getView().setLayoutParams(params);//frame = CGRectMake(left, top, width, height);

                doUpdateViewLayoutParams(_publisher.getView(), params);
                //           _publisher.view.layer.zPosition = zIndex;

                if (_publisherAudioIcon != null) {

                    LayoutParams iParams = createIconLayoutParams(top, left, width, height);
                    int micStatus = getIconResourceForPublisherStatus();
                    int micIconVisiblity = getIconVisibilityForPublisherStatus();

                    doUpdateViewIcon(_publisherAudioIcon, iParams, micStatus, micIconVisiblity);
                }
            }
            //    
            Subscriber streamInfo = subscriberDictionary.get(sid);

            //    if (streamInfo) {
            //        // Reposition the video feeds!
            //        streamInfo.view.frame = CGRectMake(left, top, width, height);
            //        streamInfo.view.layer.zPosition = zIndex;
            //    }

            if (streamInfo != null) {
                // Reposition the video feeds!

                ViewParams viewParams = new ViewParams(left, top, width, height, zIndex);//subscriberViewParams.get(sid);
                subscriberViewParams.put(sid, viewParams);
                LayoutParams newPosition = this.createLayoutParams(left, top, width, height);//viewParams.create();// this.createLayoutParams(left, top, width, height);
                //        streamInfo.getView().setLayoutParams(newPosition);
                doUpdateViewLayoutParams(streamInfo.getView(), newPosition);

                ImageView subscriberAudioIcon = subscriberAudioIconDictionary.get(sid);
                if (subscriberAudioIcon != null) {

                    LayoutParams iParams = createIconLayoutParams(top, left, width, height);
                    int speakerStatus = getIconResourceForSubscriberStatus(streamInfo);
                    int speakerIconVisiblity = getIconVisibilityForSubscriberStatus(streamInfo);

                    doUpdateViewIcon(subscriberAudioIcon, iParams, speakerStatus, speakerIconVisiblity);
                }
            }

            PluginResult result = new PluginResult(PluginResult.Status.OK,
                    String.format("updateView [stream %s, top %d, left %d, width %d, height %d, zIndex %d]", sid,
                            top, left, width, height, zIndex));
            result.setKeepCallback(true);
            return result;

        } catch (Exception e) {
            String msg = String.format(
                    "error during updateView with arguments: sid %s, top %d, left %d, width %d, height %d, zIndex %d",
                    sid, top, left, width, height, zIndex);
            System.err.println(msg);
            e.printStackTrace();
            LOG.e(PLUGIN_NAME, msg, e);
            throw (new RuntimeException(
                    "This is a 'proxied' Exception (for 'real' Exception, see first entry in stack-trace)", e));
        }

    }

    private void doUpdateViewIcon(final ImageView icon, final LayoutParams newLayoutParams, final int ressourceId,
            final int visibility) {
        this.cordova.getActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                icon.setLayoutParams(newLayoutParams);
                icon.setImageResource(ressourceId);
                icon.setVisibility(visibility);
            }
        });
    }

    private void doUpdateViewIconStatus(final ImageView icon, final int ressourceId, final int visibility) {
        this.cordova.getActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                icon.setImageResource(ressourceId);
                icon.setVisibility(visibility);
            }
        });
    }

    private void doUpdateViewLayoutParams(final View view, final LayoutParams newLayoutParams) {
        this.cordova.getActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                view.setLayoutParams(newLayoutParams);
            }
        });
    }

    private PluginResult doInitSession(String sessionId) {
        // Create Session
        _session = Session.newInstance(this.cordova.getActivity(), sessionId, this.mListener);
        // Initialize Dictionary, contains DOM info for every stream
        subscriberDictionary = new HashMap<String, Subscriber>();
        streamDictionary = new HashMap<String, Stream>();
        subscriberViewParams = new HashMap<String, OpenTokPlugin.ViewParams>();

        subscriberAudioIconDictionary = new HashMap<String, ImageView>();

        //TODO support multiple sessions?
        _publisher = null;
        mListener.reset();
        _sessionConnectedCallback = null;
        _streamCreatedCallback = null;
        _exceptionCallback = null;
        _streamDisconnectedCallback = null;
        _sessionDisconnectedCallback = null;
        _sessionConnectionCreatedCallback = null;
        _sessionConnectionDestroyedCallback = null;

        // Return Result
        return new PluginResult(PluginResult.Status.OK, "initSession [sessionId " + sessionId + "]");
    }

    private PluginResult doInitPublisher(int top, int left, int width, int height, String name,
            boolean publishAudio, boolean publishVideo, int zIndex) {

        // Publish and set View
        _publisher = Publisher.newInstance(this.cordova.getActivity(), this.mListener, name);

        _publisher.setPublishAudio(publishAudio);
        _publisher.setPublishVideo(publishVideo);

        LayoutParams params = this.createLayoutParams(left, top, width, height);

        if (isInfo())
            LOG.i(PLUGIN_NAME,
                    String.format("Adding view for publisher '%s' at (%d,%d), width %d, height %d (layer: %d)",
                            name, left, top, width, height, zIndex));

        viewAdministrator.addView(this.cordova.getActivity(), _webView, _publisher.getView(), params);

        _publisherAudioIcon = createPublisherAudioIcon();
        LayoutParams iconPosition = createIconLayoutParams(top, left, width, height);
        viewAdministrator.addView(this.cordova.getActivity(), _webView, _publisherAudioIcon, iconPosition);

        // Return to Javascript
        return new PluginResult(PluginResult.Status.OK,
                String.format("initPublisher [stream '%s' at (%d,%d), width %d, height %d (z-layer: %d)]", name,
                        left, top, width, height, zIndex));
    }

    private ImageView createPublisherAudioIcon() {

        int micStatus = getIconResourceForPublisherStatus();
        int micIconVisiblity = getIconVisibilityForPublisherStatus();

        return createAudioIcon(micStatus, micIconVisiblity);
    }

    private int getIconVisibilityForPublisherStatus() {
        if (_publisher == null) {
            return View.INVISIBLE;
        }
        return _publisher.getPublishAudio() ? View.INVISIBLE : View.VISIBLE;
    }

    private int getIconResourceForPublisherStatus() {

        if (_publisher == null) {
            return R.drawable.opentok_button_mic_off;
        }

        return _publisher.getPublishAudio() ? R.drawable.opentok_button_mic_on : R.drawable.opentok_button_mic_off;
    }

    private ImageView createSubscriberAudioIcon(Subscriber subscriber) {

        int speakerStatus = getIconResourceForSubscriberStatus(subscriber);
        int speakerIconVisiblity = getIconVisibilityForSubscriberStatus(subscriber);

        return createAudioIcon(speakerStatus, speakerIconVisiblity);
    }

    private int getIconVisibilityForSubscriberStatus(Subscriber subscriber) {
        if (subscriber == null) {
            return View.INVISIBLE;
        }
        return subscriber.getSubscribeToAudio() ? View.INVISIBLE : View.VISIBLE;
    }

    private int getIconResourceForSubscriberStatus(Subscriber subscriber) {

        if (subscriber == null) {
            return R.drawable.opentok_button_speaker_off;
        }

        return subscriber.getSubscribeToAudio() ? R.drawable.opentok_button_speaker_on
                : R.drawable.opentok_button_speaker_off;
    }

    private ImageView createAudioIcon(int initialResourceId, int initialVisibility) {

        ImageView imgView = new ImageView(this.cordova.getActivity());

        imgView.setImageResource(initialResourceId);
        imgView.setVisibility(initialVisibility);

        return imgView;
    }

    private LayoutParams createIconLayoutParams(int parentViewTop, int parentViewLeft, int parentViewWidth,
            int parentViewHeight) {

        //TODO constants?
        int offsetX = 10;
        int offsetY = 10;

        //TODO constants?
        int iw = 32;
        int ih = 32;

        //TODO verify that icon fits within parent view (-> size)
        int it = parentViewTop + parentViewHeight - ih - offsetY;
        int il = parentViewLeft + parentViewWidth - iw - offsetX;

        return this.createLayoutParams(il, it, iw, ih);
    }

    private PluginResult doDestroyPublisher() {

        ImageView thePublisherAudioIcon = _publisherAudioIcon;
        if (thePublisherAudioIcon != null) {
            _publisherAudioIcon = null;
            viewAdministrator.removeView(this.cordova.getActivity(), thePublisherAudioIcon);
        }

        Publisher thePublisher = _publisher;
        if (thePublisher != null) {
            _publisher = null;
            viewAdministrator.removeView(this.cordova.getActivity(), thePublisher.getView());
            if (this.isPublishing) {
                this.isPublishing = false;
                thePublisher.destroy();
            } else {
                this.doDebug("publisher already disposed.", "destroyPublisher");
            }
        } else {
            return new PluginResult(PluginResult.Status.ERROR,
                    "Could not destroy VIEW for publisher: publisher is NULL!");////////////////// EARLY EXIT //////////////////////////
        }

        return new PluginResult(PluginResult.Status.OK, "destroyPublisher");
    }

    private void doConnect(String key, String token, CallbackContext callbackContext) {
        if (!statusIsDeviceOnPause) {
            this._sessionConnectedCallback = callbackContext;
            _session.connect(key, token);
        }
    }

    private PluginResult doDisconnect() {
        _session.disconnect();

        //        Log.d("OPENTOK", "removing views - if any");
        // remove all views used by opentok
        // TODO: check if necessary here
        viewAdministrator.removeAllViews(cordova.getActivity());
        //        Log.d("OPENTOK", "removed views.");
        return new PluginResult(PluginResult.Status.OK, "disconnect");
    }

    private PluginResult doPublish() {
        if (!statusIsDeviceOnPause) {

            _session.publish(_publisher);

            if (_publisherAudioIcon != null) {

                doUpdateViewIconStatus(_publisherAudioIcon, getIconResourceForPublisherStatus(),
                        getIconVisibilityForPublisherStatus());

            } else {
                this.sendException("Could not updated audio status icon for publisher: resource is null");
            }

            return new PluginResult(PluginResult.Status.OK, "publish");

        } else {
            return new PluginResult(PluginResult.Status.ERROR, "Could not publsh - Reason: on pause.");
        }
    }

    private PluginResult doUnpublish() {

        if (this._publisher != null) {
            if (this.isPublishing) {
                _session.unpublish(this._publisher);
            } else if (isInfo()) {
                this.doDebug("publisher already disposed.", "unpublish");
            }
        } else {
            return new PluginResult(PluginResult.Status.ERROR, "Could not unpublish: publisher is NULL!");////////////////// EARLY EXIT //////////////////////////
        }

        return new PluginResult(PluginResult.Status.OK, "unpublish");
    }

    private PluginResult doSubscribe(String sid, int top, int left, int width, int height,
            boolean isSubscribeToVideo, int zIndex) {

        if (!statusIsDeviceOnPause) {

            // Acquire Stream, then create a subscriber object and put it into dictionary
            Stream stream = streamDictionary.get(sid);
            Subscriber subscriber = Subscriber.newInstance(this.cordova.getActivity(), stream, this.mListener);
            subscriberDictionary.put(stream.getStreamId(), subscriber);
            subscriber.setSubscribeToVideo(isSubscribeToVideo);

            _session.subscribe(subscriber);

            ViewParams viewParams = new ViewParams(top, left, width, height, zIndex);
            subscriberViewParams.put(stream.getStreamId(), viewParams);
            LayoutParams params = this.createLayoutParams(left, top, width, height);//viewParams.create();//this.createLayoutParams(left, top, width, height);
            //      subscriber.getView().setLayoutParams(params);
            //      ((OpenTokExample)this.cordova.getActivity()).addView(subscriber.getView(), params);

            if (isInfo())
                LOG.i(PLUGIN_NAME, String.format(
                        "Adding subscriber (stream %s) for publisher at (%d,%d), width %d, height %d (layer: %d)%s",
                        sid, left, top, width, height, zIndex,
                        isSubscribeToVideo ? "" : " IS NOT SUSCRIBING TO VIDEO!"));

            viewAdministrator.addView(this.cordova.getActivity(), _webView, subscriber.getView(), params);

            ImageView subscriberAudioIcon = createSubscriberAudioIcon(subscriber);
            subscriberAudioIconDictionary.put(sid, subscriberAudioIcon);
            LayoutParams iParams = this.createIconLayoutParams(top, left, width, height);
            viewAdministrator.addView(this.cordova.getActivity(), _webView, subscriberAudioIcon, iParams);

            // Return to JS event handler
            return new PluginResult(PluginResult.Status.OK,
                    String.format("subscribe [stream %s at (%d,%d), width %d, height %d (z-layer: %d)%s]", sid,
                            left, top, width, height, zIndex, isSubscribeToVideo ? "" : " _disabled video_ "));
        } else {

            // Return to JS event handler
            return new PluginResult(PluginResult.Status.ERROR, String.format(
                    "could not subscribe to [stream %s at (%d,%d), width %d, height %d (z-layer: %d)%s] -- Reason: device is on pause.",
                    sid, left, top, width, height, zIndex, isSubscribeToVideo ? "" : " _disabled video_ "));
        }
    }

    private PluginResult doUnsubscribe(String sid) {
        return doUnsubscribe(sid, false);
    }

    private PluginResult doUnsubscribe(String sid, boolean onlyCleanUp) {
        Subscriber subscriber = subscriberDictionary.get(sid);
        //      subscriber.getStream().getConnection().

        if (subscriber == null) {

            if (isError())
                LOG.e(PLUGIN_NAME,
                        String.format("unsubscribe(%s): coud not find subscriber for this stream.", sid));

            //TODO should this return an error?
            return new PluginResult(PluginResult.Status.OK, "unsubscribe: no subscriber for stream " + sid); ////////////////////////////////////////////////////////////// EARLY EXIT ////////////
        }

        ImageView audioIcon = subscriberAudioIconDictionary.get(sid);
        viewAdministrator.removeView(this.cordova.getActivity(), audioIcon);
        subscriberAudioIconDictionary.remove(sid);

        viewAdministrator.removeView(this.cordova.getActivity(), subscriber.getView());

        if (!onlyCleanUp) {
            _session.unsubscribe(subscriber);
        }

        subscriberDictionary.remove(sid);

        // Return to JS event handler
        return new PluginResult(PluginResult.Status.OK, "unsubscribe [stream " + sid + "]");
    }

    private void doTBTesting(CallbackContext callbackContext) {

        if (_exceptionCallback == null) {
            _exceptionCallback = callbackContext;
        }

        JSONObject response = new JSONObject();
        try {
            response.put("message", "HMMM Test Error!");
        } catch (JSONException e) {
            //         e.printStackTrace();
            if (isError())
                LOG.e(PLUGIN_NAME, "Could not create response (no ERROR listener registered)", e);
        }

        PluginResult result = new PluginResult(PluginResult.Status.OK, response);
        result.setKeepCallback(true);

        callbackContext.sendPluginResult(result);
    }

    private PluginResult doToggleAudio(String streamId) {

        PluginResult result = null;
        if (_publisher != null && streamId.equals(ID_PUBLISHER)) {

            boolean newAudioSetting = !_publisher.getPublishAudio();

            if (isInfo())
                LOG.i(PLUGIN_NAME, String.format("Toggle audio for publisher: mute %s", newAudioSetting));

            _publisher.setPublishAudio(newAudioSetting);

            doUpdateViewIconStatus(_publisherAudioIcon, getIconResourceForPublisherStatus(),
                    getIconVisibilityForPublisherStatus());

            result = new PluginResult(PluginResult.Status.OK, "toggleAudio [stream " + streamId + ", publisher]");
        }

        Subscriber streamInfo = subscriberDictionary.get(streamId);
        if (streamInfo != null) {

            boolean newAudioSetting = !streamInfo.getSubscribeToAudio();

            if (isInfo())
                LOG.i(PLUGIN_NAME, String.format("Toggle audio for Subscriber (stream-ID: %s): mute %s", streamId,
                        newAudioSetting));

            streamInfo.setSubscribeToAudio(newAudioSetting);

            ImageView subscriberAudioIcon = subscriberAudioIconDictionary.get(streamId);
            doUpdateViewIconStatus(subscriberAudioIcon, getIconResourceForSubscriberStatus(streamInfo),
                    getIconVisibilityForSubscriberStatus(streamInfo));

            result = new PluginResult(PluginResult.Status.OK, "toggleAudio [stream " + streamId + ", subscriber]");
        }

        if (result == null) {
            result = new PluginResult(PluginResult.Status.ERROR,
                    "No Subscriber / Publisher for streamId " + streamId);
        }

        return result;
    }

    private PluginResult doRefresh(String streamId) {

        PluginResult result = null;
        if (_publisher != null && streamId.equals(ID_PUBLISHER)) {

            boolean isAudio = _publisher.getPublishAudio();
            boolean isVideo = _publisher.getPublishVideo();

            _publisher.setPublishAudio(false);
            _publisher.setPublishVideo(false);

            _publisher.setPublishAudio(isAudio);
            _publisher.setPublishVideo(isVideo);

            //BUGFIX there seems to a problem, with the correct positioning ... trigger update through JavaScript:
            this.webView.sendJavascript(
                    "if(typeof TB !== 'undefined' && TB.updateViews){ TB.updateViews(); } else {console.error('could not enfore view update for subscribers: missing TB.updateViews() function!');}");

            if (isInfo())
                LOG.i(PLUGIN_NAME, String.format("Refreshing view for publisher..."));

            result = new PluginResult(PluginResult.Status.OK, "refresh [stream " + streamId + ", publisher]");
        }

        Subscriber streamInfo = subscriberDictionary.get(streamId);
        if (streamInfo != null) {

            //REFRESH: remove and re-add the subscriber

            ViewParams p = subscriberViewParams.get(streamId);
            doUnsubscribe(streamId);

            doSubscribe(streamId, p.top, p.left, p.width, p.height, streamInfo.getSubscribeToVideo(), p.zIndex);
            //BUGFIX there seems to a problem, with the correct positioning ... trigger update through JavaScript:
            this.webView.sendJavascript(
                    "if(typeof TB !== 'undefined' && TB.updateViews){ TB.updateViews(); } else {console.error('could not enfore view update for subscribers: missing TB.updateViews() function!');}");

            if (isInfo())
                LOG.i(PLUGIN_NAME, String.format("Refreshing view for Subscriber (stream-ID: %s)...", streamId));

            result = new PluginResult(PluginResult.Status.OK, "refresh [stream " + streamId + ", subscriber]");
        }

        if (result == null) {
            result = new PluginResult(PluginResult.Status.ERROR,
                    "No Subscriber / Publisher for streamId " + streamId);
        }

        return result;
    }

    private LayoutParams createLayoutParams(int left, int top, int width, int height) {
        //      FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(dpToPx(width), dpToPx(height));
        //      params.leftMargin = dpToPx(left);
        //      params.topMargin = dpToPx(top);
        //      
        //      //TODO set zIndex ?
        ////      params.??
        //      return params;

        return new AbsoluteLayout.LayoutParams(dpToPx(width), dpToPx(height), dpToPx(left), dpToPx(top));

        //      RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(dpToPx(width), dpToPx(height));
        //      params.leftMargin = dpToPx(left);
        //      params.topMargin = dpToPx(top);
        //      
        //      return params;

        //      FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
        //            FrameLayout.LayoutParams.MATCH_PARENT,
        //            FrameLayout.LayoutParams.MATCH_PARENT
        //      );
        //      params.leftMargin = dpToPx(left);
        //      params.topMargin = dpToPx(top);
        //      params.height = dpToPx(height);
        //      params.width = dpToPx(width);
        //      
        //      return params;
    }

    private int extractDp(JSONArray args, int index) throws JSONException {

        JSONException failure = null;
        int result = 0;
        try {
            result = args.getInt(index);
        } catch (JSONException e) {
            failure = e;
        }

        if (failure != null) {
            String temp = args.getString(index);

            //simple decimal number pattern
            Pattern p = Pattern.compile("([+-]?\\d+([,.]\\d+)?)");
            Matcher m = p.matcher(temp);
            if (m.find()) {
                result = Integer.parseInt(temp.substring(m.start(), m.end()));
            }
        }

        return result;
    }

    /**
     * Converts dp to real pixels, according to the screen density.
     * @param dp A number of density-independent pixels.
     * @return The equivalent number of real pixels.
     */
    private int dpToPx(int dp) {
        double screenDensity = this.cordova.getActivity().getResources().getDisplayMetrics().density;
        return (int) (screenDensity * (double) dp);
    }

    private CallbackContext retrieveSessionConnectionCallback;

    private void doGetSessionConnection(CallbackContext callbackContext) {

        retrieveSessionConnectionCallback = callbackContext;

        if (this.mListener.publisherConnectionId != null) {
            doSendSessionConnectionData();
        } else {
            PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
            result.setKeepCallback(true);
            callbackContext.sendPluginResult(result);
        }
    }

    /**
     * WORKAROUND: the current Android impl. does not provide the session's connection on connecting
     *          -> if the JAVASCRIPT code receives the connect-event without a connection for the session
     *             it will register a callback
     *          -> when the publisher starts, we use its connection as session-connection
     * 
     * If there was no callback set, this method does nothing.
     * 
     * After successfully sending the connection-information, the callback-instance will be reset to NULL,
     * i.e. the information will only be sent 1 time.
     * 
     */
    private void doSendSessionConnectionData() {

        if (retrieveSessionConnectionCallback != null) {
            // After session is successfully connected, the connection property is available
            JSONObject connData = new JSONObject();
            PluginResult result;
            try {

                String sessionConnectionId = this.mListener.getConnectionId(_session);
                connData.put("connectionId", sessionConnectionId);
                connData.put("data", _session.getConnection().getData());
                String creationTime = String.format("%d", _session.getConnection().getCreationTime().getTime());
                connData.put("creationTime", creationTime);

                result = new PluginResult(PluginResult.Status.OK, connData);

            } catch (JSONException e) {

                if (isError())
                    LOG.e(PLUGIN_NAME, "Could not create response", e);

                result = new PluginResult(PluginResult.Status.ERROR, "Could not create response object: " + e);
            }

            retrieveSessionConnectionCallback.sendPluginResult(result);
            retrieveSessionConnectionCallback = null;
        }
    }

    @SuppressLint("DefaultLocale")
    private class Listener implements Session.Listener, Publisher.Listener, Subscriber.Listener {

        private static final String NAME = "OpenTokPlugin.Listener";

        private String publisherConnectionId = null;
        private String publisherStreamId = null;

        public void reset() {
            this.publisherStreamId = null;
            this.publisherConnectionId = null;
        }

        @Override
        public void onSubscriberConnected(Subscriber subscriber) {

            if (isInfo())
                Log.i(NAME, "Subscriber connected.");

            if (isDebug()) {
                doDebug(debugSubscriber(subscriber, null), "onSubscriberConnected");
                doDebug(debugSession(_session, null), "onSubscriberConnected");
                doDebug(debugPublisher(_publisher, null), "onSubscriberConnected");
            }
        }

        @Override
        public void onSubscriberException(Subscriber subscriber, OpentokException exc) {

            if (isWarn())
                LOG.w(NAME, "Subscriber exception", exc);

            if (OpenTokPlugin.this._streamDisconnectedCallback != null) {

                String connId = subscriber.getStream().getConnection().getConnectionId();
                PluginResult result = new PluginResult(PluginResult.Status.OK, connId);
                result.setKeepCallback(true);

                OpenTokPlugin.this._streamDisconnectedCallback.sendPluginResult(result);

            } else if (isError()) {
                LOG.e(NAME, "Subscriber exception " + debugSubscriber(subscriber, null), exc);
            }

        }

        @Override
        public void onSubscriberVideoDisabled(Subscriber subscriber) {

            if (isInfo())
                Log.i(NAME, "The subscriber disabled video.");

        }

        @Override
        public void onPublisherChangedCamera(int id) {

            if (isInfo())
                Log.i(NAME, "The publisher changed camera to " + id);

            if (isDebug()) {
                doDebug(debugSession(_session, null), "onPublisherChangedCamera_" + id);
                doDebug(debugPublisher(_publisher, null), "onPublisherChangedCamera_" + id);
            }
        }

        @Override
        public void onPublisherException(OpentokException exc) {

            if (isWarn())
                LOG.w(NAME, "Publisher didFailWithError", exc);

            if (_exceptionCallback != null) {

                //            ErrorCode code = exc.getErrorCode();
                JSONObject response = new JSONObject();
                try {
                    response.put("message", exc.getMessage());
                    //               response.put("code", code.getErrorCode());
                } catch (JSONException e) {
                    //               e.printStackTrace();
                    if (isError())
                        LOG.e(NAME, "Could not create response", e);
                }

                PluginResult result = new PluginResult(PluginResult.Status.OK, response);
                result.setKeepCallback(true);

                OpenTokPlugin.this._exceptionCallback.sendPluginResult(result);
            }
        }

        @Override
        public void onPublisherStreamingStarted() {

            if (isInfo())
                Log.i(NAME, "The publisher started streaming.");

            isPublishing = true;
            publisherStreamId = _publisher.getStreamId();

            if (isDebug()) {
                doDebug(debugSession(_session, null), "onPublisherStreamingStarted");
                doDebug(debugPublisher(_publisher, null), "onPublisherStreamingStarted");
            }
        }

        @Override
        public void onPublisherStreamingStopped() {

            if (isInfo())
                Log.i(NAME, "The publisher stopped streaming.");

            isPublishing = false;
            //          publisherStreamId = null;

            if (isDebug()) {
                doDebug(debugSession(_session, null), "onPublisherStreamingStopped");
                //            doDebug( debugPublisher(_publisher, null), "onPublisherStreamingStopped");
            }

            String key = this.createStreamDroppedResponseDataStr(this.publisherStreamId,
                    this.publisherConnectionId);

            //TODO remove from streamDictionary?

            PluginResult result = new PluginResult(PluginResult.Status.OK, key);
            result.setKeepCallback(true);

            OpenTokPlugin.this._streamDisconnectedCallback.sendPluginResult(result);
        }

        @Override
        public void onSessionConnected() {

            if (isInfo())
                LOG.i(NAME, "Session connected");

            if (isDebug()) {
                doDebug(debugSession(_session, null), "onSessionConnected");
                //            doDebug( debugPublisher(_publisher, null), "onSessionConnected");
            }

            JSONObject response = new JSONObject();
            try {

                // SessionConnectionStatus
                response.put("sessionConnectionStatus", "OTSessionConnectionStatusConnected");//FIXME at this point in the Android SDK, we seem to always have a connected session...

                // SessionId
                //            response.put("sessionId", _session.getId());//FIXME use ID from connect-call?
                response.put("connectionCount", "1");//FIXME currently there is only 1 connection per session allowed...

                // SessionStreams
                ArrayList<JSONObject> streamList = new ArrayList<JSONObject>();
                for (Stream stream : streamDictionary.values()) {
                    JSONObject streamData = createStreamJSON(stream);
                    streamList.add(streamData);
                }
                response.put("streams", new JSONArray(streamList));

                // After session is successfully connected, the connection property is available
                JSONObject connData = new JSONObject();

                String sessionConnectionId = this.getConnectionId(_session);
                connData.put("connectionId", sessionConnectionId);

                connData.put("data", _session.getConnection().getData());
                String creationTime = String.format("%d", _session.getConnection().getCreationTime().getTime());
                connData.put("creationTime", creationTime);
                response.put("connection", connData);

                // Session Environment
                // Changed to production by default
                response.put("environment", "production");

            } catch (JSONException e) {
                e.printStackTrace();
                LOG.e(NAME, "Could not create response", e);
            }

            // After session dictionary is constructed, return the result!
            PluginResult result = new PluginResult(PluginResult.Status.OK, response);

            OpenTokPlugin.this._sessionConnectedCallback.sendPluginResult(result);
        }

        @Override
        public void onSessionCreatedConnection(Connection connection) {

            if (isInfo())
                Log.i(NAME, "Session: created connection, id " + connection.getConnectionId());

            if (isDebug()) {
                doDebug(debugConnection(connection, null), "onSessionCreatedConnection");
                doDebug(debugSession(_session, null), "onSessionCreatedConnection");
                doDebug(debugPublisher(_publisher, null), "onSessionCreatedConnection");
            }

            //NOTE: currently, the OpenTok Android library does not seem to trigger this event/listener-method (-> use stream-based events instead)...
            if (OpenTokPlugin.this._sessionConnectionCreatedCallback != null) {

                JSONObject response = new JSONObject();
                try {
                    //               response.put("reason", null);
                    response.put("type", "connectionCreated");

                    //NOTE: using "connection" instead of "connections", since we only ever
                    //      have 1 connection here -> the JS event however, should put the in an array for property "connections"
                    response.put("connection", createConnectionJSON(connection));
                } catch (JSONException e) {
                    e.printStackTrace();
                    LOG.e(NAME, "Could not create response", e);
                }

                PluginResult result = new PluginResult(PluginResult.Status.OK, response);
                result.setKeepCallback(true);

                OpenTokPlugin.this._sessionConnectionCreatedCallback.sendPluginResult(result);
            }
        }

        @Override
        public void onSessionDisconnected() {

            if (isInfo())
                LOG.i(NAME, String.format("Session disconnected: (%s)", _session));

            if (isDebug()) {
                doDebug(debugSession(_session, null), "onSessionDisconnected");
                doDebug(debugPublisher(_publisher, null), "onSessionDisconnected");
            }

            //FIXME cleanup? clear streamDictionary, subscriberDictionary?
            List<String> subscriberIds = new LinkedList<String>(subscriberDictionary.keySet());
            for (String id : subscriberIds) {
                //handle subscribers:
                // * remove subscriber & their view
                // * TODO? dispatch stream destroyed event for subscriber (not cancelable)

                doUnsubscribe(id, true);
            }

            //handle publisher:
            if (_publisher != null) {
                // * TODO? dispatch stream destroyed event for publisher; cancelable -> if NOT canceled, destroy publisher
                doDestroyPublisher();
            }

            JSONObject response = new JSONObject();
            try {
                response.put("reason", "networkDisconnected");
                response.put("type", "sessionDisconnected");
            } catch (JSONException e) {
                e.printStackTrace();
                LOG.e(NAME, "Could not create response", e);
            }

            PluginResult result = new PluginResult(PluginResult.Status.OK, response);
            result.setKeepCallback(true);

            OpenTokPlugin.this._sessionDisconnectedCallback.sendPluginResult(result);
        }

        @Override
        public void onSessionDroppedConnection(Connection connection) {

            if (isInfo())
                Log.i(NAME, "Session: dropped connection, id " + connection.getConnectionId());

            if (isDebug()) {
                doDebug(debugConnection(connection, null), "onSessionDroppedConnection");
                doDebug(debugSession(_session, null), "onSessionDroppedConnection");
                doDebug(debugPublisher(_publisher, null), "onSessionDroppedConnection");
            }

            //NOTE: currently, the OpenTok Android library does not seem to trigger this event/listener-method (-> use stream-based events instead)...
            if (OpenTokPlugin.this._sessionConnectionDestroyedCallback != null) {

                JSONObject response = new JSONObject();
                try {
                    response.put("reason", "clientDisconnected");
                    response.put("type", "connectionDestroyed");

                    //NOTE: using "connection" instead of "connections", since we only ever
                    //      have 1 connection here -> the JS event however, should put the in an array for property "connections" 
                    response.put("connection", createConnectionJSON(connection));
                } catch (JSONException e) {
                    e.printStackTrace();
                    LOG.e(NAME, "Could not create response", e);
                }

                PluginResult result = new PluginResult(PluginResult.Status.OK, response);
                result.setKeepCallback(true);

                OpenTokPlugin.this._sessionConnectionDestroyedCallback.sendPluginResult(result);

            }
        }

        @Override
        public void onSessionDroppedStream(Stream stream) {

            if (isInfo())
                LOG.i(NAME, "Dropped Stream");

            if (isDebug()) {
                doDebug(debugStream(stream, null), "onSessionDroppedStream");
                doDebug(debugSession(_session, null), "onSessionDroppedStream");
                doDebug(debugPublisher(_publisher, null), "onSessionDroppedStream");
            } else if (isInfo()) {
                doDebug(String.format(
                        "dropping stream.id %s \t - current_session: publisher.streamId= %s | publisher.connection.connectionId= %s",
                        stream.getStreamId(), this.publisherStreamId, this.publisherConnectionId),
                        "onSessionDroppedStream");
            }

            String key = this.createStreamDroppedResponseDataStr(stream);

            //TODO remove from streamDictionary?

            PluginResult result = new PluginResult(PluginResult.Status.OK, key);
            result.setKeepCallback(true);

            OpenTokPlugin.this._streamDisconnectedCallback.sendPluginResult(result);
        }

        @Override
        public void onSessionException(OpentokException exc) {

            if (isInfo())
                LOG.e(NAME, "Session did not Connect", exc);

            if (_exceptionCallback != null) {

                ErrorCode code = exc.getErrorCode();
                JSONObject response = new JSONObject();
                try {
                    response.put("message", exc.getMessage());
                    response.put("code", code.getErrorCode());
                } catch (JSONException e) {
                    e.printStackTrace();
                    LOG.e(NAME, "Could not create response", e);
                }

                PluginResult result = new PluginResult(PluginResult.Status.OK, response);
                result.setKeepCallback(true);

                OpenTokPlugin.this._exceptionCallback.sendPluginResult(result);
            }
        }

        @Override
        public void onSessionReceivedStream(Stream stream) {

            if (isInfo())
                LOG.i(NAME, "Received Stream");

            if (isDebug()) {
                doDebug(debugStream(stream, null), "onSessionReceivedStream");
                doDebug(debugSession(_session, null), "onSessionReceivedStream");
                //            doDebug( debugPublisher(_publisher, null), "onSessionReceivedStream");
            }

            streamDictionary.put(stream.getStreamId(), stream);

            //         LOG.e(NAME, String.format("onSessionReceivedStream: streamId %s, connnectionId %s \t(publisherStreamId %s)", stream.getStreamId(), stream.getConnection().getConnectionId(),  _publisher.getStreamId()));

            //         Session pubSession = _publisher.getSession();
            //         int camperaId = _publisher.getCameraId();
            if (stream.getStreamId().equalsIgnoreCase(publisherStreamId)) {
                //FIXME actually, onSessionConnected we do not yet have the stream of the publisher AND in Android Session.connection has no valid id...
                //      ... WORKAROUND: trigger session-connected event again here and use the publisher's stream-id as the session's connection-id
                //                  (we need the session's connection id for detecting, if session-received-stream events carry our own stream...)

                if (publisherConnectionId == null) {
                    publisherConnectionId = stream.getConnection().getConnectionId();

                    doSendSessionConnectionData();
                }
            }

            String data = createStreamDroppedResponseDataStr(stream);
            PluginResult result = new PluginResult(PluginResult.Status.OK, data);
            result.setKeepCallback(true);

            OpenTokPlugin.this._streamCreatedCallback.sendPluginResult(result);
        }

        private String getConnectionId(Connection conn) {
            //FIXME currently, we cannot get a valid connection ID for the session's connection...
            //      WORDAROUND: set ID to null, if no connection is present (see also doSendSessionConnectionData())

            return conn == null ? null : conn.getConnectionId();
        }

        private String getConnectionId(Session session) {
            //FIXME currently, we cannot get a valid connection ID for the session's connection...
            //      WORDAROUND: use the publisher's connection (see doSendSessionConnectionData())
            return publisherConnectionId;
        }

        private String createStreamDroppedResponseDataStr(Stream s) {
            String connId = this.getConnectionId(s.getConnection());
            return createStreamDroppedResponseDataStr(s.getStreamId(), connId);
        }

        private String createStreamDroppedResponseDataStr(String streamId, String connectionId) {
            return String.format("%s %s", connectionId, streamId);
        }

        private JSONObject createStreamJSON(Stream stream) throws JSONException {
            JSONObject streamData = new JSONObject();

            streamData.put("streamId", stream.getStreamId());

            Connection conn = stream.getConnection();
            JSONObject connData = createConnectionJSON(conn);

            streamData.put("connection", connData);
            return streamData;
        }

        private JSONObject createConnectionJSON(Connection connection) throws JSONException {
            JSONObject connData = new JSONObject();

            connData.put("connectionId", this.getConnectionId(connection));

            connData.put("data", connection.getData());
            String creationTime = String.format("%d", connection.getCreationTime().getTime());
            connData.put("creationTime", creationTime);
            return connData;
        }

    }

    private boolean isDebug() {
        return _debug.ordinal() >= DebugLevel.DEBUG.ordinal();
    }

    private boolean isInfo() {
        return _debug.ordinal() >= DebugLevel.INFO.ordinal();
    }

    private boolean isWarn() {
        return _debug.ordinal() >= DebugLevel.WARN.ordinal();
    }

    private boolean isError() {
        return _debug.ordinal() >= DebugLevel.ERROR.ordinal();
    }

    @SuppressWarnings("unused")
    private void doDebug(String str) {
        doDebug(str, null);
    }

    private void doDebug(String str, String prefix) {
        String tag = "OpenTokPlugin.DEBUGINFO";
        if (prefix == null || prefix.length() < 1) {
            prefix = "";
        } else {
            tag += "-" + prefix;
            prefix += " - ";
        }
        LOG.e(tag, prefix + str);
    }

    private String debugSession(Session s, String linePrefix) {

        if (linePrefix == null)
            linePrefix = "";

        if (s == null) {
            return linePrefix + "Session NULL !!!!\n"; ////////// EARLY EXIT //////////
        }

        String str = linePrefix + s.getClass() + "\n";

        str += debugConnection(s.getConnection(), linePrefix + " \t");

        return str + linePrefix + "------- session.end ----------\n\n";
    }

    private String debugStream(Stream s, String linePrefix) {

        if (linePrefix == null)
            linePrefix = "";

        if (s == null) {
            return linePrefix + "Stream NULL !!!!\n"; ////////// EARLY EXIT //////////
        }

        String str = linePrefix + s.getClass() + "\n";

        str += linePrefix + "  id       " + s.getStreamId() + "\n";
        str += linePrefix + "  name     " + s.getName() + "\n";
        str += linePrefix + "  time     " + s.getCreationTime() + "\n";
        str += linePrefix + "  name     " + s.getName() + "\n";

        str += linePrefix + "  v-width  " + s.getVideoWidth() + "\n";
        str += linePrefix + "  v-height " + s.getVideoHeight() + "\n";

        str += debugConnection(s.getConnection(), linePrefix + " \t");

        return str + "\n\n";
    }

    private String debugConnection(Connection c, String linePrefix) {

        if (linePrefix == null)
            linePrefix = "";

        if (c == null) {
            return linePrefix + "Connection NULL !!!!\n"; ////////// EARLY EXIT //////////
        }

        String str = "";
        str += linePrefix + c.getClass() + "\n";
        str += linePrefix + "  id   " + c.getConnectionId() + "\n";
        str += linePrefix + "  hash " + (c.getConnectionId() != null ? c.hashCode() : "NULL") + "\n";
        str += linePrefix + "  data " + c.getData() + "\n";
        str += linePrefix + "  time " + c.getCreationTime() + "\n";

        return str;
    }

    private String debugPublisher(Publisher p, String linePrefix) {
        if (linePrefix == null)
            linePrefix = "";

        if (p == null) {
            return linePrefix + "Publisher NULL !!!!\n"; ////////// EARLY EXIT //////////
        }

        String str = linePrefix + p.getClass() + "\n";

        str += linePrefix + "  name     " + p.getName() + "\n";
        str += linePrefix + "  camera   " + p.getCameraId() + "\n";
        str += linePrefix + "  stream   " + p.getStreamId() + "\n";
        str += linePrefix + "  v-active " + p.getPublishVideo() + "\n";
        str += linePrefix + "  a-active " + p.getPublishAudio() + "\n";

        str += debugSession(p.getSession(), linePrefix + " \t");

        return str + linePrefix + "------- publisher.end ----------\n\n";
    }

    private String debugSubscriber(Subscriber s, String linePrefix) {
        if (linePrefix == null)
            linePrefix = "";

        if (s == null) {
            return linePrefix + "Subscriber NULL !!!!\n"; ////////// EARLY EXIT //////////
        }

        String str = linePrefix + s.getClass() + "\n";

        str += linePrefix + "  v-active " + s.getSubscribeToVideo() + "\n";
        str += linePrefix + "  a-active " + s.getSubscribeToAudio() + "\n";

        str += debugStream(s.getStream(), linePrefix + " \t");
        str += debugSession(s.getSession(), linePrefix + " \t");

        return str + linePrefix + "------- subscriber.end ----------\n\n";
    }
}