Implement connection handling; automatically connect user to bridge.
This commit is contained in:
parent
188f3290cf
commit
3f24489138
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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,12 +49,39 @@ 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.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) {
|
||||
|
@ -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);
|
||||
}));
|
||||
|
@ -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;
|
||||
|
@ -0,0 +1,5 @@
|
||||
package com.programaker.api.data.api_results
|
||||
|
||||
class ProgramakerEstablishDirectConnectionResult(val success: Boolean) {
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package com.programaker.api.data.api_results
|
||||
|
||||
class ProgramakerPrepareConnectionResult(val type: String) {
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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<ProgramakerBridgeConfigurationBlock>
|
||||
// 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,
|
||||
|
Loading…
Reference in New Issue
Block a user