Android Open Source - cordova-plugin-opentok-android Open Tok Plugin






From Project

Back to project page cordova-plugin-opentok-android.

License

The source code is released under:

MIT License

If you think the Android project cordova-plugin-opentok-android listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

package de.dfki.iui.opentok.cordova.plugin;
/*from w  w w  .  ja  v a2 s .c  o  m*/

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";
  }
}




Java Source Code List

de.dfki.iui.opentok.OpenTokExample.java
de.dfki.iui.opentok.cordova.plugin.OpenTokPlugin.java