Implement connection handling; automatically connect user to bridge.

This commit is contained in:
Sergio Martínez Portela 2020-05-27 11:17:15 +02:00
parent 188f3290cf
commit 3f24489138
9 changed files with 196 additions and 25 deletions

View File

@ -10,6 +10,7 @@ public class ConfigManager {
private static final String PREFERENCES_NAME = "MINICARDS_PREFERENCES"; private static final String PREFERENCES_NAME = "MINICARDS_PREFERENCES";
private static final String TOKEN_KEY = "PROGRAMAKER_API_TOKEN"; private static final String TOKEN_KEY = "PROGRAMAKER_API_TOKEN";
private static final String BRIDGE_ID_KEY = "PROGRAMAKER_BRIDGE_ID"; 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) { public ConfigManager(Context ctx) {
this.context = ctx; this.context = ctx;
@ -49,4 +50,17 @@ public class ConfigManager {
SharedPreferences preferences = this.context.getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE); SharedPreferences preferences = this.context.getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE);
return preferences.getString(BRIDGE_ID_KEY, null); 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);
}
} }

View File

@ -3,6 +3,7 @@ package com.codigoparallevar.minicards.bridge;
import android.content.Context; import android.content.Context;
import android.provider.Settings; import android.provider.Settings;
import com.codigoparallevar.minicards.ConfigManager;
import com.programaker.bridge.ProgramakerBridge; import com.programaker.bridge.ProgramakerBridge;
import com.programaker.bridge.ProgramakerBridgeConfiguration; import com.programaker.bridge.ProgramakerBridgeConfiguration;
@ -39,13 +40,14 @@ public class ProgramakerAndroidBridge {
this.bridgeId = bridgeId; this.bridgeId = bridgeId;
} }
public void start(Runnable onComplete) { public void start(Runnable onReady, Runnable onComplete) {
ProgramakerBridgeConfiguration configuration = new ProgramakerBridgeConfiguration( ProgramakerBridgeConfiguration configuration = new ProgramakerBridgeConfiguration(
ProgramakerAndroidBridge.GetBridgeName(this.ctx), ProgramakerAndroidBridge.GetBridgeName(this.ctx),
new ConfigManager(this.ctx),
Collections.emptyList() 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(); this.bridgeRunner.run();
} }

View File

@ -2,12 +2,15 @@ package com.codigoparallevar.minicards.bridge;
import android.app.Service; import android.app.Service;
import android.content.Intent; import android.content.Intent;
import android.os.AsyncTask;
import android.os.IBinder; import android.os.IBinder;
import android.os.Process; import android.os.Process;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
import com.codigoparallevar.minicards.ConfigManager; import com.codigoparallevar.minicards.ConfigManager;
import com.codigoparallevar.minicards.types.functional.Tuple2;
import com.codigoparallevar.minicards.ui_helpers.DoAsync;
import com.programaker.api.ProgramakerApi; import com.programaker.api.ProgramakerApi;
public class ProgramakerBridgeService extends Service { public class ProgramakerBridgeService extends Service {
@ -24,15 +27,11 @@ public class ProgramakerBridgeService extends Service {
ConfigManager config = new ConfigManager(this); ConfigManager config = new ConfigManager(this);
String token = config.getToken(); String token = config.getToken();
String bridgeId = config.getBridgeId(); if (token == null) {
if (token == null || bridgeId == null) {
Toast.makeText(this, "Cannot start bridge", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "Cannot start bridge", Toast.LENGTH_SHORT).show();
if (token == null) { if (token == null) {
Log.e(LogTag, "Cannot start bridge: Token is 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) { if (ProgramakerBridgeService.this.bridge != null) {
@ -50,12 +49,39 @@ public class ProgramakerBridgeService extends Service {
api.setToken(token); api.setToken(token);
String userId = api.getUserId(); 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( ProgramakerBridgeService.this.bridge = ProgramakerAndroidBridge.configure(
this, this,
userId, userId,
bridgeId); bridgeId);
ProgramakerBridgeService.this.bridge.start(() -> { 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; ProgramakerBridgeService.this.bridge = null;
Log.e(LogTag, "Bridge stopped, stopping service");
ProgramakerBridgeService.this.stopSelf();
}); });
} }
catch (Throwable ex) { catch (Throwable ex) {

View File

@ -355,7 +355,7 @@ public class ProgramakerCustomBlockPart implements Part {
ProgramakerCustomBlockPart.this.freeBlock(token); ProgramakerCustomBlockPart.this.freeBlock(token);
}, ex -> { }, ex -> {
Log.e(LogTag, "Error executing function=" + this._block.getFunction_name() Log.w(LogTag, "Error executing function=" + this._block.getFunction_name()
+ "; Error=" + ex, ex); + "; Error=" + ex, ex);
ProgramakerCustomBlockPart.this.freeBlock(token); ProgramakerCustomBlockPart.this.freeBlock(token);
})); }));

View File

@ -182,6 +182,37 @@ class ProgramakerApi(private val ApiRoot: String="https://programaker.com/api")
return result.getBridgeId() 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 // Initialization
init { init {
// Disable connection reuse if necessary // 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" 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? { fun getUserId(): String? {
this.withUserId() this.withUserId()
return userId; return userId;

View File

@ -0,0 +1,5 @@
package com.programaker.api.data.api_results
class ProgramakerEstablishDirectConnectionResult(val success: Boolean) {
}

View File

@ -0,0 +1,5 @@
package com.programaker.api.data.api_results
class ProgramakerPrepareConnectionResult(val type: String) {
}

View File

@ -1,21 +1,30 @@
package com.programaker.bridge package com.programaker.bridge
import android.util.Log import android.util.Log
import com.google.gson.Gson
import okhttp3.* import okhttp3.*
import okio.ByteString import okio.ByteString
import org.json.JSONException
import org.json.JSONObject
import java.nio.charset.Charset
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class ProgramakerBridge( class ProgramakerBridge(
val bridge_id: String, private val bridge_id: String,
val user_id: String, private val user_id: String,
val config: ProgramakerBridgeConfiguration, private val config: ProgramakerBridgeConfiguration,
val onComplete: Runnable private val onReady: Runnable,
private val onComplete: Runnable
) : WebSocketListener() { ) : WebSocketListener() {
private val utf8: Charset = Charset.forName("UTF-8")
private val gson = Gson()
private val PING_PERIOD_MILLIS: Long = 15000 private val PING_PERIOD_MILLIS: Long = 15000
private val apiRoot: String = "wss://programaker.com/api" private val apiRoot: String = "wss://programaker.com/api"
private val LogTag = "ProgramakerBridge" private val LogTag = "ProgramakerBridge"
// Connection establishment
fun run() { fun run() {
val client: OkHttpClient = OkHttpClient.Builder() val client: OkHttpClient = OkHttpClient.Builder()
.pingInterval(PING_PERIOD_MILLIS, TimeUnit.MILLISECONDS) .pingInterval(PING_PERIOD_MILLIS, TimeUnit.MILLISECONDS)
@ -31,17 +40,34 @@ class ProgramakerBridge(
client.dispatcher.executorService.shutdown() client.dispatcher.executorService.shutdown()
} }
private fun getBridgeControlUrl(): String {
override fun onOpen(webSocket: WebSocket, response: Response) { return "$apiRoot/v0/users/id/$user_id/bridges/id/$bridge_id/communication"
webSocket.send(config.serialize())
} }
override fun onMessage(webSocket: WebSocket, text: String) { // Websocket management
println("MESSAGE: $text") override fun onOpen(webSocket: WebSocket, response: Response) {
webSocket.send(config.serialize())
onReady.run()
} }
override fun onMessage(webSocket: WebSocket, bytes: ByteString) { 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) { override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
@ -54,7 +80,57 @@ class ProgramakerBridge(
Log.e(LogTag, t.toString(), t) Log.e(LogTag, t.toString(), t)
} }
private fun getBridgeControlUrl(): String { // Protocol handling
return "$apiRoot/v0/users/id/$user_id/bridges/id/$bridge_id/communication" 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
} }
} }

View File

@ -1,9 +1,11 @@
package com.programaker.bridge package com.programaker.bridge
import com.codigoparallevar.minicards.ConfigManager
import org.json.JSONObject import org.json.JSONObject
class ProgramakerBridgeConfiguration( class ProgramakerBridgeConfiguration(
val service_name: String, val serviceName: String,
val configManager: ConfigManager,
val blocks: List<ProgramakerBridgeConfigurationBlock> val blocks: List<ProgramakerBridgeConfigurationBlock>
// val is_public: boolean, // No reason for this use case // val is_public: boolean, // No reason for this use case
// val registration: ???, // No reason for this use case // val registration: ???, // No reason for this use case
@ -17,7 +19,7 @@ class ProgramakerBridgeConfiguration(
} }
val config = JSONObject(hashMapOf( val config = JSONObject(hashMapOf(
"service_name" to service_name, "service_name" to serviceName,
"is_public" to false, "is_public" to false,
"registration" to null, "registration" to null,
"allow_multiple_connections" to null, "allow_multiple_connections" to null,