Back to project page cordova-plugin-opentok-android.
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.
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"; } }