[
  {
    "path": ".gitignore",
    "content": ".gradle\n/local.properties\n/.idea/workspace.xml\n/.idea/libraries\n.DS_Store\n/build\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 Kevin Gleason\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "README.md",
    "content": "# AndroidRTC\nAn example of native WebRTC on Android using PubNub's Android SDK signaling.\n\n### Big News: PubNub Android SDK for Signaling! \n\n![cover_img](http://kevingleason.me/AndroidRTC/assets/PnWebRTC.png)\n\nThis means that you can now create video chatting applications natively on Android in a breeze. Best of all, it is fully compatible with the [PubNub Javascript SDK][JS SDK]! That's right, you are minutes away from creating your very own cross platform video-chatting application.\n\n[__Get it now!__][PnWebRTC]\n\n_NOTE:_ The following demo uses the PubNub Android SDK for signaling to transfer the metadata and establish the peer-to-peer connection. Once the connection is established, the video and voice runs on public Google STUN/TURN servers.\n\nKeep in mind, PubNub can provide the signaling for WebRTC, and requires you to combine it with a hosted WebRTC solution. For more detail on what PubNub does, and what PubNub doesn’t do with WebRTC, check out this article: https://support.pubnub.com/support/solutions/articles/14000043715-does-pubnub-provide-webrtc-and-video-chat-\n\n## The AndroidRTC Example App\n\nThis app shows how to accomplish signling on a standby channel to coordinate users, then hop into a video chat and use the PubNub JavaScript SDK for signaling. You can even make a call to the AndroidRTC App [from the web interface](http://kevingleason.me/AndroidRTC).\n\n<img src=\"http://kevingleason.me/AndroidRTC/assets/Main.png\" height=500 />\n\nUsers in this app layout must be subscribed to `username-stdby`, a standby channel. The Android side checks if a user is online by checking presence on the standby channel.\n\nIn this app, a call can be placed by sending a JSON packet to the user's standby channel:\n\n    {\"call_user\":\"UserName\",\"call_time\":currentTimeMillis}\n\nUpon accepting the call, the answerer creates the SDP Offer, and video chat begins.\n\n<img src=\"http://kevingleason.me/AndroidRTC/assets/Kevin.png\" height=500 />\n\n### Incoming Calls\n\nAndroidRTC provides a good example of how to handle incoming calls.\n\n<img src=\"http://kevingleason.me/AndroidRTC/assets/Incoming.png\" height=500 />\n\n### User Messages\n\nThis app also shows how to send custom user messages. These messages could be chat, game scores, and much more.\n\n<img src=\"http://kevingleason.me/AndroidRTC/assets/Kurt.png\" height=500 />\n\n\n[PnWebRTC]:https://github.com/GleasonK/pubnub-android-webrtc\n[JavaDoc]:http://kevingleason.me/pubnub-android-webrtc/\n[AndroidRTC]:https://github.com/GleasonK/AndroidRTC/\n[JS SDK]:https://github.com/stephenlb/webrtc-sdk\n"
  },
  {
    "path": "app/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "app/build.gradle",
    "content": "apply plugin: 'com.android.application'\n\nandroid {\n    compileSdkVersion 21\n    buildToolsVersion \"20.0.0\"\n\n    defaultConfig {\n        applicationId \"me.kevingleason.androidrtc\"\n        minSdkVersion 17\n        targetSdkVersion 21\n        versionCode 1\n        versionName \"1.0\"\n    }\n    buildTypes {\n        release {\n            runProguard false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n}\n\nrepositories {\n    flatDir {\n        dirs 'libs'\n    }\n}\n\ndependencies {\n    compile fileTree(dir: 'libs', include: ['*.jar'])\n    compile 'com.pubnub:pubnub-android:3.7.4'\n    compile 'io.pristine:libjingle:9694@aar'\n    compile project(':pnwebrtc')\n}\n"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /Users/GleasonK/algs4/AndroidSDK/sdk/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n"
  },
  {
    "path": "app/src/androidTest/java/me/kevingleason/androidrtc/ApplicationTest.java",
    "content": "package me.kevingleason.androidrtc;\n\nimport android.app.Application;\nimport android.test.ApplicationTestCase;\n\n/**\n * <a href=\"http://d.android.com/tools/testing/testing_android.html\">Testing Fundamentals</a>\n */\npublic class ApplicationTest extends ApplicationTestCase<Application> {\n    public ApplicationTest() {\n        super(Application.class);\n    }\n}"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"me.kevingleason.androidrtc\" >\n\n    <!-- WebRTC Dependencies -->\n    <uses-feature android:name=\"android.hardware.camera\" />\n    <uses-feature android:name=\"android.hardware.camera.autofocus\" />\n    <uses-feature\n        android:glEsVersion=\"0x00020000\"\n        android:required=\"true\" />\n    <uses-permission android:name=\"android.permission.CAMERA\" />\n    <uses-permission android:name=\"android.permission.RECORD_AUDIO\" />\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\" />\n\n    <!-- PubNub Dependencies -->\n    <!--<uses-permission android:name=\"android.permission.INTERNET\" />-->\n    <uses-permission android:name=\"android.permission.WAKE_LOCK\" />\n    <uses-permission android:name=\"com.google.android.c2dm.permission.RECEIVE\" />\n    <permission android:name=\"your.package.name.permission.C2D_MESSAGE\" android:protectionLevel=\"signature\" />\n    <uses-permission android:name=\"your.package.name.permission.C2D_MESSAGE\" />\n\n    <application\n        android:allowBackup=\"true\"\n        android:icon=\"@drawable/ic_pubrtc\"\n        android:label=\"@string/app_name\"\n        android:theme=\"@style/AppTheme\" >\n        <activity\n            android:name=\"me.kevingleason.androidrtc.MainActivity\"\n            android:label=\"@string/app_name\" >\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n        <activity\n            android:name=\"me.kevingleason.androidrtc.VideoChatActivity\"\n            android:label=\"@string/title_activity_video_chat\"\n            android:parentActivityName=\"me.kevingleason.androidrtc.MainActivity\"\n            android:windowSoftInputMode=\"stateHidden\"\n            android:theme=\"@android:style/Theme.NoTitleBar.Fullscreen\" >\n            <meta-data\n                android:name=\"android.support.PARENT_ACTIVITY\"\n                android:value=\"me.kevingleason.androidrtc.MainActivity\" />\n        </activity>\n        <activity\n            android:name=\"me.kevingleason.androidrtc.IncomingCallActivity\"\n            android:label=\"@string/title_activity_incoming_call\">\n        </activity>\n        <activity\n            android:name=\"me.kevingleason.androidrtc.LoginActivity\"\n            android:label=\"@string/title_activity_login\" >\n        </activity>\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "app/src/main/java/me/kevingleason/androidrtc/IncomingCallActivity.java",
    "content": "package me.kevingleason.androidrtc;\n\nimport android.app.Activity;\nimport android.content.Intent;\nimport android.content.SharedPreferences;\nimport android.os.Bundle;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.view.View;\nimport android.widget.TextView;\nimport android.widget.Toast;\n\nimport com.pubnub.api.Callback;\nimport com.pubnub.api.Pubnub;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport me.kevingleason.androidrtc.util.Constants;\nimport me.kevingleason.pnwebrtc.PnPeerConnectionClient;\n\n\npublic class IncomingCallActivity extends Activity {\n    private SharedPreferences mSharedPreferences;\n    private String username;\n    private String callUser;\n\n    private Pubnub mPubNub;\n    private TextView mCallerID;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_incoming_call);\n\n        this.mSharedPreferences = getSharedPreferences(Constants.SHARED_PREFS, MODE_PRIVATE);\n        if (!this.mSharedPreferences.contains(Constants.USER_NAME)){\n            Intent intent = new Intent(this, LoginActivity.class);\n            startActivity(intent);\n            finish();\n            return;\n        }\n        this.username = this.mSharedPreferences.getString(Constants.USER_NAME, \"\");\n\n        Bundle extras = getIntent().getExtras();\n        if (extras==null || !extras.containsKey(Constants.CALL_USER)){\n            Intent intent = new Intent(this, MainActivity.class);\n            startActivity(intent);\n            Toast.makeText(this, \"Need to pass username to IncomingCallActivity in intent extras (Constants.CALL_USER).\",\n                    Toast.LENGTH_SHORT).show();\n            finish();\n            return;\n        }\n        this.callUser = extras.getString(Constants.CALL_USER, \"\");\n        this.mCallerID = (TextView) findViewById(R.id.caller_id);\n        this.mCallerID.setText(this.callUser);\n\n        this.mPubNub  = new Pubnub(Constants.PUB_KEY, Constants.SUB_KEY);\n        this.mPubNub.setUUID(this.username);\n    }\n\n\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        // Inflate the menu; this adds items to the action bar if it is present.\n        getMenuInflater().inflate(R.menu.menu_incoming_call, menu);\n        return true;\n    }\n\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        // Handle action bar item clicks here. The action bar will\n        // automatically handle clicks on the Home/Up button, so long\n        // as you specify a parent activity in AndroidManifest.xml.\n        int id = item.getItemId();\n\n        //noinspection SimplifiableIfStatement\n        if (id == R.id.action_settings) {\n            return true;\n        }\n\n        return super.onOptionsItemSelected(item);\n    }\n\n    public void acceptCall(View view){\n        Intent intent = new Intent(IncomingCallActivity.this, VideoChatActivity.class);\n        intent.putExtra(Constants.USER_NAME, this.username);\n        intent.putExtra(Constants.CALL_USER, this.callUser);\n        startActivity(intent);\n    }\n\n    /**\n     * Publish a hangup command if rejecting call.\n     * @param view\n     */\n    public void rejectCall(View view){\n        JSONObject hangupMsg = PnPeerConnectionClient.generateHangupPacket(this.username);\n        this.mPubNub.publish(this.callUser,hangupMsg, new Callback() {\n            @Override\n            public void successCallback(String channel, Object message) {\n                Intent intent = new Intent(IncomingCallActivity.this, MainActivity.class);\n                startActivity(intent);\n            }\n        });\n    }\n\n    @Override\n    protected void onStop() {\n        super.onStop();\n        if(this.mPubNub!=null){\n            this.mPubNub.unsubscribeAll();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/kevingleason/androidrtc/LoginActivity.java",
    "content": "package me.kevingleason.androidrtc;\n\nimport android.app.Activity;\nimport android.content.Intent;\nimport android.content.SharedPreferences;\nimport android.os.Bundle;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.view.View;\nimport android.widget.EditText;\n\nimport me.kevingleason.androidrtc.util.Constants;\n\n/**\n * Login Activity for the first time the app is opened, or when a user clicks the sign out button.\n * Saves the username in SharedPreferences.\n */\npublic class LoginActivity extends Activity {\n\n    private EditText mUsername;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_login);\n\n        mUsername = (EditText) findViewById(R.id.login_username);\n\n        Bundle extras = getIntent().getExtras();\n        if (extras != null){\n            String lastUsername = extras.getString(\"oldUsername\", \"\");\n            mUsername.setText(lastUsername);\n        }\n    }\n\n\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        // Inflate the menu; this adds items to the action bar if it is present.\n        getMenuInflater().inflate(R.menu.menu_login, menu);\n        return true;\n    }\n\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        // Handle action bar item clicks here. The action bar will\n        // automatically handle clicks on the Home/Up button, so long\n        // as you specify a parent activity in AndroidManifest.xml.\n        int id = item.getItemId();\n\n        //noinspection SimplifiableIfStatement\n        if (id == R.id.action_settings) {\n            return true;\n        }\n\n        return super.onOptionsItemSelected(item);\n    }\n\n    /**\n     * Takes the username from the EditText, check its validity and saves it if valid.\n     *   Then, redirects to the MainActivity.\n     * @param view Button clicked to trigger call to joinChat\n     */\n    public void joinChat(View view){\n        String username = mUsername.getText().toString();\n        if (!validUsername(username))\n            return;\n\n        SharedPreferences sp = getSharedPreferences(Constants.SHARED_PREFS,MODE_PRIVATE);\n        SharedPreferences.Editor edit = sp.edit();\n        edit.putString(Constants.USER_NAME, username);\n        edit.apply();\n\n        Intent intent = new Intent(this, MainActivity.class);\n        startActivity(intent);\n    }\n\n    /**\n     * Optional function to specify what a username in your chat app can look like.\n     * @param username The name entered by a user.\n     * @return is username valid\n     */\n    private boolean validUsername(String username) {\n        if (username.length() == 0) {\n            mUsername.setError(\"Username cannot be empty.\");\n            return false;\n        }\n        if (username.length() > 16) {\n            mUsername.setError(\"Username too long.\");\n            return false;\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/kevingleason/androidrtc/MainActivity.java",
    "content": "package me.kevingleason.androidrtc;\n\nimport android.app.ListActivity;\nimport android.content.Intent;\nimport android.content.SharedPreferences;\nimport android.os.Bundle;\nimport android.util.Log;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.view.View;\nimport android.widget.EditText;\nimport android.widget.ListView;\nimport android.widget.TextView;\nimport android.widget.Toast;\n\nimport com.pubnub.api.Callback;\nimport com.pubnub.api.Pubnub;\nimport com.pubnub.api.PubnubError;\nimport com.pubnub.api.PubnubException;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.util.ArrayList;\n\nimport me.kevingleason.androidrtc.adapters.HistoryAdapter;\nimport me.kevingleason.androidrtc.adt.HistoryItem;\nimport me.kevingleason.androidrtc.util.Constants;\n\n\npublic class MainActivity extends ListActivity {\n    private SharedPreferences mSharedPreferences;\n    private String username;\n    private String stdByChannel;\n    private Pubnub mPubNub;\n\n    private ListView mHistoryList;\n    private HistoryAdapter mHistoryAdapter;\n    private EditText mCallNumET;\n    private TextView mUsernameTV;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_main);\n\n        this.mSharedPreferences = getSharedPreferences(Constants.SHARED_PREFS, MODE_PRIVATE);\n        if (!this.mSharedPreferences.contains(Constants.USER_NAME)){\n            Intent intent = new Intent(this, LoginActivity.class);\n            startActivity(intent);\n            finish();\n            return;\n        }\n        this.username     = this.mSharedPreferences.getString(Constants.USER_NAME, \"\");\n        this.stdByChannel = this.username + Constants.STDBY_SUFFIX;\n\n        this.mHistoryList = getListView();\n        this.mCallNumET   = (EditText) findViewById(R.id.call_num);\n        this.mUsernameTV  = (TextView) findViewById(R.id.main_username);\n\n        this.mUsernameTV.setText(this.username);\n        initPubNub();\n\n        this.mHistoryAdapter = new HistoryAdapter(this, new ArrayList<HistoryItem>(), this.mPubNub);\n        this.mHistoryList.setAdapter(this.mHistoryAdapter);\n    }\n\n\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        // Inflate the menu; this adds items to the action bar if it is present.\n        getMenuInflater().inflate(R.menu.menu_main, menu);\n        return true;\n    }\n\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        // Handle action bar item clicks here. The action bar will\n        // automatically handle clicks on the Home/Up button, so long\n        // as you specify a parent activity in AndroidManifest.xml.\n        int id = item.getItemId();\n\n        //noinspection SimplifiableIfStatement\n        switch(id){\n            case R.id.action_settings:\n                return true;\n            case R.id.action_sign_out:\n                signOut();\n                return true;\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    @Override\n    protected void onStop() {\n        super.onStop();\n        if(this.mPubNub!=null){\n            this.mPubNub.unsubscribeAll();\n        }\n    }\n\n    @Override\n    protected void onRestart() {\n        super.onRestart();\n        if(this.mPubNub==null){\n            initPubNub();\n        } else {\n            subscribeStdBy();\n        }\n    }\n\n    /**\n     * Subscribe to standby channel so that it doesn't interfere with the WebRTC Signaling.\n     */\n    public void initPubNub(){\n        this.mPubNub  = new Pubnub(Constants.PUB_KEY, Constants.SUB_KEY);\n        this.mPubNub.setUUID(this.username);\n        subscribeStdBy();\n    }\n\n    /**\n     * Subscribe to standby channel\n     */\n    private void subscribeStdBy(){\n        try {\n            this.mPubNub.subscribe(this.stdByChannel, new Callback() {\n                @Override\n                public void successCallback(String channel, Object message) {\n                    Log.d(\"MA-iPN\", \"MESSAGE: \" + message.toString());\n                    if (!(message instanceof JSONObject)) return; // Ignore if not JSONObject\n                    JSONObject jsonMsg = (JSONObject) message;\n                    try {\n                        if (!jsonMsg.has(Constants.JSON_CALL_USER)) return;     //Ignore Signaling messages.\n                        String user = jsonMsg.getString(Constants.JSON_CALL_USER);\n                        dispatchIncomingCall(user);\n                    } catch (JSONException e){\n                        e.printStackTrace();\n                    }\n                }\n\n                @Override\n                public void connectCallback(String channel, Object message) {\n                    Log.d(\"MA-iPN\", \"CONNECTED: \" + message.toString());\n                    setUserStatus(Constants.STATUS_AVAILABLE);\n                }\n\n                @Override\n                public void errorCallback(String channel, PubnubError error) {\n                    Log.d(\"MA-iPN\",\"ERROR: \" + error.toString());\n                }\n            });\n        } catch (PubnubException e){\n            Log.d(\"HERE\",\"HEREEEE\");\n            e.printStackTrace();\n        }\n    }\n\n    /**\n     * Take the user to a video screen. USER_NAME is a required field.\n     * @param view button that is clicked to trigger toVideo\n     */\n    public void makeCall(View view){\n        String callNum = mCallNumET.getText().toString();\n        if (callNum.isEmpty() || callNum.equals(this.username)){\n            showToast(\"Enter a valid user ID to call.\");\n            return;\n        }\n        dispatchCall(callNum);\n    }\n\n    /**TODO: Debate who calls who. Should one be on standby? Or use State API for busy/available\n     * Check that user is online. If they are, dispatch the call by publishing to their standby\n     *   channel. If the publish was successful, then change activities over to the video chat.\n     * The called user will then have the option to accept of decline the call. If they accept,\n     *   they will be brought to the video chat activity as well, to connect video/audio. If\n     *   they decline, a hangup will be issued, and the VideoChat adapter's onHangup callback will\n     *   be invoked.\n     * @param callNum Number to publish a call to.\n     */\n    public void dispatchCall(final String callNum){\n        final String callNumStdBy = callNum + Constants.STDBY_SUFFIX;\n        this.mPubNub.hereNow(callNumStdBy, new Callback() {\n            @Override\n            public void successCallback(String channel, Object message) {\n                Log.d(\"MA-dC\", \"HERE_NOW: \" +\" CH - \" + callNumStdBy + \" \" + message.toString());\n                try {\n                    int occupancy = ((JSONObject) message).getInt(Constants.JSON_OCCUPANCY);\n                    if (occupancy == 0) {\n                        showToast(\"User is not online!\");\n                        return;\n                    }\n                    JSONObject jsonCall = new JSONObject();\n                    jsonCall.put(Constants.JSON_CALL_USER, username);\n                    jsonCall.put(Constants.JSON_CALL_TIME, System.currentTimeMillis());\n                    mPubNub.publish(callNumStdBy, jsonCall, new Callback() {\n                        @Override\n                        public void successCallback(String channel, Object message) {\n                            Log.d(\"MA-dC\", \"SUCCESS: \" + message.toString());\n                            Intent intent = new Intent(MainActivity.this, VideoChatActivity.class);\n                            intent.putExtra(Constants.USER_NAME, username);\n                            intent.putExtra(Constants.CALL_USER, callNum);  // Only accept from this number?\n                            startActivity(intent);\n                        }\n                    });\n                } catch (JSONException e) {\n                    e.printStackTrace();\n                }\n            }\n        });\n    }\n\n    /**\n     * Handle incoming calls. TODO: Implement an accept/reject functionality.\n     * @param userId\n     */\n    private void dispatchIncomingCall(String userId){\n        showToast(\"Call from: \" + userId);\n        Intent intent = new Intent(MainActivity.this, IncomingCallActivity.class);\n        intent.putExtra(Constants.USER_NAME, username);\n        intent.putExtra(Constants.CALL_USER, userId);\n        startActivity(intent);\n    }\n\n    private void setUserStatus(String status){\n        try {\n            JSONObject state = new JSONObject();\n            state.put(Constants.JSON_STATUS, status);\n            this.mPubNub.setState(this.stdByChannel, this.username, state, new Callback() {\n                @Override\n                public void successCallback(String channel, Object message) {\n                    Log.d(\"MA-sUS\",\"State Set: \" + message.toString());\n                }\n            });\n        } catch (JSONException e){\n            e.printStackTrace();\n        }\n    }\n\n    private void getUserStatus(String userId){\n        String stdByUser = userId + Constants.STDBY_SUFFIX;\n        this.mPubNub.getState(stdByUser, userId, new Callback() {\n            @Override\n            public void successCallback(String channel, Object message) {\n                Log.d(\"MA-gUS\", \"User Status: \" + message.toString());\n            }\n        });\n    }\n\n    /**\n     * Ensures that toast is run on the UI thread.\n     * @param message\n     */\n    private void showToast(final String message){\n        runOnUiThread(new Runnable() {\n            @Override\n            public void run() {\n                Toast.makeText(MainActivity.this, message, Toast.LENGTH_SHORT).show();\n            }\n        });\n    }\n\n    /**\n     * Log out, remove username from SharedPreferences, unsubscribe from PubNub, and send user back\n     *   to the LoginActivity\n     */\n    public void signOut(){\n        this.mPubNub.unsubscribeAll();\n        SharedPreferences.Editor edit = this.mSharedPreferences.edit();\n        edit.remove(Constants.USER_NAME);\n        edit.apply();\n        Intent intent = new Intent(this, LoginActivity.class);\n        intent.putExtra(\"oldUsername\", this.username);\n        startActivity(intent);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/kevingleason/androidrtc/VideoChatActivity.java",
    "content": "package me.kevingleason.androidrtc;\n\nimport android.app.Activity;\nimport android.app.ListActivity;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.opengl.GLSurfaceView;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.util.Log;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.view.View;\nimport android.view.WindowManager;\nimport android.view.inputmethod.InputMethodManager;\nimport android.widget.EditText;\nimport android.widget.ListView;\nimport android.widget.TextView;\nimport android.widget.Toast;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\nimport org.webrtc.AudioSource;\nimport org.webrtc.AudioTrack;\nimport org.webrtc.MediaConstraints;\nimport org.webrtc.MediaStream;\nimport org.webrtc.PeerConnection;\nimport org.webrtc.PeerConnectionFactory;\nimport org.webrtc.VideoCapturer;\nimport org.webrtc.VideoCapturerAndroid;\nimport org.webrtc.VideoRenderer;\nimport org.webrtc.VideoRendererGui;\nimport org.webrtc.VideoSource;\nimport org.webrtc.VideoTrack;\n\nimport java.util.ArrayList;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.concurrent.ExecutionException;\n\nimport me.kevingleason.androidrtc.adapters.ChatAdapter;\nimport me.kevingleason.androidrtc.adt.ChatMessage;\nimport me.kevingleason.androidrtc.servers.XirSysRequest;\nimport me.kevingleason.androidrtc.util.Constants;\nimport me.kevingleason.androidrtc.util.LogRTCListener;\nimport me.kevingleason.pnwebrtc.PnPeer;\nimport me.kevingleason.pnwebrtc.PnRTCClient;\nimport me.kevingleason.pnwebrtc.PnSignalingParams;\n\n/**\n * This chat will begin/subscribe to a video chat.\n * REQUIRED: The intent must contain a\n */\npublic class VideoChatActivity extends ListActivity {\n    public static final String VIDEO_TRACK_ID = \"videoPN\";\n    public static final String AUDIO_TRACK_ID = \"audioPN\";\n    public static final String LOCAL_MEDIA_STREAM_ID = \"localStreamPN\";\n\n    private PnRTCClient pnRTCClient;\n    private VideoSource localVideoSource;\n    private VideoRenderer.Callbacks localRender;\n    private VideoRenderer.Callbacks remoteRender;\n    private GLSurfaceView videoView;\n    private EditText mChatEditText;\n    private ListView mChatList;\n    private ChatAdapter mChatAdapter;\n    private TextView mCallStatus;\n\n    private String username;\n    private boolean backPressed = false;\n    private Thread  backPressedThread = null;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_video_chat);\n        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);\n\n        Bundle extras = getIntent().getExtras();\n        if (extras == null || !extras.containsKey(Constants.USER_NAME)) {\n            Intent intent = new Intent(this, MainActivity.class);\n            startActivity(intent);\n            Toast.makeText(this, \"Need to pass username to VideoChatActivity in intent extras (Constants.USER_NAME).\",\n                    Toast.LENGTH_SHORT).show();\n            finish();\n            return;\n        }\n        this.username      = extras.getString(Constants.USER_NAME, \"\");\n        this.mChatList     = getListView();\n        this.mChatEditText = (EditText) findViewById(R.id.chat_input);\n        this.mCallStatus   = (TextView) findViewById(R.id.call_status);\n\n        // Set up the List View for chatting\n        List<ChatMessage> ll = new LinkedList<ChatMessage>();\n        mChatAdapter = new ChatAdapter(this, ll);\n        mChatList.setAdapter(mChatAdapter);\n\n        // First, we initiate the PeerConnectionFactory with our application context and some options.\n        PeerConnectionFactory.initializeAndroidGlobals(\n                this,  // Context\n                true,  // Audio Enabled\n                true,  // Video Enabled\n                true,  // Hardware Acceleration Enabled\n                null); // Render EGL Context\n\n        PeerConnectionFactory pcFactory = new PeerConnectionFactory();\n        this.pnRTCClient = new PnRTCClient(Constants.PUB_KEY, Constants.SUB_KEY, this.username);\n        List<PeerConnection.IceServer> servers = getXirSysIceServers();\n        if (!servers.isEmpty()){\n            this.pnRTCClient.setSignalParams(new PnSignalingParams());\n        }\n\n        // Returns the number of cams & front/back face device name\n        int camNumber = VideoCapturerAndroid.getDeviceCount();\n        String frontFacingCam = VideoCapturerAndroid.getNameOfFrontFacingDevice();\n        String backFacingCam = VideoCapturerAndroid.getNameOfBackFacingDevice();\n\n        // Creates a VideoCapturerAndroid instance for the device name\n        VideoCapturer capturer = VideoCapturerAndroid.create(frontFacingCam);\n\n        // First create a Video Source, then we can make a Video Track\n        localVideoSource = pcFactory.createVideoSource(capturer, this.pnRTCClient.videoConstraints());\n        VideoTrack localVideoTrack = pcFactory.createVideoTrack(VIDEO_TRACK_ID, localVideoSource);\n\n        // First we create an AudioSource then we can create our AudioTrack\n        AudioSource audioSource = pcFactory.createAudioSource(this.pnRTCClient.audioConstraints());\n        AudioTrack localAudioTrack = pcFactory.createAudioTrack(AUDIO_TRACK_ID, audioSource);\n\n        // To create our VideoRenderer, we can use the included VideoRendererGui for simplicity\n        // First we need to set the GLSurfaceView that it should render to\n        this.videoView = (GLSurfaceView) findViewById(R.id.gl_surface);\n\n        // Then we set that view, and pass a Runnable to run once the surface is ready\n        VideoRendererGui.setView(videoView, null);\n\n        // Now that VideoRendererGui is ready, we can get our VideoRenderer.\n        // IN THIS ORDER. Effects which is on top or bottom\n        remoteRender = VideoRendererGui.create(0, 0, 100, 100, VideoRendererGui.ScalingType.SCALE_ASPECT_FILL, false);\n        localRender = VideoRendererGui.create(0, 0, 100, 100, VideoRendererGui.ScalingType.SCALE_ASPECT_FILL, true);\n\n        // We start out with an empty MediaStream object, created with help from our PeerConnectionFactory\n        //  Note that LOCAL_MEDIA_STREAM_ID can be any string\n        MediaStream mediaStream = pcFactory.createLocalMediaStream(LOCAL_MEDIA_STREAM_ID);\n\n        // Now we can add our tracks.\n        mediaStream.addTrack(localVideoTrack);\n        mediaStream.addTrack(localAudioTrack);\n\n        // First attach the RTC Listener so that callback events will be triggered\n        this.pnRTCClient.attachRTCListener(new DemoRTCListener());\n\n        // Then attach your local media stream to the PnRTCClient.\n        //  This will trigger the onLocalStream callback.\n        this.pnRTCClient.attachLocalMediaStream(mediaStream);\n\n        // Listen on a channel. This is your \"phone number,\" also set the max chat users.\n        this.pnRTCClient.listenOn(\"Kevin\");\n        this.pnRTCClient.setMaxConnections(1);\n\n        // If the intent contains a number to dial, call it now that you are connected.\n        //  Else, remain listening for a call.\n        if (extras.containsKey(Constants.CALL_USER)) {\n            String callUser = extras.getString(Constants.CALL_USER, \"\");\n            connectToUser(callUser);\n        }\n    }\n\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        // Inflate the menu; this adds items to the action bar if it is present.\n        getMenuInflater().inflate(R.menu.menu_video_chat, menu);\n        return true;\n    }\n\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        // Handle action bar item clicks here. The action bar will\n        // automatically handle clicks on the Home/Up button, so long\n        // as you specify a parent activity in AndroidManifest.xml.\n        int id = item.getItemId();\n\n        //noinspection SimplifiableIfStatement\n        if (id == R.id.action_settings) {\n            return true;\n        }\n\n        return super.onOptionsItemSelected(item);\n    }\n\n    @Override\n    protected void onPause() {\n        super.onPause();\n        this.videoView.onPause();\n        this.localVideoSource.stop();\n    }\n\n    @Override\n    protected void onResume() {\n        super.onResume();\n        this.videoView.onResume();\n        this.localVideoSource.restart();\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n        if (this.localVideoSource != null) {\n            this.localVideoSource.stop();\n        }\n        if (this.pnRTCClient != null) {\n            this.pnRTCClient.onDestroy();\n        }\n    }\n\n    @Override\n    public void onBackPressed() {\n        if (!this.backPressed){\n            this.backPressed = true;\n            Toast.makeText(this,\"Press back again to end.\",Toast.LENGTH_SHORT).show();\n            this.backPressedThread = new Thread(new Runnable() {\n                @Override\n                public void run() {\n                    try {\n                        Thread.sleep(5000);\n                        backPressed = false;\n                    } catch (InterruptedException e){ Log.d(\"VCA-oBP\",\"Successfully interrupted\"); }\n                }\n            });\n            this.backPressedThread.start();\n            return;\n        }\n        if (this.backPressedThread != null)\n            this.backPressedThread.interrupt();\n        super.onBackPressed();\n    }\n\n    public List<PeerConnection.IceServer> getXirSysIceServers(){\n        List<PeerConnection.IceServer> servers = new ArrayList<PeerConnection.IceServer>();\n        try {\n            servers = new XirSysRequest().execute().get();\n        } catch (InterruptedException e){\n            e.printStackTrace();\n        }catch (ExecutionException e){\n            e.printStackTrace();\n        }\n        return servers;\n    }\n\n    public void connectToUser(String user) {\n        this.pnRTCClient.connect(user);\n    }\n\n    public void hangup(View view) {\n        this.pnRTCClient.closeAllConnections();\n        endCall();\n    }\n\n    private void endCall() {\n        startActivity(new Intent(VideoChatActivity.this, MainActivity.class));\n        finish();\n    }\n\n\n    public void sendMessage(View view) {\n        String message = mChatEditText.getText().toString();\n        if (message.equals(\"\")) return; // Return if empty\n        ChatMessage chatMsg = new ChatMessage(this.username, message, System.currentTimeMillis());\n        mChatAdapter.addMessage(chatMsg);\n        JSONObject messageJSON = new JSONObject();\n        try {\n            messageJSON.put(Constants.JSON_MSG_UUID, chatMsg.getSender());\n            messageJSON.put(Constants.JSON_MSG, chatMsg.getMessage());\n            messageJSON.put(Constants.JSON_TIME, chatMsg.getTimeStamp());\n            this.pnRTCClient.transmitAll(messageJSON);\n        } catch (JSONException e) {\n            e.printStackTrace();\n        }\n        // Hide keyboard when you send a message.\n        View focusView = this.getCurrentFocus();\n        if (focusView != null) {\n            InputMethodManager inputManager = (InputMethodManager) this.getSystemService(Context.INPUT_METHOD_SERVICE);\n            inputManager.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);\n        }\n        mChatEditText.setText(\"\");\n    }\n\n    /**\n     * LogRTCListener is used for debugging purposes, it prints all RTC messages.\n     * DemoRTC is just a Log Listener with the added functionality to append screens.\n     */\n    private class DemoRTCListener extends LogRTCListener {\n        @Override\n        public void onLocalStream(final MediaStream localStream) {\n            super.onLocalStream(localStream); // Will log values\n            VideoChatActivity.this.runOnUiThread(new Runnable() {\n                @Override\n                public void run() {\n                    if(localStream.videoTracks.size()==0) return;\n                    localStream.videoTracks.get(0).addRenderer(new VideoRenderer(localRender));\n                }\n            });\n        }\n\n        @Override\n        public void onAddRemoteStream(final MediaStream remoteStream, final PnPeer peer) {\n            super.onAddRemoteStream(remoteStream, peer); // Will log values\n            VideoChatActivity.this.runOnUiThread(new Runnable() {\n                @Override\n                public void run() {\n                    Toast.makeText(VideoChatActivity.this,\"Connected to \" + peer.getId(), Toast.LENGTH_SHORT).show();\n                    try {\n                        if(remoteStream.audioTracks.size()==0 || remoteStream.videoTracks.size()==0) return;\n                        mCallStatus.setVisibility(View.GONE);\n                        remoteStream.videoTracks.get(0).addRenderer(new VideoRenderer(remoteRender));\n                        VideoRendererGui.update(remoteRender, 0, 0, 100, 100, VideoRendererGui.ScalingType.SCALE_ASPECT_FILL, false);\n                        VideoRendererGui.update(localRender, 72, 65, 25, 25, VideoRendererGui.ScalingType.SCALE_ASPECT_FIT, true);\n                    }\n                    catch (Exception e){ e.printStackTrace(); }\n                }\n            });\n        }\n\n        @Override\n        public void onMessage(PnPeer peer, Object message) {\n            super.onMessage(peer, message);  // Will log values\n            if (!(message instanceof JSONObject)) return; //Ignore if not JSONObject\n            JSONObject jsonMsg = (JSONObject) message;\n            try {\n                String uuid = jsonMsg.getString(Constants.JSON_MSG_UUID);\n                String msg  = jsonMsg.getString(Constants.JSON_MSG);\n                long   time = jsonMsg.getLong(Constants.JSON_TIME);\n                final ChatMessage chatMsg = new ChatMessage(uuid, msg, time);\n                VideoChatActivity.this.runOnUiThread(new Runnable() {\n                    @Override\n                    public void run() {\n                        mChatAdapter.addMessage(chatMsg);\n                    }\n                });\n            } catch (JSONException e){\n                e.printStackTrace();\n            }\n        }\n\n        @Override\n        public void onPeerConnectionClosed(PnPeer peer) {\n            super.onPeerConnectionClosed(peer);\n            VideoChatActivity.this.runOnUiThread(new Runnable() {\n                @Override\n                public void run() {\n                    mCallStatus.setText(\"Call Ended...\");\n                    mCallStatus.setVisibility(View.VISIBLE);\n                }\n            });\n            try {Thread.sleep(1500);} catch (InterruptedException e){e.printStackTrace();}\n            Intent intent = new Intent(VideoChatActivity.this, MainActivity.class);\n            startActivity(intent);\n            finish();\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/me/kevingleason/androidrtc/adapters/ChatAdapter.java",
    "content": "package me.kevingleason.androidrtc.adapters;\n\nimport android.content.Context;\nimport android.util.Log;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.animation.AccelerateInterpolator;\nimport android.view.animation.AlphaAnimation;\nimport android.view.animation.Animation;\nimport android.view.animation.AnimationSet;\nimport android.widget.ArrayAdapter;\nimport android.widget.TextView;\n\nimport java.text.SimpleDateFormat;\nimport java.util.Calendar;\nimport java.util.List;\n\nimport me.kevingleason.androidrtc.R;\nimport me.kevingleason.androidrtc.adt.ChatMessage;\n\n\n/**\n * Created by GleasonK on 6/25/15.\n */\npublic class ChatAdapter extends ArrayAdapter<ChatMessage> {\n    private static final long FADE_TIMEOUT = 3000;\n\n    private final Context context;\n    private LayoutInflater inflater;\n    private List<ChatMessage> values;\n\n    public ChatAdapter(Context context, List<ChatMessage> values) {\n        super(context, R.layout.chat_message_row_layout, android.R.id.text1, values);\n        this.context = context;\n        this.inflater = LayoutInflater.from(context);\n        this.values=values;\n    }\n\n    class ViewHolder {\n        TextView sender;\n        TextView message;\n        TextView timeStamp;\n        ChatMessage chatMsg;\n    }\n\n    @Override\n    public View getView(final int position, View convertView, ViewGroup parent) {\n        ChatMessage chatMsg;\n        if(position >= values.size()){ chatMsg = new ChatMessage(\"\",\"\",0); } // Catch Edge Case\n        else { chatMsg = this.values.get(position); }\n        ViewHolder holder;\n        if (convertView == null) {\n            holder = new ViewHolder();\n            convertView = inflater.inflate(R.layout.chat_message_row_layout, parent, false);\n            holder.sender = (TextView) convertView.findViewById(R.id.chat_user);\n            holder.message = (TextView) convertView.findViewById(R.id.chat_message);\n            holder.timeStamp = (TextView) convertView.findViewById(R.id.chat_timestamp);\n            convertView.setTag(holder);\n            Log.d(\"Adapter\", \"Recreating fadeout.\");\n        } else {\n            holder = (ViewHolder) convertView.getTag();\n        }\n        holder.sender.setText(chatMsg.getSender() + \": \");\n        holder.message.setText(chatMsg.getMessage());\n        holder.timeStamp.setText(formatTimeStamp(chatMsg.getTimeStamp()));\n        holder.chatMsg=chatMsg;\n        setFadeOut3(convertView, chatMsg);\n        return convertView;\n    }\n\n    @Override\n    public int getCount() {\n        return this.values.size();\n    }\n\n    @Override\n    public boolean hasStableIds() {\n        return true;\n    }\n\n    @Override\n    public long getItemId(int position){\n        if (position >= values.size()){ return -1; }\n        return values.get(position).hashCode();\n    }\n\n    public void removeMsg(int loc){\n        this.values.remove(loc);\n        notifyDataSetChanged();\n    }\n\n    public void addMessage(ChatMessage chatMsg){\n        this.values.add(chatMsg);\n        notifyDataSetChanged();\n    }\n\n    private void setFadeOut2(final View view, final ChatMessage message){\n        Log.i(\"AdapterFade\", \"Caling Fade2\");\n        view.animate().setDuration(1000).setStartDelay(2000).alpha(0)\n        .withEndAction(new Runnable() {\n            @Override\n            public void run() {\n                if (values.contains(message))\n                    values.remove(message);\n                notifyDataSetChanged();\n                view.setAlpha(1);\n            }\n        });\n    }\n\n    private void setFadeOut3(final View view, final ChatMessage message){\n        Log.i(\"AdapterFade\", \"Caling Fade3\");\n        long elapsed = System.currentTimeMillis() - message.getTimeStamp();\n        if (elapsed >= FADE_TIMEOUT){\n            if (values.contains(message))\n                values.remove(message);\n            notifyDataSetChanged();\n            return;\n        }\n        view.animate().setStartDelay(FADE_TIMEOUT - elapsed).setDuration(1500).alpha(0)\n                .withEndAction(new Runnable() {\n                    @Override\n                    public void run() {\n                        if (values.contains(message)){\n                            values.remove(message);\n                        }\n                        notifyDataSetChanged();\n                        view.setAlpha(1);\n                    }\n                });\n    }\n\n\n    private void setFadeOut(final View view, final ChatMessage message){\n        long elapsed = System.currentTimeMillis() - message.getTimeStamp();\n        if (elapsed >= FADE_TIMEOUT){\n            if (values.contains(message))\n                values.remove(message);\n            notifyDataSetChanged();\n            return;\n        }\n\n        view.setHasTransientState(true);\n        Animation fadeOut = new AlphaAnimation(1, 0);\n        fadeOut.setInterpolator(new AccelerateInterpolator()); //and this\n        fadeOut.setStartOffset(FADE_TIMEOUT - elapsed);\n        fadeOut.setDuration(1000);\n\n        AnimationSet animation = new AnimationSet(false);\n        animation.addAnimation(fadeOut);\n        animation.setRepeatCount(1);\n\n        view.setAnimation(animation);\n        animation.setAnimationListener(new Animation.AnimationListener() {\n            @Override\n            public void onAnimationStart(Animation animation) {}\n\n            @Override\n            public void onAnimationEnd(Animation animation) {\n                if (values.contains(message)){\n                    values.remove(message);\n                }\n                notifyDataSetChanged();\n                view.setAlpha(1);\n                view.setHasTransientState(false);\n            }\n\n            @Override\n            public void onAnimationRepeat(Animation animation) {}\n        });\n\n\n    }\n\n    /**\n     * Format the long System.currentTimeMillis() to a better looking timestamp. Uses a calendar\n     *   object to format with the user's current time zone.\n     * @param timeStamp\n     * @return\n     */\n    public static String formatTimeStamp(long timeStamp){\n        // Create a DateFormatter object for displaying date in specified format.\n        SimpleDateFormat formatter = new SimpleDateFormat(\"h:mm.ss a\");\n\n        // Create a calendar object that will convert the date and time value in milliseconds to date.\n        Calendar calendar = Calendar.getInstance();\n        calendar.setTimeInMillis(timeStamp);\n        return formatter.format(calendar.getTime());\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/me/kevingleason/androidrtc/adapters/HistoryAdapter.java",
    "content": "package me.kevingleason.androidrtc.adapters;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.util.Log;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ArrayAdapter;\nimport android.widget.ImageButton;\nimport android.widget.TextView;\n\nimport com.pubnub.api.Callback;\nimport com.pubnub.api.Pubnub;\n\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.text.SimpleDateFormat;\nimport java.util.ArrayList;\nimport java.util.Calendar;\nimport java.util.HashMap;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\n\nimport me.kevingleason.androidrtc.MainActivity;\nimport me.kevingleason.androidrtc.R;\nimport me.kevingleason.androidrtc.adt.ChatUser;\nimport me.kevingleason.androidrtc.adt.HistoryItem;\nimport me.kevingleason.androidrtc.util.Constants;\n\n/**\n * Created by GleasonK on 7/31/15.\n */\npublic class HistoryAdapter extends ArrayAdapter<HistoryItem> {\n    private final Context context;\n    private Pubnub mPubNub;\n    private LayoutInflater inflater;\n    private List<HistoryItem> values;\n    private Map<String, ChatUser> users;\n\n\n    public HistoryAdapter(Context context, List<HistoryItem> values, Pubnub pubnub) {\n        super(context, R.layout.history_row_layout, android.R.id.text1, values);\n        this.context  = context;\n        this.inflater = LayoutInflater.from(context);\n        this.mPubNub  = pubnub;\n        this.values   = values;\n        this.users    = new HashMap<String, ChatUser>();\n        updateHistory();\n    }\n\n    class ViewHolder {\n        TextView    user;\n        TextView    status;\n        TextView    time;\n        ImageButton callBtn;\n        HistoryItem histItem;\n    }\n\n    @Override\n    public View getView(final int position, View convertView, ViewGroup parent) {\n        final HistoryItem hItem = this.values.get(position);\n        ViewHolder holder;\n        if (convertView == null) {\n            holder = new ViewHolder();\n            convertView    = inflater.inflate(R.layout.history_row_layout, parent, false);\n            holder.user    = (TextView) convertView.findViewById(R.id.history_name);\n            holder.status  = (TextView) convertView.findViewById(R.id.history_status);\n            holder.time    = (TextView) convertView.findViewById(R.id.history_time);\n            holder.callBtn = (ImageButton) convertView.findViewById(R.id.history_call);\n            convertView.setTag(holder);\n        } else {\n            holder = (ViewHolder) convertView.getTag();\n        }\n        holder.user.setText(hItem.getUser().getUserId());\n        holder.time.setText(formatTimeStamp(hItem.getTimeStamp()));\n        holder.status.setText(hItem.getUser().getStatus());\n        if (hItem.getUser().getStatus().equals(Constants.STATUS_OFFLINE))\n            getUserStatus(hItem.getUser(), holder.status);\n        holder.callBtn.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                ((MainActivity)context).dispatchCall(hItem.getUser().getUserId());\n            }\n        });\n        holder.histItem=hItem;\n        return convertView;\n    }\n\n    @Override\n    public int getCount() {\n        return this.values.size();\n    }\n\n    public void removeButton(int loc){\n        this.values.remove(loc);\n        notifyDataSetChanged();\n    }\n\n    private void getUserStatus(final ChatUser user, final TextView statusView){\n        String stdByUser = user.getUserId() + Constants.STDBY_SUFFIX;\n        this.mPubNub.getState(stdByUser, user.getUserId(), new Callback() {\n            @Override\n            public void successCallback(String channel, Object message) {\n                JSONObject jsonMsg = (JSONObject) message;\n                try {\n                    if (!jsonMsg.has(Constants.JSON_STATUS)) return;\n                    final String status = jsonMsg.getString(Constants.JSON_STATUS);\n                    user.setStatus(status);\n                    ((Activity)getContext()).runOnUiThread(new Runnable() {\n                        @Override\n                        public void run() {\n                            statusView.setText(status);\n                        }\n                    });\n                } catch (JSONException e){\n                    e.printStackTrace();\n                }\n            }\n        });\n    }\n\n    public void updateHistory(){\n        final List<HistoryItem> rtcHistory = new LinkedList<HistoryItem>();\n        String usrStdBy = this.mPubNub.getUUID() + Constants.STDBY_SUFFIX;\n        this.mPubNub.history(usrStdBy, 25, new Callback() {\n            @Override\n            public void successCallback(String channel, Object message) {\n                Log.d(\"HA-uH\",\"HISTORY: \" + message.toString());\n                try {\n                    JSONArray historyArray = ((JSONArray) message).getJSONArray(0);\n                    for(int i=0; i< historyArray.length(); i++){\n                        JSONObject historyJson = historyArray.getJSONObject(i);\n                        String userName = historyJson.getString(Constants.JSON_CALL_USER);\n                        long timeStamp  = historyJson.getLong(Constants.JSON_CALL_TIME);\n                        ChatUser cUser  = new ChatUser(userName);\n                        if (users.containsKey(userName)){\n                            cUser = users.get(userName);\n                        } else {\n                            users.put(userName, cUser);\n                        }\n                        rtcHistory.add(0, new HistoryItem(cUser, timeStamp));\n                    }\n                    values = rtcHistory;\n                    updateAdapter();\n                } catch (JSONException e){\n                    // e.printStackTrace();\n                }\n            }\n        });\n    }\n\n    private void updateAdapter(){\n        ((Activity)context).runOnUiThread(new Runnable() {\n            @Override\n            public void run() {\n                notifyDataSetChanged();\n            }\n        });\n    }\n\n    /**\n     * Format the long System.currentTimeMillis() to a better looking timestamp. Uses a calendar\n     *   object to format with the user's current time zone.\n     * @param timeStamp\n     * @return\n     */\n    public static String formatTimeStamp(long timeStamp){\n        // Create a DateFormatter object for displaying date in specified format.\n        SimpleDateFormat formatter = new SimpleDateFormat(\"MMM d, h:mm a\");\n\n        // Create a calendar object that will convert the date and time value in milliseconds to date.\n        Calendar calendar = Calendar.getInstance();\n        calendar.setTimeInMillis(timeStamp);\n        return formatter.format(calendar.getTime());\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/java/me/kevingleason/androidrtc/adt/ChatMessage.java",
    "content": "package me.kevingleason.androidrtc.adt;\n\n/**\n * Created by GleasonK on 6/25/15.\n */\npublic class ChatMessage {\n    private String sender;\n    private String message;\n    private long timeStamp;\n\n    public ChatMessage(String sender, String message, long timeStamp){\n        this.sender = sender;\n        this.message = message;\n        this.timeStamp=timeStamp;\n    }\n\n    public String getSender() {\n        return sender;\n    }\n\n    public String getMessage() {\n        return message;\n    }\n\n    public long getTimeStamp() {\n        return timeStamp;\n    }\n\n    @Override\n    public int hashCode() {\n        return (this.sender + this.message + this.timeStamp).hashCode();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/kevingleason/androidrtc/adt/ChatUser.java",
    "content": "package me.kevingleason.androidrtc.adt;\n\nimport me.kevingleason.androidrtc.util.Constants;\n\n/**\n * Created by GleasonK on 7/31/15.\n */\npublic class ChatUser {\n    private String userId;\n    private String status;\n\n    public ChatUser(String userId) {\n        this.userId = userId;\n        this.status = Constants.STATUS_OFFLINE;\n    }\n\n    public ChatUser(String userId, String status) {\n        this.userId = userId;\n        this.status = status;\n    }\n\n    public String getUserId() {\n        return userId;\n    }\n\n    public String getStatus() {\n        return status;\n    }\n\n    public void setStatus(String status) {\n        this.status = status;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this==o) return true;\n        if (!(o instanceof ChatUser)) return false;\n        ChatUser cu = (ChatUser)o;\n        return this.userId.equals(((ChatUser) o).getUserId());\n    }\n\n    @Override\n    public int hashCode() {\n        return this.getUserId().hashCode();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/kevingleason/androidrtc/adt/HistoryItem.java",
    "content": "package me.kevingleason.androidrtc.adt;\n\n/**\n * Created by GleasonK on 7/31/15.\n */\npublic class HistoryItem {\n    private ChatUser user;\n    private Long timeStamp;\n\n    public HistoryItem(ChatUser user, Long timeStamp){\n        this.user=user;\n        this.timeStamp=timeStamp;\n    }\n\n    public ChatUser getUser() {\n        return user;\n    }\n\n    public Long getTimeStamp() {\n        return timeStamp;\n    }\n}"
  },
  {
    "path": "app/src/main/java/me/kevingleason/androidrtc/servers/XirSysRequest.java",
    "content": "package me.kevingleason.androidrtc.servers;\n\nimport android.os.AsyncTask;\nimport android.util.Log;\n\nimport org.apache.http.HttpRequest;\nimport org.apache.http.HttpResponse;\nimport org.apache.http.NameValuePair;\nimport org.apache.http.client.ClientProtocolException;\nimport org.apache.http.client.HttpClient;\nimport org.apache.http.client.entity.UrlEncodedFormEntity;\nimport org.apache.http.client.methods.HttpPost;\nimport org.apache.http.impl.client.DefaultHttpClient;\nimport org.apache.http.message.BasicNameValuePair;\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\nimport org.json.JSONTokener;\nimport org.webrtc.PeerConnection;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.io.UnsupportedEncodingException;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Created by GleasonK on 11/12/15.\n */\npublic class XirSysRequest extends AsyncTask<Void,Void,List<PeerConnection.IceServer>> {\n\n    public List<PeerConnection.IceServer> doInBackground(Void... params){\n        List<PeerConnection.IceServer> servers = new ArrayList<PeerConnection.IceServer>();\n        HttpClient httpClient = new DefaultHttpClient();\n        HttpPost request = new HttpPost(\"https://service.xirsys.com/ice\");\n        List<NameValuePair> data = new ArrayList<NameValuePair>();\n        data.add(new BasicNameValuePair(\"room\", \"default\"));\n        data.add(new BasicNameValuePair(\"application\", \"default\"));\n        data.add(new BasicNameValuePair(\"domain\", \"kevingleason.me\"));\n        data.add(new BasicNameValuePair(\"ident\", \"gleasonk\"));\n        data.add(new BasicNameValuePair(\"secret\", \"b9066b5e-1f75-11e5-866a-c400956a1e19\"));\n        data.add(new BasicNameValuePair(\"secure\", \"1\"));\n        //Encoding POST data\n        try {\n            request.setEntity(new UrlEncodedFormEntity(data));\n            HttpResponse response = httpClient.execute(request);\n            // write response to log\n            Log.d(\"Http Post Response:\", response.toString());\n\n            BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), \"UTF-8\"));\n            StringBuilder builder = new StringBuilder();\n            for (String line=null; (line = reader.readLine()) != null;) {\n                builder.append(line).append(\"\\n\");\n            }\n            JSONTokener tokener = new JSONTokener(builder.toString());\n            JSONObject json = new JSONObject(tokener);\n            if (json.isNull(\"e\")){\n                JSONArray iceServers = json.getJSONObject(\"d\").getJSONArray(\"iceServers\");\n                for (int i = 0; i < iceServers.length(); i++) {\n                    JSONObject srv = iceServers.getJSONObject(i);\n                    PeerConnection.IceServer is;\n                    if (srv.has(\"username\"))\n                        is = new PeerConnection.IceServer(srv.getString(\"url\"),\n                                    srv.getString(\"username\"),srv.getString(\"credential\"));\n                    else\n                        is = new PeerConnection.IceServer(srv.getString(\"url\"));\n                    servers.add(is);\n                }\n            }\n        } catch (UnsupportedEncodingException e) {\n            e.printStackTrace();\n        } catch (ClientProtocolException e) {\n            e.printStackTrace();\n        } catch (IOException e) {\n            e.printStackTrace();\n        } catch (JSONException e){\n            e.printStackTrace();\n        }\n        Log.i(\"XIRSYS\",\"Servers: \" + servers.toString());\n        return servers;\n    }\n\n\n}\n\n/*\nfunction get_xirsys_servers() {\n    var servers;\n    $.ajax({\n        type: 'POST',\n        url: 'https://service.xirsys.com/ice',\n        data: {\n            room: 'default',\n            application: 'default',\n            domain: 'kevingleason.me',\n            ident: 'gleasonk',\n            secret: 'b9066b5e-1f75-11e5-866a-c400956a1e19',\n            secure: 1,\n        },\n        success: function(res) {\n\t        console.log(res);\n            res = JSON.parse(res);\n            if (!res.e) servers = res.d.iceServers;\n        },\n        async: false\n    });\n    return servers;\n}\n */\n"
  },
  {
    "path": "app/src/main/java/me/kevingleason/androidrtc/util/Constants.java",
    "content": "package me.kevingleason.androidrtc.util;\n\n/**\n * Created by GleasonK on 7/30/15.\n */\npublic class Constants {\n    public static final String SHARED_PREFS = \"me.kg.androidrtc.SHARED_PREFS\";\n    public static final String USER_NAME    = \"me.kg.androidrtc.SHARED_PREFS.USER_NAME\";\n    public static final String CALL_USER    = \"me.kg.androidrtc.SHARED_PREFS.CALL_USER\";\n    public static final String STDBY_SUFFIX = \"-stdby\";\n\n    public static final String PUB_KEY = \"pub-c-9d0d75a5-38db-404f-ac2a-884e18b041d8\"; // Your Pub Key\n    public static final String SUB_KEY = \"sub-c-4e25fb64-37c7-11e5-a477-0619f8945a4f\"; // Your Sub Key\n\n//    public static final String PUB_KEY = \"demo\"; // Your Pub Key\n//    public static final String SUB_KEY = \"demo\"; // Your Sub Key\n\n    public static final String JSON_CALL_USER = \"call_user\";\n    public static final String JSON_CALL_TIME = \"call_time\";\n    public static final String JSON_OCCUPANCY = \"occupancy\";\n    public static final String JSON_STATUS    = \"status\";\n\n    // JSON for user messages\n    public static final String JSON_USER_MSG  = \"user_message\";\n    public static final String JSON_MSG_UUID  = \"msg_uuid\";\n    public static final String JSON_MSG       = \"msg_message\";\n    public static final String JSON_TIME      = \"msg_timestamp\";\n\n    public static final String STATUS_AVAILABLE = \"Available\";\n    public static final String STATUS_OFFLINE   = \"Offline\";\n    public static final String STATUS_BUSY      = \"Busy\";\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/kevingleason/androidrtc/util/LogRTCListener.java",
    "content": "package me.kevingleason.androidrtc.util;\n\nimport android.util.Log;\n\nimport org.webrtc.MediaStream;\n\nimport me.kevingleason.pnwebrtc.PnPeer;\nimport me.kevingleason.pnwebrtc.PnRTCListener;\nimport me.kevingleason.pnwebrtc.PnRTCMessage;\n\n/**\n * <p>Created by GleasonK on 7/23/15.</p>\n */\npublic class LogRTCListener extends PnRTCListener {\n    @Override\n    public void onCallReady(String callId) {\n        Log.i(\"RTCListener\", \"OnCallReady - \" + callId);\n    }\n\n    @Override\n    public void onConnected(String userId) {\n        Log.i(\"RTCListener\", \"OnConnected - \" + userId);\n    }\n\n    @Override\n    public void onPeerStatusChanged(PnPeer peer) {\n        Log.i(\"RTCListener\", \"OnPeerStatusChanged - \" + peer.toString());\n    }\n\n    @Override\n    public void onPeerConnectionClosed(PnPeer peer) {\n        Log.i(\"RTCListener\", \"OnPeerConnectionClosed - \" + peer.toString());\n    }\n\n    @Override\n    public void onLocalStream(MediaStream localStream) {\n        Log.i(\"RTCListener\", \"OnLocalStream - \" + localStream.toString());\n    }\n\n    @Override\n    public void onAddRemoteStream(MediaStream remoteStream, PnPeer peer) {\n        Log.i(\"RTCListener\", \"OnAddRemoteStream - \" + peer.toString());\n    }\n\n    @Override\n    public void onRemoveRemoteStream(MediaStream remoteStream, PnPeer peer) {\n        Log.i(\"RTCListener\", \"OnRemoveRemoteStream - \" + peer.toString());\n    }\n\n    @Override\n    public void onMessage(PnPeer peer, Object message) {\n        Log.i(\"RTCListener\", \"OnMessage - \" + message.toString());\n    }\n\n    @Override\n    public void onDebug(PnRTCMessage message) {\n        Log.i(\"RTCListener\", \"OnDebug - \" + message.getMessage());\n    }\n}\n"
  },
  {
    "path": "app/src/main/res/drawable/light_fade_down.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item>\n        <shape>\n            <gradient\n                android:angle=\"90\"\n                android:endColor=\"#c8747474\"\n                android:startColor=\"#00000000\"\n                android:type=\"linear\" />\n        </shape>\n    </item>\n</selector>"
  },
  {
    "path": "app/src/main/res/drawable/light_fade_up.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item>\n        <shape>\n            <gradient\n                android:angle=\"90\"\n                android:startColor=\"#c8747474\"\n                android:endColor=\"#00000000\"\n                android:type=\"linear\" />\n        </shape>\n    </item>\n</selector>"
  },
  {
    "path": "app/src/main/res/drawable/online_circle.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"oval\">\n\n    <solid\n        android:color=\"#ff00f306\"/>\n\n    <size\n        android:width=\"10dp\"\n        android:height=\"10dp\"/>\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/round_button.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n\n    <solid android:color=\"#b4ff0000\" />\n    <padding android:top=\"5dp\"\n        android:right=\"5dp\"\n        android:left=\"5dp\"\n        android:bottom=\"5dp\"/>\n\n    <corners android:bottomRightRadius=\"45dip\"\n              android:bottomLeftRadius=\"45dip\"\n                android:topRightRadius=\"45dip\"\n                 android:topLeftRadius=\"45dip\"/>\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/round_button_send.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n\n    <gradient\n        android:angle=\"135\"\n        android:endColor=\"@color/pn_blue\"\n        android:startColor=\"@color/pn_blue_dark\"\n        android:type=\"linear\" />\n    <padding android:top=\"5dp\"\n        android:right=\"5dp\"\n        android:left=\"5dp\"\n        android:bottom=\"5dp\"/>\n\n    <corners android:bottomRightRadius=\"45dip\"\n              android:bottomLeftRadius=\"45dip\"\n                android:topRightRadius=\"45dip\"\n                 android:topLeftRadius=\"45dip\"/>\n</shape>"
  },
  {
    "path": "app/src/main/res/layout/activity_incoming_call.xml",
    "content": "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\" android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\"me.kevingleason.pubrtc.IncomingCallActivity\"\n    android:orientation=\"vertical\"\n    android:background=\"@color/pn_blue\">\n\n    <TextView\n        android:text=\"@string/incoming_call\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:textSize=\"26sp\"\n        android:textColor=\"@color/white\"\n        android:layout_margin=\"25dp\"/>\n\n    <ImageView\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:src=\"@drawable/ic_pubrtc\"/>\n\n    <TextView\n        android:id=\"@+id/caller_id\"\n        android:text=\"@string/incoming_call_from\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:textSize=\"26sp\"\n        android:textColor=\"@color/white\"\n        android:layout_margin=\"25dp\"\n        />\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_marginTop=\"60dp\">\n\n        <ImageButton\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_weight=\"1\"\n            android:background=\"@color/green\"\n            android:onClick=\"acceptCall\"\n            android:src=\"@drawable/ic_action_call\"/>\n        <ImageButton\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:background=\"@color/pn_red\"\n            android:layout_weight=\"1\"\n            android:onClick=\"rejectCall\"\n            android:src=\"@drawable/ic_action_end_call\"/>\n\n    </LinearLayout>\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_login.xml",
    "content": "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\" android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\" android:paddingLeft=\"@dimen/activity_horizontal_margin\"\n    android:paddingRight=\"@dimen/activity_horizontal_margin\"\n    android:paddingTop=\"@dimen/activity_vertical_margin\"\n    android:paddingBottom=\"@dimen/activity_vertical_margin\"\n    tools:context=\"me.kevingleason.pubnubchat.LoginActivity\">\n\n    <TextView\n        android:text=\"@string/login\"\n        android:textSize=\"35sp\"\n        android:gravity=\"center\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"40dp\"\n        android:textColor=\"@color/pn_blue\"\n        android:id=\"@+id/textView\"\n        android:layout_alignParentTop=\"true\"\n        android:layout_centerHorizontal=\"true\" />\n\n    <EditText\n        android:hint=\"@string/username\"\n        android:inputType=\"text\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"75dp\"\n        android:textSize=\"30sp\"\n        android:textColor=\"#333\"\n        android:gravity=\"center\"\n        android:id=\"@+id/login_username\"\n        android:layout_below=\"@+id/textView\"\n        android:layout_alignParentRight=\"true\"\n        android:layout_alignParentEnd=\"true\"\n        android:layout_marginTop=\"45dp\" />\n\n    <Button\n        android:id=\"@+id/button\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"100dp\"\n        android:text=\"@string/login_submit\"\n        android:textSize=\"25sp\"\n        android:textColor=\"#FFF\"\n        android:background=\"@color/pn_red\"\n        android:layout_above=\"@+id/imageHolder\"\n        android:layout_alignParentLeft=\"true\"\n        android:layout_alignParentStart=\"true\"\n        android:layout_marginBottom=\"35dp\"\n        android:onClick=\"joinChat\"/>\n\n    <LinearLayout\n        android:id=\"@+id/imageHolder\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginBottom=\"10dp\"\n        android:layout_alignParentBottom=\"true\">\n\n        <ImageView\n            android:layout_width=\"0dp\"\n            android:layout_height=\"70dp\"\n            android:layout_weight=\"2\"\n            android:src=\"@drawable/pow_by_pubnub\"\n            android:layout_alignParentBottom=\"true\"\n            android:layout_alignParentLeft=\"true\"\n            android:layout_alignParentStart=\"true\"\n            android:layout_marginBottom=\"10dp\" />\n\n        <ImageView\n            android:layout_width=\"0dp\"\n            android:layout_height=\"70dp\"\n            android:layout_weight=\"1\"\n            android:src=\"@drawable/ic_pubrtc\"\n            android:layout_alignParentBottom=\"true\"\n            android:layout_alignParentLeft=\"true\"\n            android:layout_alignParentStart=\"true\"\n            android:layout_marginBottom=\"10dp\" />\n\n    </LinearLayout>\n\n</RelativeLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "content": "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\" android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".MainActivity\"\n    android:orientation=\"vertical\">\n\n    <RelativeLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"50dp\"\n        android:background=\"@color/pn_red\"\n        >\n\n        <TextView\n            android:id=\"@+id/main_username\"\n            android:text=\"Kevin\"\n            android:layout_centerInParent=\"true\"\n            android:textColor=\"@color/white\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:textSize=\"25sp\"/>\n\n        <View\n            android:layout_alignParentBottom=\"true\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"3dp\"\n            android:background=\"@drawable/light_fade_up\"/>\n\n    </RelativeLayout>\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:paddingLeft  =\"@dimen/activity_horizontal_margin\"\n        android:paddingRight =\"@dimen/activity_horizontal_margin\"\n        android:paddingTop   =\"10dp\"\n        android:paddingBottom=\"10dp\"\n        android:orientation=\"horizontal\">\n        <EditText\n            android:id=\"@+id/call_num\"\n            android:hint=\"Enter a number...\"\n            android:inputType=\"textShortMessage\"\n            android:maxLines=\"1\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_weight=\"4\"/>\n        <RelativeLayout\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_weight=\"1\">\n            <ImageButton\n                android:background=\"@drawable/round_button_send\"\n                android:src=\"@drawable/ic_action_call\"\n                android:layout_centerInParent=\"true\"\n                android:layout_height=\"50dp\"\n                android:layout_width =\"50dp\"\n                android:scaleType=\"fitCenter\"\n                android:padding=\"8dp\"\n                android:onClick=\"makeCall\"/>\n        </RelativeLayout>\n\n    </LinearLayout>\n\n    <View\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"3dp\"\n        android:background=\"@drawable/light_fade_down\"/>\n\n    <ListView\n        android:id=\"@android:id/list\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        />\n    <TextView\n        android:id=\"@android:id/empty\"\n        android:background=\"@color/grey\"\n        android:gravity=\"center\"\n        android:textSize=\"20sp\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:text=\"@string/no_call_history\"\n        />\n\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_video_chat.xml",
    "content": "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\" android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\"me.kevingleason.androidrtc.VideoChatActivity\">\n\n    <android.opengl.GLSurfaceView\n        android:id=\"@+id/gl_surface\"\n        android:layout_height=\"match_parent\"\n        android:layout_width=\"match_parent\" />\n\n    <TextView\n        android:id=\"@+id/call_status\"\n        android:text=\"Connecting...\"\n        android:padding=\"10dp\"\n        android:textSize=\"25sp\"\n        android:layout_centerHorizontal=\"true\"\n        android:layout_above=\"@+id/call_chat_box\"\n        android:textColor=\"@color/white\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\" />\n\n    <LinearLayout\n        android:id=\"@+id/call_chat_box\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"10dp\"\n        android:background=\"#64000000\"\n        android:layout_alignParentBottom=\"true\"\n        android:orientation=\"horizontal\">\n\n        <EditText\n            android:id=\"@+id/chat_input\"\n            android:textColor=\"#FFF\"\n            android:backgroundTint=\"@color/pn_blue\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_weight=\"4\"\n            android:textColorHint=\"@color/white\"\n            android:hint=\"Enter Message...\"/>\n\n        <RelativeLayout\n            android:layout_width=\"0dp\"\n            android:layout_weight=\"1\"\n            android:layout_height=\"match_parent\">\n\n            <ImageButton\n                android:layout_centerHorizontal=\"true\"\n                android:contentDescription=\"Send\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"@drawable/round_button_send\"\n                android:src=\"@drawable/ic_action_send_now\"\n                android:onClick=\"sendMessage\" />\n\n        </RelativeLayout>\n\n    </LinearLayout>\n\n    <ListView\n        android:id=\"@android:id/list\"\n        android:layout_height=\"match_parent\"\n        android:layout_width=\"wrap_content\"\n        android:minWidth=\"300dp\"\n        android:layout_alignParentEnd=\"true\"\n        android:layout_above=\"@id/call_chat_box\"\n        android:stackFromBottom=\"true\"\n        android:divider=\"@null\"\n        android:dividerHeight=\"0dp\"\n        />\n\n    <ImageButton\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"10dp\"\n        android:src=\"@drawable/ic_action_end_call\"\n        android:background=\"@drawable/round_button\"\n        android:onClick=\"hangup\"\n        />\n\n</RelativeLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/chat_message_row_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"@color/pn_blue\"\n    android:padding=\"10dp\">\n\n    <RelativeLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n        <TextView\n            android:id=\"@+id/chat_user\"\n            android:layout_alignParentStart=\"true\"\n            android:textColor=\"@color/white\"\n            android:text=\"Kevin\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" />\n\n        <TextView\n            android:id=\"@+id/chat_timestamp\"\n            android:text=\"now\"\n            android:textColor=\"@color/white\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentEnd=\"true\"/>\n\n    </RelativeLayout>\n\n\n    <TextView\n        android:id=\"@+id/chat_message\"\n        android:textColor=\"@color/white\"\n        android:text=\"Hello World!\"\n        android:textSize=\"20sp\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/history_row_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <RelativeLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"10dp\">\n\n        <LinearLayout\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_centerVertical=\"true\"\n            android:orientation=\"vertical\">\n\n            <TextView\n                android:id=\"@+id/history_name\"\n                android:text=\"Kevin Gleason\"\n                android:textSize=\"20sp\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_centerVertical=\"true\"/>\n\n\n            <LinearLayout\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"horizontal\">\n                <TextView\n                    android:id=\"@+id/history_time\"\n                    android:text=\"Aug 3, 7:30pm\"\n                    android:layout_marginEnd=\"5dp\"\n                    android:layout_marginRight=\"5dp\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:textSize=\"12sp\"/>\n\n                <TextView\n                    android:id=\"@+id/history_status\"\n                    android:text=\"Offline\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:textSize=\"12sp\"/>\n\n            </LinearLayout>\n\n\n        </LinearLayout>\n\n\n        <ImageButton\n            android:id=\"@+id/history_call\"\n            android:src=\"@drawable/ic_action_call_dark\"\n            android:background=\"@null\"\n            android:layout_width=\"50dp\"\n            android:layout_height=\"50dp\"\n            android:padding=\"10dp\"\n            android:scaleType=\"fitCenter\"\n            android:layout_centerVertical=\"true\"\n            android:layout_alignParentEnd=\"true\"\n            android:layout_alignParentRight=\"true\"/>\n\n    </RelativeLayout>\n\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/menu/menu_incoming_call.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:context=\"me.kevingleason.pubrtc.IncomingCallActivity\">\n    <item android:id=\"@+id/action_settings\" android:title=\"@string/action_settings\"\n        android:orderInCategory=\"100\" android:showAsAction=\"never\" />\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/menu_login.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:context=\"me.kevingleason.androidrtc.LoginActivity\">\n    <item android:id=\"@+id/action_settings\" android:title=\"@string/action_settings\"\n        android:orderInCategory=\"100\" android:showAsAction=\"never\" />\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/menu_main.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\" tools:context=\".MainActivity\">\n    <item android:id=\"@+id/action_sign_out\" android:title=\"@string/action_sign_out\"\n        android:orderInCategory=\"1\" android:showAsAction=\"never\" />\n    <item android:id=\"@+id/action_settings\" android:title=\"@string/action_settings\"\n    android:orderInCategory=\"100\" android:showAsAction=\"never\" />\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/menu_video_chat.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:context=\"me.kevingleason.androidrtc.VideoChatActivity\">\n    <item android:id=\"@+id/action_settings\" android:title=\"@string/action_settings\"\n        android:orderInCategory=\"100\" android:showAsAction=\"never\" />\n</menu>\n"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"pn_red\">#d02129</color>\n    <color name=\"pn_blue\">#ff19a2c6</color>\n    <color name=\"pn_blue_dark\">#ff187998</color>\n    <color name=\"white\">#FFF</color>\n    <color name=\"green\">#ff1ce322</color>\n    <color name=\"grey\">#eee</color>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "content": "<resources>\n    <!-- Default screen margins, per the Android Design guidelines. -->\n    <dimen name=\"activity_horizontal_margin\">16dp</dimen>\n    <dimen name=\"activity_vertical_margin\">16dp</dimen>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <string name=\"app_name\">AndroidRTC</string>\n    <string name=\"hello_world\">Hello world!</string>\n    <string name=\"action_settings\">Settings</string>\n    <string name=\"title_activity_video_chat\">Video Call</string>\n    <string name=\"title_activity_incoming_call\">Incoming Call</string>\n\n    <string name=\"title_activity_login\">Login</string>\n    <string name=\"login\">Choose a username:</string>\n    <string name=\"username\">Username</string>\n    <string name=\"login_submit\">Sign in!</string>\n\n    <string name=\"enter_new_channel\">Enter new channel:</string>\n    <string name=\"action_sign_out\">Sign Out</string>\n    <string name=\"here_now\">Here Now:</string>\n    <string name=\"no_call_history\">No call History\\n\\nRecent calls appear here.</string>\n\n    <!-- Incoming Call Activity -->\n    <string name=\"incoming_call\">Incoming Call&#8230;</string>\n    <string name=\"incoming_call_from\">Kevin Gleason</string>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"android:Theme.Holo.Light.DarkActionBar\">\n    </style>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-v21/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <style name=\"AppTheme\" parent=\"android:Theme.Material.Light\">\n        <item name=\"android:colorPrimary\">@color/pn_blue</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-w820dp/dimens.xml",
    "content": "<resources>\n    <!-- Example customization of dimensions originally defined in res/values/dimens.xml\n         (such as screen margins) for screens with more than 820dp of available width. This\n         would include 7\" and 10\" devices in landscape (~960dp and ~1280dp respectively). -->\n    <dimen name=\"activity_horizontal_margin\">64dp</dimen>\n</resources>\n"
  },
  {
    "path": "build.gradle",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n    repositories {\n        jcenter()\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:0.13.2'\n\n        // NOTE: Do not place your application dependencies here; they belong\n        // in the individual module build.gradle files\n    }\n}\n\nallprojects {\n    repositories {\n        jcenter()\n    }\n}\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Wed Apr 10 15:27:10 PDT 2013\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-2.1-all.zip\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\n# Default value: -Xmx10248m -XX:MaxPermSize=256m\n# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8\n\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n\nVERSION_NAME=1.0.6\nVERSION_CODE=6\nGROUP=me.kevingleason\n\nPOM_DESCRIPTION=Android WebRTC signaling library using PubNub\nPOM_URL=https://github.com/GleasonK/pubnub-android-webrtc\nPOM_SCM_URL=https://github.com/GleasonK/pubnub-android-webrtc\nPOM_SCM_CONNECTION=scm:git@github.com:GleasonK/pubnub-android-webrtc.git\nPOM_SCM_DEV_CONNECTION=scm:git@github.com:GleasonK/pubnub-android-webrtc.git\nPOM_LICENCE_NAME=The Apache Software License, Version 2.0\nPOM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt\nPOM_LICENCE_DIST=repo\nPOM_DEVELOPER_ID=GleasonK\nPOM_DEVELOPER_NAME=Kevin Gleason"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn ( ) {\n    echo \"$*\"\n}\n\ndie ( ) {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\nesac\n\n# For Cygwin, ensure paths are in UNIX format before anything is touched.\nif $cygwin ; then\n    [ -n \"$JAVA_HOME\" ] && JAVA_HOME=`cygpath --unix \"$JAVA_HOME\"`\nfi\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >&-\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >&-\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules\nfunction splitJvmOpts() {\n    JVM_OPTS=(\"$@\")\n}\neval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\nJVM_OPTS[${#JVM_OPTS[*]}]=\"-Dorg.gradle.appname=$APP_BASE_NAME\"\n\nexec \"$JAVACMD\" \"${JVM_OPTS[@]}\" -classpath \"$CLASSPATH\" org.gradle.wrapper.GradleWrapperMain \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif \"%ERRORLEVEL%\" == \"0\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:init\r\n@rem Get command-line arguments, handling Windowz variants\r\n\r\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\r\nif \"%@eval[2+2]\" == \"4\" goto 4NT_args\r\n\r\n:win9xME_args\r\n@rem Slurp the command line arguments.\r\nset CMD_LINE_ARGS=\r\nset _SKIP=2\r\n\r\n:win9xME_args_slurp\r\nif \"x%~1\" == \"x\" goto execute\r\n\r\nset CMD_LINE_ARGS=%*\r\ngoto execute\r\n\r\n:4NT_args\r\n@rem Get arguments from the 4NT Shell from JP Software\r\nset CMD_LINE_ARGS=%$\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\r\nexit /b 1\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "pnwebrtc/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "pnwebrtc/build.gradle",
    "content": "apply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion 21\n    buildToolsVersion \"20.0.0\"\n\n    defaultConfig {\n        applicationId \"me.kevingleason.pnwebrtc\"\n        minSdkVersion 15\n        targetSdkVersion 21\n        versionName project.VERSION_NAME\n        versionCode Integer.parseInt(project.VERSION_CODE)\n    }\n    buildTypes {\n        release {\n            runProguard false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n}\n\ndependencies {\n    compile fileTree(dir: 'libs', include: ['*.jar'])\n    compile 'io.pristine:libjingle:9694@aar'\n    compile 'com.pubnub:pubnub-android:3.7.4'\n}\n\n//apply from: '../maven_push.gradle'\n"
  },
  {
    "path": "pnwebrtc/gradle.properties",
    "content": "POM_NAME=PubNub WebRTC API\nPOM_ARTIFACT_ID=pnwebrtc\nPOM_PACKAGING=aar\n"
  },
  {
    "path": "pnwebrtc/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /Users/GleasonK/algs4/AndroidSDK/sdk/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n"
  },
  {
    "path": "pnwebrtc/src/androidTest/java/me/kevingleason/pnwebrtc/ApplicationTest.java",
    "content": "package me.kevingleason.pnwebrtc;\n\nimport android.app.Application;\nimport android.test.ApplicationTestCase;\n\n/**\n * <a href=\"http://d.android.com/tools/testing/testing_android.html\">Testing Fundamentals</a>\n */\npublic class ApplicationTest extends ApplicationTestCase<Application> {\n    public ApplicationTest() {\n        super(Application.class);\n    }\n}"
  },
  {
    "path": "pnwebrtc/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"me.kevingleason.pnwebrtc\">\n\n    <!-- WebRTC Dependencies -->\n    <uses-feature android:name=\"android.hardware.camera\" />\n    <uses-feature android:name=\"android.hardware.camera.autofocus\" />\n    <uses-feature\n        android:glEsVersion=\"0x00020000\"\n        android:required=\"true\" />\n    <uses-permission android:name=\"android.permission.CAMERA\" />\n    <uses-permission android:name=\"android.permission.RECORD_AUDIO\" />\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\" />\n\n    <!-- PubNub Dependencies -->\n    <!--<uses-permission android:name=\"android.permission.INTERNET\" />-->\n    <uses-permission android:name=\"android.permission.WAKE_LOCK\" />\n    <uses-permission android:name=\"com.google.android.c2dm.permission.RECEIVE\" />\n    <permission android:name=\"your.package.name.permission.C2D_MESSAGE\" android:protectionLevel=\"signature\" />\n    <uses-permission android:name=\"your.package.name.permission.C2D_MESSAGE\" />\n\n    <application android:allowBackup=\"true\" android:label=\"@string/app_name\">\n\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "pnwebrtc/src/main/java/me/kevingleason/pnwebrtc/PnPeer.java",
    "content": "package me.kevingleason.pnwebrtc;\n\nimport android.util.Log;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\nimport org.webrtc.DataChannel;\nimport org.webrtc.IceCandidate;\nimport org.webrtc.MediaStream;\nimport org.webrtc.PeerConnection;\nimport org.webrtc.SdpObserver;\nimport org.webrtc.SessionDescription;\n\n/**\n * <h1>PubNub Peer object to hold information on {@link org.webrtc.PeerConnection}</h1>\n * <pre>\n * Author:  Kevin Gleason - Boston College '16\n * File:    PnPeer.java\n * Date:    7/22/15\n * Use:     Store information about various Peer Connections\n * &copy; 2009 - 2015 PubNub, Inc.\n * </pre>\n */\npublic class PnPeer implements SdpObserver, PeerConnection.Observer {\n    public static final String TAG = \"PnPeer\";\n    public static final String STATUS_CONNECTING   = \"CONNECTING\";\n    public static final String STATUS_CONNECTED    = \"CONNECTED\"; // TODO: Where to change status to this?\n    public static final String STATUS_DISCONNECTED = \"DISCONNECTED\";\n    public static final String TYPE_NONE           = \"NONE\";\n    public static final String TYPE_OFFER          = \"offer\";\n    public static final String TYPE_ANSWER         = \"answer\";\n\n    private PnPeerConnectionClient pcClient;\n    PeerConnection pc;\n    String id;\n    String type;\n    String status;\n    boolean dialed;\n    boolean received;\n    // Todo: Maybe attach MediaStream as private var?\n\n    public PnPeer(String id, PnPeerConnectionClient pcClient) {\n        Log.d(TAG, \"new Peer: \" + id);\n        this.id = id;\n        this.type = TYPE_NONE;\n        this.dialed = false;\n        this.received = false;\n        this.pcClient = pcClient;\n        this.pc = pcClient.pcFactory.createPeerConnection(pcClient.signalingParams.iceServers,\n                pcClient.signalingParams.pcConstraints, this);\n        setStatus(STATUS_CONNECTING);\n        pc.addStream(pcClient.getLocalMediaStream());\n    }\n\n    public synchronized void setStatus(String status){\n        this.status = status;\n        pcClient.mRtcListener.onPeerStatusChanged(this);\n    }\n\n    public String getStatus() {\n        return status;\n    }\n\n    public void setType(String type){this.type = type;}\n\n    public String getType() {\n        return type;\n    }\n\n    public boolean isDialed() {\n        return dialed;\n    }\n\n    public void setDialed(boolean dialed) {\n        this.dialed = dialed;\n    }\n\n    public boolean isReceived() {\n        return received;\n    }\n\n    public void setReceived(boolean received) {\n        this.received = received;\n    }\n\n    public PeerConnection getPc() {\n        return pc;\n    }\n\n    public String getId() {\n        return id;\n    }\n\n    public void hangup(){\n        if (this.status.equals(STATUS_DISCONNECTED)) return; // Already hung up on.\n        this.pcClient.removePeer(this.id);\n        setStatus(STATUS_DISCONNECTED);\n    }\n\n    @Override\n    public void onCreateSuccess(final SessionDescription sdp) {\n        // TODO: modify sdp to use pcParams prefered codecs\n        try {\n            JSONObject payload = new JSONObject();\n            payload.put(\"type\", sdp.type.canonicalForm());\n            payload.put(\"sdp\", sdp.description);\n            pcClient.transmitMessage(id, payload);\n            pc.setLocalDescription(PnPeer.this, sdp);\n        } catch (JSONException e) {\n            e.printStackTrace();\n        }\n    }\n\n    @Override\n    public void onSetSuccess() {\n    }\n\n    @Override\n    public void onCreateFailure(String s) {\n    }\n\n    @Override\n    public void onSetFailure(String s) {\n    }\n\n    @Override\n    public void onSignalingChange(PeerConnection.SignalingState signalingState) {\n    }\n\n    @Override\n    public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {\n        if (this.status.equals(STATUS_DISCONNECTED)) return; // Already hung up on.\n        if (iceConnectionState == PeerConnection.IceConnectionState.DISCONNECTED) {\n            pcClient.removePeer(id); // TODO: Ponder. Also, might want to Pub a disconnect.\n            setStatus(STATUS_DISCONNECTED);\n        }\n    }\n\n    // Todo: Look into what this should be used for\n    @Override\n    public void onIceConnectionReceivingChange(boolean iceConnectionReceivingChange){\n\n    }\n\n    @Override\n    public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {\n    }\n\n    @Override\n    public void onIceCandidate(final IceCandidate candidate) {\n        try {\n            JSONObject payload = new JSONObject();\n            payload.put(\"sdpMLineIndex\", candidate.sdpMLineIndex);\n            payload.put(\"sdpMid\", candidate.sdpMid);\n            payload.put(\"candidate\", candidate.sdp);\n            pcClient.transmitMessage(id, payload);\n        } catch (JSONException e) {\n            e.printStackTrace();\n        }\n    }\n\n    @Override\n    public void onAddStream(MediaStream mediaStream) {\n        Log.d(TAG, \"onAddStream \" + mediaStream.label());\n        // remote streams are displayed from 1 to MAX_PEER (0 is localStream)\n        pcClient.mRtcListener.onAddRemoteStream(mediaStream, PnPeer.this);\n    }\n\n    @Override\n    public void onRemoveStream(MediaStream mediaStream) {\n        Log.d(TAG, \"onRemoveStream \" + mediaStream.label());\n        PnPeer peer = pcClient.removePeer(id);\n        pcClient.mRtcListener.onRemoveRemoteStream(mediaStream, peer);\n    }\n\n    @Override\n    public void onDataChannel(DataChannel dataChannel) {\n    }\n\n    @Override\n    public void onRenegotiationNeeded() {\n\n    }\n\n    /**\n     * Overriding toString for debugging purposes.\n     * @return String representation of a peer.\n     */\n    @Override\n    public String toString(){\n        return this.id + \" Status: \" + this.status + \" Dialed: \" + this.dialed +\n                \" Received: \" + this.received + \" Type: \" + this.type;\n    }\n\n}\n"
  },
  {
    "path": "pnwebrtc/src/main/java/me/kevingleason/pnwebrtc/PnPeerConnectionClient.java",
    "content": "package me.kevingleason.pnwebrtc;\n\nimport android.util.Log;\n\nimport com.pubnub.api.Callback;\nimport com.pubnub.api.Pubnub;\nimport com.pubnub.api.PubnubError;\nimport com.pubnub.api.PubnubException;\n\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\nimport org.webrtc.IceCandidate;\nimport org.webrtc.MediaStream;\nimport org.webrtc.PeerConnection;\nimport org.webrtc.PeerConnectionFactory;\nimport org.webrtc.SessionDescription;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * <h1>PeerConnection manager for {@link me.kevingleason.pnwebrtc.PnRTCClient}</h1>\n * <pre>\n * Author:  Kevin Gleason - Boston College '16\n * File:    PnPeerConnectionClient.java\n * Date:    7/20/15\n * Use:     WebRTC PeerConnection Manager\n * &copy; 2009 - 2015 PubNub, Inc.\n * </pre>\n *\n * {@link PnPeerConnectionClient} is used to manage peer connections.\n */\npublic class PnPeerConnectionClient {\n    private SessionDescription localSdp  = null; // either offer or answer SDP\n    private MediaStream localMediaStream = null;\n    PeerConnectionFactory pcFactory;\n    PnRTCListener mRtcListener;\n    PnSignalingParams signalingParams;\n    int MAX_CONNECTIONS = Integer.MAX_VALUE;\n\n    private Pubnub mPubNub;\n    private PnRTCReceiver mSubscribeReceiver;\n    private Map<String,PnAction> actionMap;\n    private Map<String,PnPeer> peers;\n    private String id;\n\n    public PnPeerConnectionClient(Pubnub pubnub, PnSignalingParams signalingParams, PnRTCListener rtcListener){\n        this.mPubNub = pubnub;\n        this.signalingParams = signalingParams;\n        this.mRtcListener = rtcListener;\n        this.pcFactory = new PeerConnectionFactory(); // TODO: Check it allowed, else extra param\n        this.peers = new HashMap<String, PnPeer>();\n        init();\n    }\n\n    private void init(){\n        this.actionMap = new HashMap<String, PnAction>();\n        this.actionMap.put(CreateOfferAction.TRIGGER,     new CreateOfferAction());\n        this.actionMap.put(CreateAnswerAction.TRIGGER,    new CreateAnswerAction());\n        this.actionMap.put(SetRemoteSDPAction.TRIGGER,    new SetRemoteSDPAction());\n        this.actionMap.put(AddIceCandidateAction.TRIGGER, new AddIceCandidateAction());\n        this.actionMap.put(PnUserHangupAction.TRIGGER,    new PnUserHangupAction());\n        this.actionMap.put(PnUserMessageAction.TRIGGER,   new PnUserMessageAction());\n        mSubscribeReceiver = new PnRTCReceiver();\n    }\n\n    boolean listenOn(String myId){  // Todo: return success?\n        if (localMediaStream==null){       // Not true for streaming?\n            mRtcListener.onDebug(new PnRTCMessage(\"Need to add media stream before you can connect.\"));\n            return false;\n        }\n        if (this.id != null){  // Prevent listening on multiple channels.\n            mRtcListener.onDebug(new PnRTCMessage(\"Already listening on \" + this.id + \". Cannot have multiple connections.\"));\n            return false;\n        }\n        this.id = myId;\n        subscribe(myId);\n        return true;\n    }\n\n    /**TODO: Add a max user threshold.\n     * Connect with another user by their ID.\n     * @param userId The user to establish a WebRTC connection with\n     * @return boolean value of success\n     */\n    boolean connect(String userId) {\n        if (!peers.containsKey(userId)) { // Prevents duplicate dials.\n            if (peers.size() < MAX_CONNECTIONS) {\n                PnPeer peer = addPeer(userId);\n                peer.pc.addStream(this.localMediaStream);\n                try {\n                    actionMap.get(CreateOfferAction.TRIGGER).execute(userId, new JSONObject());\n                } catch (JSONException e){\n                    e.printStackTrace();\n                    return false;\n                }\n                return true;\n            }\n        }\n        this.mRtcListener.onDebug(new PnRTCMessage(\"CONNECT FAILED. Duplicate dial or max peer \" +\n                \"connections exceeded. Max: \" + MAX_CONNECTIONS + \" Current: \" + this.peers.size()));\n        return false;\n    }\n\n    public void setRTCListener(PnRTCListener listener){\n        this.mRtcListener = listener;\n    }\n\n    private void subscribe(String channel){\n        try {\n            mPubNub.subscribe(channel, this.mSubscribeReceiver);\n        } catch (PubnubException e){\n            e.printStackTrace();\n        }\n    }\n\n    public void setLocalMediaStream(MediaStream localStream){\n        this.localMediaStream = localStream;\n        mRtcListener.onLocalStream(localStream);\n    }\n\n    public MediaStream getLocalMediaStream(){\n        return this.localMediaStream;\n    }\n\n    private PnPeer addPeer(String id) {\n        PnPeer peer = new PnPeer(id, this);\n        peers.put(id, peer);\n        return peer;\n    }\n\n    PnPeer removePeer(String id) {\n        PnPeer peer = peers.get(id);\n        peer.pc.close();\n        return peers.remove(peer.id);\n    }\n\n    List<PnPeer> getPeers(){\n        return new ArrayList<PnPeer>(this.peers.values());\n    }\n\n    /**\n     * Close connection (hangup) no a certain peer.\n     * @param id PnPeer id to close connection with\n     */\n    public void closeConnection(String id){\n        JSONObject packet = new JSONObject();\n        try {\n            if (!this.peers.containsKey(id)) return;\n            PnPeer peer = this.peers.get(id);\n            peer.hangup();\n            packet.put(PnRTCMessage.JSON_HANGUP, true);\n            transmitMessage(id, packet);\n            mRtcListener.onPeerConnectionClosed(peer);\n        } catch (JSONException e){\n            e.printStackTrace();\n        }\n    }\n\n    /**\n     * Close connections (hangup) on all open connections.\n     */\n    public void closeAllConnections() {\n        Iterator<String> peerIds = this.peers.keySet().iterator();\n        while (peerIds.hasNext()) {\n            closeConnection(peerIds.next());\n        }\n    }\n\n    /**\n     * Send SDP Offers/Answers nd ICE candidates to peers.\n     * @param toID The id or \"number\" that you wish to transmit a message to.\n     * @param packet The JSON data to be transmitted\n     */\n    void transmitMessage(String toID, JSONObject packet){\n        if (this.id==null){ // Not logged in. Put an error in the debug cb.\n            mRtcListener.onDebug(new PnRTCMessage(\"Cannot transmit before calling Client.connect\"));\n        }\n        try {\n            JSONObject message = new JSONObject();\n            message.put(PnRTCMessage.JSON_PACKET, packet);\n            message.put(PnRTCMessage.JSON_ID, \"\"); //Todo: session id, unused in js SDK?\n            message.put(PnRTCMessage.JSON_NUMBER, this.id);\n            this.mPubNub.publish(toID, message, new Callback() {  // Todo: reconsider callback.\n                @Override\n                public void successCallback(String channel, Object message, String timetoken) {\n                    mRtcListener.onDebug(new PnRTCMessage((JSONObject)message));\n                }\n\n                @Override\n                public void errorCallback(String channel, PubnubError error) {\n                    mRtcListener.onDebug(new PnRTCMessage(error.errorObject));\n                }\n            });\n        } catch (JSONException e){\n            e.printStackTrace();\n        }\n    }\n\n    private interface PnAction{\n        void execute(String peerId, JSONObject payload) throws JSONException;\n    }\n\n    private class CreateOfferAction implements PnAction{\n        public static final String TRIGGER = \"init\";\n        public void execute(String peerId, JSONObject payload) throws JSONException {\n            Log.d(\"COAction\",\"CreateOfferAction\");\n            PnPeer peer = peers.get(peerId);\n            peer.setDialed(true);\n            peer.setType(PnPeer.TYPE_ANSWER);\n            peer.pc.createOffer(peer, signalingParams.pcConstraints);\n        }\n    }\n\n    private class CreateAnswerAction implements PnAction{\n        public static final String TRIGGER = \"offer\";\n        public void execute(String peerId, JSONObject payload) throws JSONException {\n            Log.d(\"CAAction\",\"CreateAnswerAction\");\n            PnPeer peer = peers.get(peerId);\n            peer.setType(PnPeer.TYPE_OFFER);\n            peer.setStatus(PnPeer.STATUS_CONNECTED);\n            SessionDescription sdp = new SessionDescription(\n                    SessionDescription.Type.fromCanonicalForm(payload.getString(\"type\")),\n                    payload.getString(\"sdp\")\n            );\n            peer.pc.setRemoteDescription(peer, sdp);\n            peer.pc.createAnswer(peer, signalingParams.pcConstraints);\n        }\n    }\n\n    private class SetRemoteSDPAction implements PnAction{\n        public static final String TRIGGER = \"answer\";\n        public void execute(String peerId, JSONObject payload) throws JSONException {\n            Log.d(\"SRSAction\",\"SetRemoteSDPAction\");\n            PnPeer peer = peers.get(peerId);\n            SessionDescription sdp = new SessionDescription(\n                    SessionDescription.Type.fromCanonicalForm(payload.getString(\"type\")),\n                    payload.getString(\"sdp\")\n            );\n            peer.pc.setRemoteDescription(peer, sdp);\n        }\n    }\n\n    private class AddIceCandidateAction implements PnAction{\n        public static final String TRIGGER = \"candidate\";\n        public void execute(String peerId, JSONObject payload) throws JSONException {\n            Log.d(\"AICAction\",\"AddIceCandidateAction\");\n            PeerConnection pc = peers.get(peerId).pc;\n            if (pc.getRemoteDescription() != null) {\n                IceCandidate candidate = new IceCandidate(\n                        payload.getString(\"sdpMid\"),\n                        payload.getInt(\"sdpMLineIndex\"),\n                        payload.getString(\"candidate\")\n                );\n                pc.addIceCandidate(candidate);\n            }\n        }\n    }\n\n    private class PnUserHangupAction implements PnAction{\n        public static final String TRIGGER = PnRTCMessage.JSON_HANGUP;\n        public void execute(String peerId, JSONObject payload) throws JSONException {\n            Log.d(\"PnUserHangup\",\"PnUserHangupAction\");\n            PnPeer peer = peers.get(peerId);\n            peer.hangup();\n            mRtcListener.onPeerConnectionClosed(peer);\n            // Todo: Consider Callback?\n        }\n    }\n\n    private class PnUserMessageAction implements PnAction{\n        public static final String TRIGGER = PnRTCMessage.JSON_USERMSG;\n        public void execute(String peerId, JSONObject payload) throws JSONException {\n            Log.d(\"PnUserMessage\",\"AddIceCandidateAction\");\n            JSONObject msgJson = payload.getJSONObject(PnRTCMessage.JSON_USERMSG);\n            PnPeer peer = peers.get(peerId);\n            mRtcListener.onMessage(peer, msgJson);\n        }\n    }\n\n\n    /**\n     * @param userId Your id. Used to tag the message before publishing it to another user.\n     * @return\n     */\n    public static JSONObject generateHangupPacket(String userId){\n        JSONObject json = new JSONObject();\n        try {\n            JSONObject packet = new JSONObject();\n            packet.put(PnRTCMessage.JSON_HANGUP, true);\n            json.put(PnRTCMessage.JSON_PACKET, packet);\n            json.put(PnRTCMessage.JSON_ID, \"\"); //Todo: session id, unused in js SDK?\n            json.put(PnRTCMessage.JSON_NUMBER, userId);\n        } catch (JSONException e){\n            e.printStackTrace();\n        }\n        return json;\n    }\n\n    /**\n     * Static method to generate the proper JSON for a user message. Use this when you don't have\n     *   a {@link me.kevingleason.pnwebrtc.PnRTCClient} instantiated. Simply send a publish with the\n     *   returned JSONObject to the ID that a user is subscribed to.\n     * @param userId Your UserID, needed to tag the message\n     * @param message The message you with to send some other user\n     * @return JSONObject properly formatted for the PubNub WebRTC API\n     */\n    public static JSONObject generateUserMessage(String userId, JSONObject message){\n        JSONObject json = new JSONObject();\n        try {\n            JSONObject packet = new JSONObject();\n            packet.put(PnRTCMessage.JSON_USERMSG, message);\n            json.put(PnRTCMessage.JSON_PACKET, packet);\n            json.put(PnRTCMessage.JSON_ID, \"\"); //Todo: session id, unused in js SDK?\n            json.put(PnRTCMessage.JSON_NUMBER, userId);\n        } catch (JSONException e){\n            e.printStackTrace();\n        }\n        return json;\n    }\n\n    private class PnRTCReceiver extends Callback {\n\n        @Override\n        public void connectCallback(String channel, Object message) {\n            mRtcListener.onDebug(new PnRTCMessage(((JSONArray) message).toString()));\n            mRtcListener.onConnected(channel);\n        }\n\n        @Override\n        public void successCallback(String channel, Object message) {\n            if (!(message instanceof JSONObject)) return; // Ignore if not valid JSON.\n            JSONObject jsonMessage = (JSONObject) message;\n            mRtcListener.onDebug(new PnRTCMessage(jsonMessage));\n            try {\n                String peerId     = jsonMessage.getString(PnRTCMessage.JSON_NUMBER);\n                JSONObject packet = jsonMessage.getJSONObject(PnRTCMessage.JSON_PACKET);\n                PnPeer peer;\n                if (!peers.containsKey(peerId)){\n                    // Possibly threshold number of allowed users\n                    peer = addPeer(peerId);\n                    peer.pc.addStream(localMediaStream);\n                } else {\n                    peer = peers.get(peerId);\n                }\n                if (peer.getStatus().equals(PnPeer.STATUS_DISCONNECTED)) return; // Do nothing if disconnected.\n                if (packet.has(PnRTCMessage.JSON_USERMSG)) {\n                    actionMap.get(PnUserMessageAction.TRIGGER).execute(peerId,packet);\n                    return;\n                }\n                if (packet.has(PnRTCMessage.JSON_HANGUP)){\n                    actionMap.get(PnUserHangupAction.TRIGGER).execute(peerId,packet);\n                    return;\n                }\n                if (packet.has(PnRTCMessage.JSON_THUMBNAIL)) {\n                    return;   // No handler for thumbnail or hangup yet, will be separate controller callback\n                }\n                if (packet.has(PnRTCMessage.JSON_SDP)) {\n                    if(!peer.received) {\n                        peer.setReceived(true);\n                        mRtcListener.onDebug(new PnRTCMessage(\"SDP - \" + peer.toString()));\n                        // Todo: reveivercb(peer);\n                    }\n                    String type = packet.getString(PnRTCMessage.JSON_TYPE);\n                    actionMap.get(type).execute(peerId, packet);\n                    return;\n                }\n                if (packet.has(PnRTCMessage.JSON_ICE)){\n                    actionMap.get(AddIceCandidateAction.TRIGGER).execute(peerId,packet);\n                    return;\n                }\n            } catch (JSONException e){\n                e.printStackTrace();\n            }\n        }\n\n        @Override\n        public void errorCallback(String channel, PubnubError error) {\n            super.errorCallback(channel, error);\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "pnwebrtc/src/main/java/me/kevingleason/pnwebrtc/PnRTCClient.java",
    "content": "package me.kevingleason.pnwebrtc;\n\n\nimport com.pubnub.api.Pubnub;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\nimport org.webrtc.MediaConstraints;\nimport org.webrtc.MediaStream;\n\nimport java.util.List;\n\n\n/**\n * <h1>Main WebRTC Signaling class, holds all functions to set up {@link org.webrtc.PeerConnection}</h1>\n * <pre>\n * Author:  Kevin Gleason - Boston College '16\n * File:    PnRTCClient.java\n * Date:    7/20/15\n * Use:     PubNub WebRTC Signaling\n * &copy; 2009 - 2015 PubNub, Inc.\n * </pre>\n */\npublic class PnRTCClient {\n    private PnSignalingParams pnSignalingParams;\n    private Pubnub mPubNub;\n    private PnPeerConnectionClient pcClient;\n    private String UUID;\n\n    /**\n     * Minimal constructor. Requires a valid Pub and Sub key. Get your Pub/Sub keys for free at\n     *  https://admin.pubnub.com/#/register and find keys on developer portal.\n     * No UUID provided so a random phone number will be generated with this constructor (XXX-XXXX).\n     * @param pubKey PubNub Pub Key\n     * @param subKey PubNub Sub Key\n     */\n    public PnRTCClient(String pubKey, String subKey) {\n        this.UUID = generateRandomNumber();\n        this.mPubNub  = new Pubnub(pubKey, subKey);\n        this.mPubNub.setUUID(this.UUID);\n        this.pnSignalingParams = PnSignalingParams.defaultInstance();\n        this.pcClient = new PnPeerConnectionClient(this.mPubNub, this.pnSignalingParams, new PnRTCListener() {});\n    }\n\n    /**\n     * Slightly more verbose constructor. Requires a valid Pub and Sub key. Get your Pub/Sub keys for free at\n     *  https://admin.pubnub.com/#/register and find keys on developer portal.\n     * @param pubKey PubNub Pub Key\n     * @param subKey PubNub Sub Key\n     * @param UUID Any UUID to be used as a username\n     */\n    public PnRTCClient(String pubKey, String subKey, String UUID) {\n        this.UUID = UUID;\n        this.mPubNub  = new Pubnub(pubKey, subKey);\n        this.mPubNub.setUUID(this.UUID);\n        this.pnSignalingParams = PnSignalingParams.defaultInstance();\n        this.pcClient = new PnPeerConnectionClient(this.mPubNub, this.pnSignalingParams, new PnRTCListener() {});\n    }\n\n    /**\n     * Return the {@link me.kevingleason.pnwebrtc.PnRTCClient} peer connection constraints.\n     * @return Peer Connection Constrains\n     */\n    public MediaConstraints pcConstraints() {\n        return pnSignalingParams.pcConstraints;\n    }\n\n    /**\n     * Return the {@link me.kevingleason.pnwebrtc.PnRTCClient} video constraints.\n     * @return Video Constrains\n     */\n    public MediaConstraints videoConstraints() {\n        return pnSignalingParams.videoConstraints;\n    }\n\n    /**\n     * Return the {@link me.kevingleason.pnwebrtc.PnRTCClient} audio constraints.\n     * @return Audio Constrains\n     */\n    public MediaConstraints audioConstraints() {\n        return pnSignalingParams.audioConstraints;\n    }\n\n    /**\n     * Return the {@link me.kevingleason.pnwebrtc.PnRTCClient} Pubnub instance.\n     * @return The PnRTCClient's {@link com.pubnub.api.Pubnub} instance\n     */\n    public Pubnub getPubNub(){\n        return this.mPubNub;\n    }\n\n    /**\n     * Return the UUID (username) of the {@link me.kevingleason.pnwebrtc.PnRTCClient}. If not\n     *   provided by the constructor, a random phone number is generated and can be retrieived\n     *   with this method\n     * @return The UUID username of the client\n     */\n    public String getUUID() {\n        return UUID;\n    }\n\n    /**\n     * Set the signaling parameters. This includes {@link org.webrtc.MediaConstraints} for\n     *   {@link org.webrtc.PeerConnection}, Video, and Audio, as well as a list of possible\n     *   {@link org.webrtc.PeerConnection.IceServer} candidates.\n     * @param signalParams Parameters for WebRTC Signaling\n     */\n    public void setSignalParams(PnSignalingParams signalParams){\n        this.pnSignalingParams = signalParams;\n    }\n\n    /**\n     * Need to attach mediaStream before you can connect.\n     * @param mediaStream Not null local media stream\n     */\n    public void attachLocalMediaStream(MediaStream mediaStream){\n        this.pcClient.setLocalMediaStream(mediaStream);\n    }\n\n    /**\n     * Attach custom listener for callbacks!\n     * @param listener The listener which extends PnRTCListener to implement callbacks\n     */\n    public void attachRTCListener(PnRTCListener listener){\n        this.pcClient.setRTCListener(listener);\n    }\n\n    /**\n     * Set the maximum simultaneous connections allowed\n     * @param max Max simultaneous connections\n     */\n    public void setMaxConnections(int max){\n        this.pcClient.MAX_CONNECTIONS = max;\n    }\n\n    /**\n     * Subscribe to a channel using PubNub to listen for calls.\n     * @param channel The channel to listen on, your \"phone number\"\n     */\n    public void listenOn(String channel){\n        this.pcClient.listenOn(channel);\n    }\n\n    /**\n     * Connect with another user by their ID.\n     * @param userId The user to establish a WebRTC connection with\n     */\n    public void connect(String userId){\n        this.pcClient.connect(userId);\n    }\n\n    /**\n     * Close a single peer connection. Send a PubNub hangup signal as well\n     * @param userId User to close a connection with\n     */\n    public void closeConnection(String userId){\n        this.pcClient.closeConnection(userId);\n    }\n\n    /**\n     * Close all peer connections. Send a PubNub hangup signal as well.\n     */\n    public void closeAllConnections(){\n        this.pcClient.closeAllConnections();\n    }\n\n    /**\n     * Send a custom JSONObject user message to a single peer.\n     * @param userId user to send a message to\n     * @param message the JSON message to pass to a peer.\n     */\n    public void transmit(String userId, JSONObject message){\n        JSONObject usrMsgJson = new JSONObject();\n        try {\n            usrMsgJson.put(PnRTCMessage.JSON_USERMSG, message);\n            this.pcClient.transmitMessage(userId, usrMsgJson);\n        } catch (JSONException e){\n            e.printStackTrace();\n        }\n    }\n\n    /**\n     * Send a custom JSONObject user message to all peers.\n     * @param message the JSON message to pass to all peers.\n     */\n    public void transmitAll(JSONObject message){\n        List<PnPeer> peerList = this.pcClient.getPeers();\n        for(PnPeer p : peerList){\n            transmit(p.getId(), message);\n        }\n    }\n\n    private static String generateRandomNumber(){\n        String areaCode = String.valueOf((int)(Math.random()*1000));\n        String digits   = String.valueOf((int)(Math.random()*10000));\n        while (areaCode.length() < 3) areaCode += \"0\";\n        while (digits.length()   < 4) digits   += \"0\";\n        return areaCode + \"-\" + digits;\n    }\n\n    /**\n     * Call this method in Activity.onDestroy() to clost all open connections and clean up\n     *   instance for garbage collection.\n     */\n    public void onDestroy() {\n        this.pcClient.closeAllConnections();\n        this.mPubNub.unsubscribeAll();\n    }\n\n}\n"
  },
  {
    "path": "pnwebrtc/src/main/java/me/kevingleason/pnwebrtc/PnRTCListener.java",
    "content": "package me.kevingleason.pnwebrtc;\n\nimport org.webrtc.MediaStream;\n\n/**\n * <h1>Callback listener for various WebRTC and {@link org.webrtc.PeerConnection.Observer} events.</h1>\n * <pre>\n * Author:  Kevin Gleason - Boston College '16\n * File:    PnRTCListener.java\n * Date:    7/20/15\n * Use:     Callback listener for various WebRTC events\n * &copy; 2009 - 2015 PubNub, Inc.\n * </pre>\n * <h2>About this class:</h2>\n * <p>\n *     Implement this interface to be notified of WebRTC events.\n *     It is an abstract class with default behaviors of doing nothing.\n *     Use a PnRTCListener to implement the various callbacks of your WebRTC application.\n * </p>\n */\npublic abstract class PnRTCListener{\n    public void onCallReady(String callId){} // TODO: Maybe not needed?\n\n    /**\n     * Called in {@link com.pubnub.api.Pubnub} object's subscribe connected callback.\n     * Means that you are ready to receive calls.\n     * @param userId The channel you are subscribed to, the userId you may be called on.\n     */\n    public void onConnected(String userId){}\n\n    /**\n     * Peer status changed. {@link PnPeer} status changed, can be\n     * CONNECTING, CONNECTED, or DISCONNECTED.\n     * @param peer The peer object, can use to check peer.getStatus()\n     */\n    public void onPeerStatusChanged(PnPeer peer){}\n\n    /**TODO: Is this different than onPeerStatusChanged == DISCONNECTED?\n     * Called when a hangup occurs.\n     * @param peer The peer who was hung up on, or who hung up on you\n     */\n    public void onPeerConnectionClosed(PnPeer peer){}\n\n    /**\n     * Called in {@link PnPeerConnectionClient} when setLocalStream\n     * is invoked.\n     * @param localStream The users local stream from Android's front or back camera.\n     */\n    public void onLocalStream(MediaStream localStream){}\n\n    /**\n     * Called when a remote stream is added in the {@link org.webrtc.PeerConnection.Observer}\n     * in {@link PnPeer}.\n     * @param remoteStream The remote stream that was added\n     * @param peer The peer that added the remote stream\n     *             Todo: Maybe not the right peer?\n     */\n    public void onAddRemoteStream(MediaStream remoteStream, PnPeer peer){}\n\n    /**\n     * Called in the {@link org.webrtc.PeerConnection.Observer} implemented\n     * by {@link PnPeer}.\n     * @param remoteStream The stream that was removed by your peer\n     * @param peer The peer that removed the stream.\n     */\n    public void onRemoveRemoteStream(MediaStream remoteStream, PnPeer peer){}\n\n    /**\n     * Called when a user message is send via {@link com.pubnub.api.Pubnub} object.\n     * @param peer The peer who sent the message\n     * @param message The {@link org.json.JSONObject} message sent by the user.\n     */\n    public void onMessage(PnPeer peer, Object message){}\n\n    /**\n     * A helpful debugging callback for testing and developing your app.\n     * @param message The {@link PnRTCMessage} debug message.\n     */\n    public void onDebug(PnRTCMessage message){}\n}"
  },
  {
    "path": "pnwebrtc/src/main/java/me/kevingleason/pnwebrtc/PnRTCMessage.java",
    "content": "package me.kevingleason.pnwebrtc;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\n/**\n * <h1>Used for debug messages and static definitions of JSON keys</h1>\n * <pre>\n * Author:  Kevin Gleason - Boston College '16\n * File:    PnRTCMessage.java\n * Date:    7/20/15\n * Use:     Debug messages and JSON key definitions\n * &copy; 2009 - 2015 PubNub, Inc.\n * </pre>\n */\npublic class PnRTCMessage extends JSONObject {\n    public static final String JSON_TYPE       = \"type\";\n    public static final String JSON_PACKET     = \"packet\";\n    public static final String JSON_ID         = \"id\";\n    public static final String JSON_NUMBER     = \"number\"; // Todo: Change to more accurate name.\n    public static final String JSON_MESSAGE    = \"message\";\n    public static final String JSON_USERMSG    = \"usermsg\";\n    public static final String JSON_HANGUP     = \"hangup\";\n    public static final String JSON_THUMBNAIL  = \"thumbnail\";\n    public static final String JSON_SDP        = \"sdp\";\n    public static final String JSON_ICE        = \"candidate\"; // Identify ICE\n\n    private String message;\n    private JSONObject json;\n\n    public PnRTCMessage(String message){\n        super();\n        try {\n            this.put(JSON_MESSAGE, message);\n        } catch (JSONException e){\n            throw new RuntimeException(\"Invalid JSON Payload\");\n        }\n        this.message = message;\n        this.json = this;\n    }\n\n    public PnRTCMessage(JSONObject json){\n        super();\n        try {\n            if (json==null){\n                json = new JSONObject(\"{error:1}\");\n            }\n            this.put(JSON_MESSAGE, json);\n        } catch (JSONException e){\n            throw new RuntimeException(\"Invalid JSON Payload\");\n        }\n        this.message = json.toString();\n        this.json = this;\n    }\n\n    public String getMessage() {\n        return message;\n    }\n\n    public JSONObject getJSON(){\n        return this.json;\n    }\n}\n"
  },
  {
    "path": "pnwebrtc/src/main/java/me/kevingleason/pnwebrtc/PnSignalingParams.java",
    "content": "package me.kevingleason.pnwebrtc;\n\nimport org.webrtc.MediaConstraints;\nimport org.webrtc.PeerConnection;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * <h1>Define {@link org.webrtc.MediaConstraints} and {@link org.webrtc.PeerConnection.IceServer} for WebRTC PeerConnections</h1>\n * <pre>\n * Author:  Kevin Gleason - Boston College '16\n * File:    PnSignalingParams.java\n * Date:    7/20/15\n * Use:     Hold the signaling parameters of a WebRTC PeerConnection\n * &copy; 2009 - 2015 PubNub, Inc.\n * </pre>\n * <p>IceServers allow Trickling, so they are not final.</p>\n *\n */\npublic class PnSignalingParams {\n    public List<PeerConnection.IceServer> iceServers;\n    public final MediaConstraints pcConstraints;\n    public final MediaConstraints videoConstraints;\n    public final MediaConstraints audioConstraints;\n\n    public PnSignalingParams(\n            List<PeerConnection.IceServer> iceServers,\n            MediaConstraints pcConstraints,\n            MediaConstraints videoConstraints,\n            MediaConstraints audioConstraints) {\n        this.iceServers       = (iceServers==null)       ? defaultIceServers()       : iceServers;\n        this.pcConstraints    = (pcConstraints==null)    ? defaultPcConstraints()    : pcConstraints;\n        this.videoConstraints = (videoConstraints==null) ? defaultVideoConstraints() : videoConstraints;\n        this.audioConstraints = (audioConstraints==null) ? defaultAudioConstraints() : audioConstraints;\n    }\n\n    /**\n     * Default Ice Servers, but specified parameters.\n     * @param pcConstraints\n     * @param videoConstraints\n     * @param audioConstraints\n     */\n    public PnSignalingParams(\n            MediaConstraints pcConstraints,\n            MediaConstraints videoConstraints,\n            MediaConstraints audioConstraints) {\n        this.iceServers       = PnSignalingParams.defaultIceServers();\n        this.pcConstraints    = (pcConstraints==null)    ? defaultPcConstraints()    : pcConstraints;\n        this.videoConstraints = (videoConstraints==null) ? defaultVideoConstraints() : videoConstraints;\n        this.audioConstraints = (audioConstraints==null) ? defaultAudioConstraints() : audioConstraints;\n    }\n\n    /**\n     * Default media params, but specified Ice Servers\n     * @param iceServers\n     */\n    public PnSignalingParams(List<PeerConnection.IceServer> iceServers) {\n        this.iceServers       = iceServers; //defaultIceServers();\n        this.pcConstraints    = defaultPcConstraints();\n        this.videoConstraints = defaultVideoConstraints();\n        this.audioConstraints = defaultAudioConstraints();\n//        addIceServers(iceServers);\n    }\n\n    /**\n     * Default media params and ICE servers.\n     */\n    public PnSignalingParams() {\n        this.iceServers       = defaultIceServers();\n        this.pcConstraints    = defaultPcConstraints();\n        this.videoConstraints = defaultVideoConstraints();\n        this.audioConstraints = defaultAudioConstraints();\n    }\n\n    /**\n     * The default parameters for media constraints. Might have to tweak in future.\n     * @return default parameters\n     */\n    public static PnSignalingParams defaultInstance() {\n        MediaConstraints pcConstraints    = PnSignalingParams.defaultPcConstraints();\n        MediaConstraints videoConstraints = PnSignalingParams.defaultVideoConstraints();\n        MediaConstraints audioConstraints = PnSignalingParams.defaultAudioConstraints();\n        List<PeerConnection.IceServer> iceServers = PnSignalingParams.defaultIceServers();\n        return new PnSignalingParams(iceServers, pcConstraints, videoConstraints, audioConstraints);\n    }\n\n    private static MediaConstraints defaultPcConstraints(){\n        MediaConstraints pcConstraints = new MediaConstraints();\n        pcConstraints.optional.add(new MediaConstraints.KeyValuePair(\"DtlsSrtpKeyAgreement\", \"true\"));\n        pcConstraints.mandatory.add(new MediaConstraints.KeyValuePair(\"OfferToReceiveAudio\", \"true\"));\n        pcConstraints.mandatory.add(new MediaConstraints.KeyValuePair(\"OfferToReceiveVideo\", \"true\"));\n        return pcConstraints;\n    }\n\n    private static MediaConstraints defaultVideoConstraints(){\n        MediaConstraints videoConstraints = new MediaConstraints();\n        videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair(\"maxWidth\",\"1280\"));\n        videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair(\"maxHeight\",\"720\"));\n        videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair(\"minWidth\", \"640\"));\n        videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair(\"minHeight\",\"480\"));\n        return videoConstraints;\n    }\n\n    private static MediaConstraints defaultAudioConstraints(){\n        MediaConstraints audioConstraints = new MediaConstraints();\n        return audioConstraints;\n    }\n\n    public static List<PeerConnection.IceServer> defaultIceServers(){\n        List<PeerConnection.IceServer> iceServers = new ArrayList<PeerConnection.IceServer>(25);\n        iceServers.add(new PeerConnection.IceServer(\"stun:stun.l.google.com:19302\"));\n        iceServers.add(new PeerConnection.IceServer(\"stun:stun.services.mozilla.com\"));\n        iceServers.add(new PeerConnection.IceServer(\"turn:turn.bistri.com:80\", \"homeo\", \"homeo\"));\n        iceServers.add(new PeerConnection.IceServer(\"turn:turn.anyfirewall.com:443?transport=tcp\", \"webrtc\", \"webrtc\"));\n\n        // Extra Defaults - 19 STUN servers + 4 initial = 23 severs (+2 padding) = Array cap 25\n        iceServers.add(new PeerConnection.IceServer(\"stun:stun1.l.google.com:19302\"));\n        iceServers.add(new PeerConnection.IceServer(\"stun:stun2.l.google.com:19302\"));\n        iceServers.add(new PeerConnection.IceServer(\"stun:stun3.l.google.com:19302\"));\n        iceServers.add(new PeerConnection.IceServer(\"stun:stun4.l.google.com:19302\"));\n        iceServers.add(new PeerConnection.IceServer(\"stun:23.21.150.121\"));\n        iceServers.add(new PeerConnection.IceServer(\"stun:stun01.sipphone.com\"));\n        iceServers.add(new PeerConnection.IceServer(\"stun:stun.ekiga.net\"));\n        iceServers.add(new PeerConnection.IceServer(\"stun:stun.fwdnet.net\"));\n        iceServers.add(new PeerConnection.IceServer(\"stun:stun.ideasip.com\"));\n        iceServers.add(new PeerConnection.IceServer(\"stun:stun.iptel.org\"));\n        iceServers.add(new PeerConnection.IceServer(\"stun:stun.rixtelecom.se\"));\n        iceServers.add(new PeerConnection.IceServer(\"stun:stun.schlund.de\"));\n        iceServers.add(new PeerConnection.IceServer(\"stun:stunserver.org\"));\n        iceServers.add(new PeerConnection.IceServer(\"stun:stun.softjoys.com\"));\n        iceServers.add(new PeerConnection.IceServer(\"stun:stun.voiparound.com\"));\n        iceServers.add(new PeerConnection.IceServer(\"stun:stun.voipbuster.com\"));\n        iceServers.add(new PeerConnection.IceServer(\"stun:stun.voipstunt.com\"));\n        iceServers.add(new PeerConnection.IceServer(\"stun:stun.voxgratia.org\"));\n        iceServers.add(new PeerConnection.IceServer(\"stun:stun.xten.com\"));\n\n        return iceServers;\n    }\n\n    /**\n     * Append default servers to the end of given list and set as iceServers instance variable\n     * @param iceServers List of iceServers\n     */\n    public void addIceServers(List<PeerConnection.IceServer> iceServers){\n        if(this.iceServers!=null) {\n            iceServers.addAll(this.iceServers);\n        }\n        this.iceServers = iceServers;\n    }\n\n    /**\n     * Instantiate iceServers if they are not already, and add Ice Server to beginning of list.\n     * @param iceServers Ice Server to add\n     */\n    public void addIceServers(PeerConnection.IceServer iceServers){\n        if (this.iceServers == null){\n            this.iceServers = new ArrayList<PeerConnection.IceServer>();\n        }\n        this.iceServers.add(0, iceServers);\n    }\n}\n\n"
  },
  {
    "path": "pnwebrtc/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">PnWebRTC</string>\n</resources>\n"
  },
  {
    "path": "settings.gradle",
    "content": "include ':app', ':pnwebrtc'\n"
  }
]