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 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
}));
|
}));
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user