diff --git a/app/build.gradle b/app/build.gradle
index bef0b57..f2090bd 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -41,6 +41,8 @@ dependencies {
implementation 'androidx.cardview:cardview:1.0.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.google.code.gson:gson:2.8.6'
+
+ implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.7.2'
}
repositories {
mavenCentral()
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index aa60a2f..b5ca93e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -7,6 +7,7 @@
android:required="true" />
+
, List>>()
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
- new Tuple3<>(new Producer, List>>() {
- @Override
- public Tuple2, List> get() {
- return new Tuple2<>(
- CardActivity.this.ProgramakerApi.fetchConnectedBridges(),
- CardActivity.this.ProgramakerApi.fetchCustomBlocks());
- }
- }, new Consumer,List>>() {
- @Override
- public void apply(Tuple2,List> result) {
- partsHolder.addCustomBlocks(result.item1, result.item2);
- Log.d("CARDActivity", "custom blocks: " + result.toString());
- }
- }, new Consumer () {
- @Override
- public void apply(Throwable exception) {
- Log.e("CARDActivity", "error retrieving custom blocks: " + exception.toString());
- }
- }));
-
+ new Tuple3<>(() ->
+ new Tuple2<>(
+ CardActivity.this.ProgramakerApi.fetchConnectedBridges(),
+ CardActivity.this.ProgramakerApi.fetchCustomBlocks()),
+ result -> {
+ partsHolder.addCustomBlocks(result.item1, result.item2);
+ Log.d("CARDActivity", "custom blocks: " + result.toString());
+ },
+ exception -> Log.e("CARDActivity", "error retrieving custom blocks: " + exception.toString())));
// Hide action bar
ActionBar actionBar = getSupportActionBar();
diff --git a/app/src/main/java/com/codigoparallevar/minicards/ConfigManager.java b/app/src/main/java/com/codigoparallevar/minicards/ConfigManager.java
index c30d300..447f22a 100644
--- a/app/src/main/java/com/codigoparallevar/minicards/ConfigManager.java
+++ b/app/src/main/java/com/codigoparallevar/minicards/ConfigManager.java
@@ -2,14 +2,14 @@ package com.codigoparallevar.minicards;
import android.content.Context;
import android.content.SharedPreferences;
-import android.view.View;
import static android.content.Context.MODE_PRIVATE;
public class ConfigManager {
private final Context context;
- private static final String TOKEN_KEY = "PROGRAMAKER_API_TOKEN";
private static final String PREFERENCES_NAME = "MINICARDS_PREFERENCES";
+ private static final String TOKEN_KEY = "PROGRAMAKER_API_TOKEN";
+ private static final String BRIDGE_ID_KEY = "PROGRAMAKER_BRIDGE_ID";
public ConfigManager(Context ctx) {
this.context = ctx;
@@ -20,16 +20,33 @@ public class ConfigManager {
SharedPreferences.Editor edit = preferences.edit();
edit.putString(TOKEN_KEY, token);
- edit.commit();
+ edit.apply();
}
public String getToken() {
SharedPreferences preferences = this.context.getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE);
- if (!preferences.contains(TOKEN_KEY)) {
- return null;
- }
- else {
- return preferences.getString(TOKEN_KEY, null);
+ return preferences.getString(TOKEN_KEY, null);
+ }
+
+ public void removeBridgeId() {
+ SharedPreferences preferences = this.context.getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE);
+ SharedPreferences.Editor edit = preferences.edit();
+
+ if (preferences.contains(BRIDGE_ID_KEY)) {
+ edit.remove(BRIDGE_ID_KEY).apply();
}
}
+
+ public void setBridgeId(String bridgeId) {
+ SharedPreferences preferences = this.context.getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE);
+ SharedPreferences.Editor edit = preferences.edit();
+
+ edit.putString(BRIDGE_ID_KEY, bridgeId);
+ edit.commit();
+ }
+
+ public String getBridgeId() {
+ SharedPreferences preferences = this.context.getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE);
+ return preferences.getString(BRIDGE_ID_KEY, null);
+ }
}
diff --git a/app/src/main/java/com/codigoparallevar/minicards/DeckPreviewActivity.java b/app/src/main/java/com/codigoparallevar/minicards/DeckPreviewActivity.java
index de62056..194924d 100644
--- a/app/src/main/java/com/codigoparallevar/minicards/DeckPreviewActivity.java
+++ b/app/src/main/java/com/codigoparallevar/minicards/DeckPreviewActivity.java
@@ -4,14 +4,6 @@ import android.app.Dialog;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.os.Bundle;
-
-import com.codigoparallevar.minicards.types.functional.Consumer;
-import com.codigoparallevar.minicards.types.functional.Tuple2;
-import com.codigoparallevar.minicards.types.functional.Tuple3;
-import com.google.android.material.floatingactionbutton.FloatingActionButton;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.widget.Toolbar;
-
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
@@ -23,19 +15,32 @@ import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.widget.Toolbar;
+
+import com.codigoparallevar.minicards.bridge.ProgramakerAndroidBridge;
+import com.codigoparallevar.minicards.types.functional.Consumer;
+import com.codigoparallevar.minicards.types.functional.Tuple2;
+import com.codigoparallevar.minicards.types.functional.Tuple3;
+import com.codigoparallevar.minicards.ui_helpers.DoAsync;
+import com.codigoparallevar.minicards.ui_helpers.GetAsync;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import com.programaker.api.ProgramakerApi;
+
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Vector;
-import com.programaker.api.ProgramakerApi;
-
public class DeckPreviewActivity extends ReloadableAppCompatActivity {
public static final String INTENT = "com.codigoparallevar.minicards.DECK";
+ private static final String LogTag = "DeckPreview";
+
private ListView listView;
private CardPreviewArrayAdapter cardArrayAdapter;
private ProgramakerApi ProgramakerApi;
private ConfigManager Config;
+ private ProgramakerAndroidBridge bridge = null;
protected void openLoginDialog(View view) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
@@ -92,22 +97,20 @@ public class DeckPreviewActivity extends ReloadableAppCompatActivity {
new Tuple3<>(
loginUsernameText.getText().toString(),
loginPasswordText.getText().toString(),
- new Consumer() {
- public void apply(String token) {
- if (token == null) {
- messageLabel.setText(R.string.invalid_user_pass);
- } else {
- DeckPreviewActivity.this.Config.setToken(token);
- DeckPreviewActivity.this.ProgramakerApi.setToken(token);
- final Button loginToProgramakerButton = findViewById(R.id.login_in_programaker_button);
+ token -> {
+ if (token == null) {
+ messageLabel.setText(R.string.invalid_user_pass);
+ } else {
+ DeckPreviewActivity.this.Config.setToken(token);
+ DeckPreviewActivity.this.ProgramakerApi.setToken(token);
+ final Button loginToProgramakerButton = findViewById(R.id.login_in_programaker_button);
- loginToProgramakerButton.setVisibility(View.GONE);
- // Re-check... just in case
- new CheckNeededLoginButton().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
- new Tuple2<>(DeckPreviewActivity.this.ProgramakerApi,
- loginToProgramakerButton));
- dialog.cancel();
- }
+ loginToProgramakerButton.setVisibility(View.GONE);
+ // Re-check... just in case
+ checkNeededLoginButton(
+ DeckPreviewActivity.this.ProgramakerApi,
+ loginToProgramakerButton);
+ dialog.cancel();
}
}));
}
@@ -119,15 +122,14 @@ public class DeckPreviewActivity extends ReloadableAppCompatActivity {
@Override
protected Tuple2> doInBackground(Tuple3>... tuples) {
ProgramakerApi api = new ProgramakerApi();
- boolean logged = false;
String token = null;
try {
token = api.login(tuples[0]._x, tuples[0]._y);
}
catch (Exception e) {
- Log.e("Login to PrograMaker", e.toString());
+ Log.e("Login to PrograMaker", e.toString(), e);
}
- return new Tuple2>(token, tuples[0]._z);
+ return new Tuple2<>(token, tuples[0]._z);
}
@Override
@@ -136,20 +138,6 @@ public class DeckPreviewActivity extends ReloadableAppCompatActivity {
}
}
- static class CheckNeededLoginButton extends AsyncTask, Void, Tuple2>{
- @Override
- protected Tuple2 doInBackground(Tuple2... tuples) {
- return new Tuple2<>(tuples[0].item1.check(), tuples[0].item2);
- }
-
- @Override
- protected void onPostExecute(Tuple2 result) {
- if (!result.item1) {
- result.item2.setVisibility(View.VISIBLE);
- }
- }
- }
-
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -183,14 +171,54 @@ public class DeckPreviewActivity extends ReloadableAppCompatActivity {
loginButton.setVisibility(View.VISIBLE);
}
else {
+
this.ProgramakerApi.setToken(token);
loginButton.setVisibility(View.GONE);
// Double check that is not needed, token might have been deleted
- new CheckNeededLoginButton().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
- new Tuple2<>(this.ProgramakerApi, loginButton));
+ checkNeededLoginButton(this.ProgramakerApi, loginButton);
}
}
+ private void checkNeededLoginButton(ProgramakerApi api, Button loginButton) {
+ new GetAsync().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
+ new Tuple3<>(
+ () -> api.check(),
+ result -> {
+ if (!result) {
+ loginButton.setVisibility(View.VISIBLE);
+ DeckPreviewActivity.this.Config.removeBridgeId();
+ } else {
+ String bridgeId = DeckPreviewActivity.this.Config.getBridgeId();
+ if (bridgeId == null) {
+ new GetAsync().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
+ new Tuple3<>(
+ () -> api.createBridge(ProgramakerAndroidBridge.GetBridgeName(this)),
+ newBridgeId -> {
+ DeckPreviewActivity.this.Config.setBridgeId(newBridgeId);
+ this.bridge = ProgramakerAndroidBridge.configure(this, api.getUserId(), newBridgeId);
+ new DoAsync().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
+ new Tuple2<>(
+ () -> this.bridge.start(),
+ ex -> Log.e(LogTag, "Error on bridge: " + ex, ex)
+ ));
+ },
+ ex -> Log.e(LogTag, "Error creating bridge: " + ex, ex)
+ ));
+ }
+ else {
+ this.bridge = ProgramakerAndroidBridge.configure(this, api.getUserId(), bridgeId);
+ new DoAsync().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
+ new Tuple2<>(
+ () -> this.bridge.start(),
+ ex -> Log.e(LogTag, "Error on bridge: " + ex, ex)
+ ));
+ }
+ }
+ },
+ ex -> Log.e(LogTag, "Error checking API:" + ex, ex)
+ ));
+ }
+
@Override
protected void onResume() {
super.onResume();
diff --git a/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerAndroidBridge.java b/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerAndroidBridge.java
new file mode 100644
index 0000000..3ef3705
--- /dev/null
+++ b/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerAndroidBridge.java
@@ -0,0 +1,52 @@
+package com.codigoparallevar.minicards.bridge;
+
+import android.content.Context;
+import android.provider.Settings;
+
+import com.programaker.bridge.ProgramakerBridge;
+import com.programaker.bridge.ProgramakerBridgeConfiguration;
+
+import java.util.Collections;
+
+public class ProgramakerAndroidBridge {
+ private static final String LogTag = "PM Android Bridge";
+ private ProgramakerBridge bridgeRunner = null;
+
+ // Static
+ public static ProgramakerAndroidBridge configure(Context ctx, String userId, String bridgeId) {
+ return new ProgramakerAndroidBridge(ctx, userId, bridgeId);
+ }
+
+ public static String GetBridgeName(Context ctx) {
+ String deviceName = Settings.Secure.getString(ctx.getContentResolver(), "bluetooth_name");
+ String serviceName = "MiniCards on " + deviceName;
+ return serviceName;
+ }
+
+// public static ProgramakerBridgeConfiguration GetConfiguration(Context ctx) {
+// List blocks = new LinkedList<>();
+// return new ProgramakerBridgeConfiguration(serviceName, blocks);
+// }
+ // Builder
+
+ private final Context ctx;
+ private final String userId;
+ private final String bridgeId;
+
+ private ProgramakerAndroidBridge(Context ctx, String userId, String bridgeId) {
+ this.ctx = ctx;
+ this.userId = userId;
+ this.bridgeId = bridgeId;
+ }
+
+ public void start() {
+ ProgramakerBridgeConfiguration configuration = new ProgramakerBridgeConfiguration(
+ ProgramakerAndroidBridge.GetBridgeName(this.ctx),
+ Collections.emptyList()
+ );
+
+ this.bridgeRunner = new ProgramakerBridge(this.bridgeId, this.userId, configuration);
+ this.bridgeRunner.run();
+ }
+
+}
diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/ProgramakerCustomBlockPart.java b/app/src/main/java/com/codigoparallevar/minicards/parts/ProgramakerCustomBlockPart.java
index 0ffff73..92867e4 100644
--- a/app/src/main/java/com/codigoparallevar/minicards/parts/ProgramakerCustomBlockPart.java
+++ b/app/src/main/java/com/codigoparallevar/minicards/parts/ProgramakerCustomBlockPart.java
@@ -354,15 +354,15 @@ public class ProgramakerCustomBlockPart implements Part {
ProgramakerCustomBlockPart.this.runBlockOperation();
ProgramakerCustomBlockPart.this.freeBlock(token);
- }, param -> {
+ }, ex -> {
Log.e(LogTag, "Error executing function=" + this._block.getFunction_name()
- + "; Error=" + param);
+ + "; Error=" + ex, ex);
ProgramakerCustomBlockPart.this.freeBlock(token);
}));
}
catch (Exception ex) {
Log.e(LogTag, "Error executing function=" + this._block.getFunction_name()
- + "; Error=" + ex);
+ + "; Error=" + ex, ex);
this.freeBlock(token);
}
} else {
@@ -444,7 +444,7 @@ public class ProgramakerCustomBlockPart implements Part {
index = Integer.parseInt(chunks[1]);
}
catch (NumberFormatException ex) {
- Log.e(LogTag, "Error parsing connector id="+inputConnectorId);
+ Log.e(LogTag, "Error parsing connector id="+inputConnectorId, ex);
}
if (index != null && index < inputConnectors.size()) {
diff --git a/app/src/main/java/com/programaker/api/ProgramakerApi.kt b/app/src/main/java/com/programaker/api/ProgramakerApi.kt
index aad148e..b6da424 100644
--- a/app/src/main/java/com/programaker/api/ProgramakerApi.kt
+++ b/app/src/main/java/com/programaker/api/ProgramakerApi.kt
@@ -107,7 +107,7 @@ class ProgramakerApi(private val ApiRoot: String="https://programaker.com/api")
throw ProgramakerProtocolException()
}
catch (ex: Exception) {
- Log.e(LogTag, "Unexpected exception: " + ex)
+ Log.e(LogTag, "Unexpected exception: " + ex, ex)
throw ProgramakerProtocolException()
}
return result.result
@@ -137,6 +137,7 @@ class ProgramakerApi(private val ApiRoot: String="https://programaker.com/api")
}
return result.result
}
+
fun callBlock(block: ProgramakerCustomBlock, arguments: List): ProgramakerFunctionCallResult {
val conn = URL(getBlockUrl(block)).openConnection() as HttpURLConnection
conn.setRequestProperty("Content-Type", "application/json")
@@ -159,6 +160,28 @@ class ProgramakerApi(private val ApiRoot: String="https://programaker.com/api")
return result
}
+ fun createBridge(name: String): String {
+ val conn = URL(getCreateBridgeUrl()).openConnection() as HttpURLConnection
+ conn.setRequestProperty("Content-Type", "application/json")
+ addAuthHeader(conn)
+
+ conn.requestMethod = "POST";
+ conn.doOutput = true;
+
+ val postData = JSONObject(hashMapOf(
+ "name" to name
+ ) as Map<*, *>)
+
+ val wr = DataOutputStream(conn.outputStream)
+ wr.writeBytes(postData.toString());
+ wr.flush();
+ wr.close();
+
+ val result: ProgramakerCreateBridgeResult
+ result = parseJson(conn.inputStream, ProgramakerCreateBridgeResult::class.java)
+ return result.getBridgeId()
+ }
+
// Initialization
init {
// Disable connection reuse if necessary
@@ -192,6 +215,16 @@ class ProgramakerApi(private val ApiRoot: String="https://programaker.com/api")
return "$ApiRoot/v0/users/id/$userId/bridges/id/${block.bridge_id}/functions/${block.function_name}"
}
+ private fun getCreateBridgeUrl(): String {
+ this.withUserName()
+ return "$ApiRoot/v0/users/$userName/bridges"
+ }
+
+ fun getUserId(): String? {
+ this.withUserId()
+ return userId;
+ }
+
private fun withUserName() {
if (userName == null) {
if (token == null) {
@@ -240,6 +273,6 @@ private fun FileNotFoundException.logError(tag: String) {
class JsonParseException(private val content: String, private val cls: Type) : Exception() {
fun logError(tag: String) {
- Log.e(tag, "Cannot JSON parse: ${this.content} as ${this.cls}")
+ Log.e(tag, "Cannot JSON parse: ${this.content} as ${this.cls}", this)
}
}
diff --git a/app/src/main/java/com/programaker/api/data/api_results/ProgramakerCreateBridgeResult.kt b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerCreateBridgeResult.kt
new file mode 100644
index 0000000..4d96e44
--- /dev/null
+++ b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerCreateBridgeResult.kt
@@ -0,0 +1,17 @@
+package com.programaker.api.data.api_results
+
+class ProgramakerCreateBridgeResult (
+ val control_url: String
+) {
+ fun getBridgeId(): String {
+ val url = control_url.trim('/');
+ if (url.endsWith("communication")) {
+ val no_communication = url.substring(0, url.length - "communication".length);
+ val chunks = no_communication.trim('/').split("/");
+ return chunks[chunks.size - 1]
+ }
+ else {
+ throw IllegalArgumentException("Unknown control_url format")
+ }
+ }
+}
diff --git a/app/src/main/java/com/programaker/api/exceptions/ProgramakerProtocolException.kt b/app/src/main/java/com/programaker/api/exceptions/ProgramakerProtocolException.kt
index de1a765..6e4f78b 100644
--- a/app/src/main/java/com/programaker/api/exceptions/ProgramakerProtocolException.kt
+++ b/app/src/main/java/com/programaker/api/exceptions/ProgramakerProtocolException.kt
@@ -1,5 +1,6 @@
package com.programaker.api.exceptions
-class ProgramakerProtocolException : Throwable() {
+import java.lang.Exception
+class ProgramakerProtocolException() : Exception() {
}
diff --git a/app/src/main/java/com/programaker/bridge/ProgramakerBridge.kt b/app/src/main/java/com/programaker/bridge/ProgramakerBridge.kt
new file mode 100644
index 0000000..b182831
--- /dev/null
+++ b/app/src/main/java/com/programaker/bridge/ProgramakerBridge.kt
@@ -0,0 +1,59 @@
+package com.programaker.bridge
+
+import android.util.Log
+import okhttp3.*
+import okio.ByteString
+import java.util.concurrent.TimeUnit
+
+
+class ProgramakerBridge(
+ val bridge_id: String,
+ val user_id: String,
+ val config: ProgramakerBridgeConfiguration
+) : WebSocketListener() {
+ private val PING_PERIOD_MILLIS: Long = 15000
+ private val PING_TIMEOUT_MILLIS: Long = 15000
+ private val apiRoot: String = "wss://programaker.com/api"
+ private val LogTag = "ProgramakerBridge"
+
+ fun run() {
+ val client: OkHttpClient = OkHttpClient.Builder()
+ .pingInterval(PING_PERIOD_MILLIS, TimeUnit.MILLISECONDS)
+ .readTimeout(0, TimeUnit.MILLISECONDS)
+ .build()
+
+ val request: Request = Request.Builder()
+ .url(getBridgeControlUrl())
+ .build()
+ client.newWebSocket(request, this)
+
+ // Trigger shutdown of the dispatcher's executor so this process can exit cleanly.
+ client.dispatcher.executorService.shutdown()
+ }
+
+
+ override fun onOpen(webSocket: WebSocket, response: Response) {
+ webSocket.send(config.serialize())
+ }
+
+ override fun onMessage(webSocket: WebSocket, text: String) {
+ println("MESSAGE: $text")
+ }
+
+ override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
+ println("MESSAGE: " + bytes.hex())
+ }
+
+ override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
+ webSocket.close(1000, null)
+ Log.i(LogTag, "Closing bridge socket {code=$code, reason=$reason}")
+ }
+
+ override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
+ Log.e(LogTag, t.toString(), t)
+ }
+
+ private fun getBridgeControlUrl(): String {
+ return "$apiRoot/v0/users/id/$user_id/bridges/id/$bridge_id/communication"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/programaker/bridge/ProgramakerBridgeConfiguration.kt b/app/src/main/java/com/programaker/bridge/ProgramakerBridgeConfiguration.kt
new file mode 100644
index 0000000..bd2dd06
--- /dev/null
+++ b/app/src/main/java/com/programaker/bridge/ProgramakerBridgeConfiguration.kt
@@ -0,0 +1,35 @@
+package com.programaker.bridge
+
+import org.json.JSONObject
+
+class ProgramakerBridgeConfiguration(
+ val service_name: String,
+ val blocks: List
+ // val is_public: boolean, // No reason for this use case
+ // val registration: ???, // No reason for this use case
+ // val allow_multiple_connections: boolean // No reason for this use case
+ // val icon: ???, // Not supported yet // TODO: Add support
+) {
+ fun serialize(): String {
+ var serializedBlocks = listOf()
+ if (blocks != null) {
+ serializedBlocks = blocks.map { it.serialize() }
+ }
+
+ val config = JSONObject(hashMapOf(
+ "service_name" to service_name,
+ "is_public" to false,
+ "registration" to null,
+ "allow_multiple_connections" to null,
+ "icon" to null,
+ "blocks" to serializedBlocks
+ ) as Map<*, *>)
+
+ val wrapper = JSONObject(hashMapOf(
+ "type" to "CONFIGURATION",
+ "value" to config
+ ) as Map<*, *>)
+
+ return wrapper.toString()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/programaker/bridge/ProgramakerBridgeConfigurationBlock.kt b/app/src/main/java/com/programaker/bridge/ProgramakerBridgeConfigurationBlock.kt
new file mode 100644
index 0000000..83a909a
--- /dev/null
+++ b/app/src/main/java/com/programaker/bridge/ProgramakerBridgeConfigurationBlock.kt
@@ -0,0 +1,11 @@
+package com.programaker.bridge
+
+import org.json.JSONObject
+
+class ProgramakerBridgeConfigurationBlock {
+ fun serialize() : JSONObject {
+ val obj = JSONObject();
+ return obj;
+ }
+
+}
diff --git a/build.gradle b/build.gradle
index 3c8f96b..d301845 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,6 +2,7 @@
buildscript {
ext.kotlin_version = '1.3.61'
+ ext.ktor_version = '1.3.0'
repositories {
google()