diff --git a/app/src/main/java/com/codigoparallevar/minicards/ConfigManager.java b/app/src/main/java/com/codigoparallevar/minicards/ConfigManager.java index 447f22a..825dc5d 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/ConfigManager.java +++ b/app/src/main/java/com/codigoparallevar/minicards/ConfigManager.java @@ -10,6 +10,7 @@ public class ConfigManager { 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"; + private static final String BRIDGE_CONNECTION_ID_KEY = "PROGRAMAKER_BRIDGE_CONNECTION_ID"; public ConfigManager(Context ctx) { this.context = ctx; @@ -49,4 +50,17 @@ public class ConfigManager { SharedPreferences preferences = this.context.getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE); return preferences.getString(BRIDGE_ID_KEY, null); } + + public void setBridgeConnectionId(String connectionId) { + SharedPreferences preferences = this.context.getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE); + SharedPreferences.Editor edit = preferences.edit(); + + edit.putString(BRIDGE_CONNECTION_ID_KEY, connectionId); + edit.commit(); + } + + public String getBridgeConnectionId() { + SharedPreferences preferences = this.context.getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE); + return preferences.getString(BRIDGE_CONNECTION_ID_KEY, null); + } } diff --git a/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerAndroidBridge.java b/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerAndroidBridge.java index 90d8ccd..048832e 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerAndroidBridge.java +++ b/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerAndroidBridge.java @@ -3,6 +3,7 @@ package com.codigoparallevar.minicards.bridge; import android.content.Context; import android.provider.Settings; +import com.codigoparallevar.minicards.ConfigManager; import com.programaker.bridge.ProgramakerBridge; import com.programaker.bridge.ProgramakerBridgeConfiguration; @@ -39,13 +40,14 @@ public class ProgramakerAndroidBridge { this.bridgeId = bridgeId; } - public void start(Runnable onComplete) { + public void start(Runnable onReady, Runnable onComplete) { ProgramakerBridgeConfiguration configuration = new ProgramakerBridgeConfiguration( ProgramakerAndroidBridge.GetBridgeName(this.ctx), + new ConfigManager(this.ctx), Collections.emptyList() ); - this.bridgeRunner = new ProgramakerBridge(this.bridgeId, this.userId, configuration, onComplete); + this.bridgeRunner = new ProgramakerBridge(this.bridgeId, this.userId, configuration, onReady, onComplete); this.bridgeRunner.run(); } diff --git a/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerBridgeService.java b/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerBridgeService.java index cbd615f..2871b23 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerBridgeService.java +++ b/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerBridgeService.java @@ -2,12 +2,15 @@ package com.codigoparallevar.minicards.bridge; import android.app.Service; import android.content.Intent; +import android.os.AsyncTask; import android.os.IBinder; import android.os.Process; import android.util.Log; import android.widget.Toast; import com.codigoparallevar.minicards.ConfigManager; +import com.codigoparallevar.minicards.types.functional.Tuple2; +import com.codigoparallevar.minicards.ui_helpers.DoAsync; import com.programaker.api.ProgramakerApi; public class ProgramakerBridgeService extends Service { @@ -24,15 +27,11 @@ public class ProgramakerBridgeService extends Service { ConfigManager config = new ConfigManager(this); String token = config.getToken(); - String bridgeId = config.getBridgeId(); - if (token == null || bridgeId == null) { + if (token == null) { Toast.makeText(this, "Cannot start bridge", Toast.LENGTH_SHORT).show(); if (token == null) { Log.e(LogTag, "Cannot start bridge: Token is null"); } - if (bridgeId == null) { - Log.e(LogTag, "Cannot start bridge: BridgeId is null (not created?)"); - } } if (ProgramakerBridgeService.this.bridge != null) { @@ -50,13 +49,40 @@ public class ProgramakerBridgeService extends Service { api.setToken(token); String userId = api.getUserId(); + String bridgeIdCheck = config.getBridgeId(); + if (bridgeIdCheck == null) { + bridgeIdCheck = api.createBridge(ProgramakerAndroidBridge.GetBridgeName(this)); + config.setBridgeId(bridgeIdCheck); + } + final String bridgeId = bridgeIdCheck; + ProgramakerBridgeService.this.bridge = ProgramakerAndroidBridge.configure( this, userId, bridgeId); - ProgramakerBridgeService.this.bridge.start(() -> { - ProgramakerBridgeService.this.bridge = null; - }); + ProgramakerBridgeService.this.bridge.start( + () -> { // On ready + if (config.getBridgeConnectionId() == null) { + new DoAsync().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, + new Tuple2<>( + () -> { + boolean established = api.establishConnection(bridgeId); + if (!established) { + Log.e(LogTag, "Error establishing connection to bridge"); + } + }, + ex -> { + Log.e(LogTag, "Error establishing bridge connection: " + ex, ex); + } + ) + ); + } + }, + () -> { // On completed + ProgramakerBridgeService.this.bridge = null; + Log.e(LogTag, "Bridge stopped, stopping service"); + ProgramakerBridgeService.this.stopSelf(); + }); } catch (Throwable ex) { Log.e(LogTag, "Error on bridge", ex); 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 92867e4..1749c1a 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/parts/ProgramakerCustomBlockPart.java +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/ProgramakerCustomBlockPart.java @@ -355,7 +355,7 @@ public class ProgramakerCustomBlockPart implements Part { ProgramakerCustomBlockPart.this.freeBlock(token); }, ex -> { - Log.e(LogTag, "Error executing function=" + this._block.getFunction_name() + Log.w(LogTag, "Error executing function=" + this._block.getFunction_name() + "; Error=" + ex, ex); ProgramakerCustomBlockPart.this.freeBlock(token); })); diff --git a/app/src/main/java/com/programaker/api/ProgramakerApi.kt b/app/src/main/java/com/programaker/api/ProgramakerApi.kt index b6da424..5bab848 100644 --- a/app/src/main/java/com/programaker/api/ProgramakerApi.kt +++ b/app/src/main/java/com/programaker/api/ProgramakerApi.kt @@ -182,6 +182,37 @@ class ProgramakerApi(private val ApiRoot: String="https://programaker.com/api") return result.getBridgeId() } + fun establishConnection(bridgeId: String): Boolean { + // NOTE: This establishes a connection to a bridge as long as it can be done without interaction + + // Prepare connection + val prepareConn = URL(getPrepareConnectionUrl(bridgeId)).openConnection() as HttpURLConnection + addAuthHeader(prepareConn) + + val prepareResult: ProgramakerPrepareConnectionResult + prepareResult = parseJson(prepareConn.inputStream, ProgramakerPrepareConnectionResult::class.java) + if (prepareResult.type != "direct") { + throw Exception("Expected 'direct' connection type, found '${prepareResult.type}'") + } + + // Establish connection + val establishConn = URL(getEstablishConnectionUrl(bridgeId)).openConnection() as HttpURLConnection + establishConn.setRequestProperty("Content-Type", "application/json") + addAuthHeader(establishConn) + + establishConn.requestMethod = "POST"; + establishConn.doOutput = true; + + val establishWrite = DataOutputStream(establishConn.outputStream) + establishWrite.writeBytes(JSONObject().toString()); + establishWrite.flush(); + establishWrite.close(); + + val establishResult: ProgramakerEstablishDirectConnectionResult + establishResult = parseJson(establishConn.inputStream, ProgramakerEstablishDirectConnectionResult::class.java) + return establishResult.success + } + // Initialization init { // Disable connection reuse if necessary @@ -220,6 +251,16 @@ class ProgramakerApi(private val ApiRoot: String="https://programaker.com/api") return "$ApiRoot/v0/users/$userName/bridges" } + private fun getPrepareConnectionUrl(bridgeId: String): String { + this.withUserName() + return "$ApiRoot/v0/users/$userName/services/id/$bridgeId/how-to-enable" + } + + private fun getEstablishConnectionUrl(bridgeId: String): String { + this.withUserName() + return "$ApiRoot/v0/users/$userName/services/id/$bridgeId/register" + } + fun getUserId(): String? { this.withUserId() return userId; diff --git a/app/src/main/java/com/programaker/api/data/api_results/ProgramakerEstablishDirectConnectionResult.kt b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerEstablishDirectConnectionResult.kt new file mode 100644 index 0000000..4d0f729 --- /dev/null +++ b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerEstablishDirectConnectionResult.kt @@ -0,0 +1,5 @@ +package com.programaker.api.data.api_results + +class ProgramakerEstablishDirectConnectionResult(val success: Boolean) { + +} diff --git a/app/src/main/java/com/programaker/api/data/api_results/ProgramakerPrepareConnectionResult.kt b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerPrepareConnectionResult.kt new file mode 100644 index 0000000..7dd7be9 --- /dev/null +++ b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerPrepareConnectionResult.kt @@ -0,0 +1,5 @@ +package com.programaker.api.data.api_results + +class ProgramakerPrepareConnectionResult(val type: String) { + +} diff --git a/app/src/main/java/com/programaker/bridge/ProgramakerBridge.kt b/app/src/main/java/com/programaker/bridge/ProgramakerBridge.kt index c8c62c2..4be269d 100644 --- a/app/src/main/java/com/programaker/bridge/ProgramakerBridge.kt +++ b/app/src/main/java/com/programaker/bridge/ProgramakerBridge.kt @@ -1,21 +1,30 @@ package com.programaker.bridge import android.util.Log +import com.google.gson.Gson import okhttp3.* import okio.ByteString +import org.json.JSONException +import org.json.JSONObject +import java.nio.charset.Charset import java.util.concurrent.TimeUnit class ProgramakerBridge( - val bridge_id: String, - val user_id: String, - val config: ProgramakerBridgeConfiguration, - val onComplete: Runnable + private val bridge_id: String, + private val user_id: String, + private val config: ProgramakerBridgeConfiguration, + private val onReady: Runnable, + private val onComplete: Runnable ) : WebSocketListener() { + private val utf8: Charset = Charset.forName("UTF-8") + private val gson = Gson() + private val PING_PERIOD_MILLIS: Long = 15000 private val apiRoot: String = "wss://programaker.com/api" private val LogTag = "ProgramakerBridge" + // Connection establishment fun run() { val client: OkHttpClient = OkHttpClient.Builder() .pingInterval(PING_PERIOD_MILLIS, TimeUnit.MILLISECONDS) @@ -31,17 +40,34 @@ class ProgramakerBridge( client.dispatcher.executorService.shutdown() } - - override fun onOpen(webSocket: WebSocket, response: Response) { - webSocket.send(config.serialize()) + private fun getBridgeControlUrl(): String { + return "$apiRoot/v0/users/id/$user_id/bridges/id/$bridge_id/communication" } - override fun onMessage(webSocket: WebSocket, text: String) { - println("MESSAGE: $text") + // Websocket management + override fun onOpen(webSocket: WebSocket, response: Response) { + webSocket.send(config.serialize()) + onReady.run() } override fun onMessage(webSocket: WebSocket, bytes: ByteString) { - println("MESSAGE: " + bytes.hex()) + onMessage(webSocket, bytes.string(utf8)) + } + + override fun onMessage(webSocket: WebSocket, text: String) { + Log.d(LogTag, "Message: $text") + val json = gson.fromJson(text, HashMap::class.java) + if (json == null) { + this.onFailure(webSocket, JSONException("Error decoding: $text"), null) + } + else { + try { + this.onCommand(webSocket, json) + } + catch (ex: Throwable ) { + Log.e(LogTag, "Error on command handling: $ex", ex) + } + } } override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { @@ -54,7 +80,57 @@ class ProgramakerBridge( Log.e(LogTag, t.toString(), t) } - private fun getBridgeControlUrl(): String { - return "$apiRoot/v0/users/id/$user_id/bridges/id/$bridge_id/communication" + // Protocol handling + private fun onCommand(webSocket: WebSocket, json: Map<*, *>) { + val type = json.get("type") as String + val messageId = json.get("message_id") as String + val value = json.get("value") as Map<*, *> + var userId: String? = null + var extraData: Map<*, *>? = null + if (json.containsKey("user_id")) { + userId = json.get("user_id") as String + } + if (json.containsKey("extra_data")) { + extraData = json.get("extra_data") as Map<*, *> + } + + when (type) { + "GET_HOW_TO_SERVICE_REGISTRATION" -> handleGetServiceRegistrationInfo(webSocket, value, messageId, userId, extraData) + "REGISTRATION" -> handlePerformRegistration(webSocket, value, messageId, userId, extraData) + else -> + { + Log.w(LogTag, "Unknown command type: $type") + } + } + } + + private fun handleGetServiceRegistrationInfo(webSocket: WebSocket, value: Map<*, *>, messageId: String?, userId: String?, extraData: Map<*, *>?) { + // Registration is automatic + webSocket.send( + JSONObject( + hashMapOf( + "message_id" to messageId, + "success" to true, + "result" to null + ) as Map<*, *> + ).toString() + ) + } + + private fun handlePerformRegistration(webSocket: WebSocket, value: Map<*, *>, messageId: String, userId: String?, extraData: Map<*, *>?) { + // Registration is automatic + if (userId == null) { + throw Exception("No connection ID received") + } + + webSocket.send( + JSONObject( + hashMapOf( + "message_id" to messageId, + "success" to true + ) as Map<*, *> + ).toString() + ) + this.config.configManager.bridgeConnectionId = userId } } \ 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 index bd2dd06..924085d 100644 --- a/app/src/main/java/com/programaker/bridge/ProgramakerBridgeConfiguration.kt +++ b/app/src/main/java/com/programaker/bridge/ProgramakerBridgeConfiguration.kt @@ -1,9 +1,11 @@ package com.programaker.bridge +import com.codigoparallevar.minicards.ConfigManager import org.json.JSONObject class ProgramakerBridgeConfiguration( - val service_name: String, + val serviceName: String, + val configManager: ConfigManager, val blocks: List // val is_public: boolean, // No reason for this use case // val registration: ???, // No reason for this use case @@ -17,7 +19,7 @@ class ProgramakerBridgeConfiguration( } val config = JSONObject(hashMapOf( - "service_name" to service_name, + "service_name" to serviceName, "is_public" to false, "registration" to null, "allow_multiple_connections" to null,