Implement base bridge connection.

This commit is contained in:
Sergio Martínez Portela 2020-05-26 17:43:19 +02:00
parent a9b5c8f02b
commit da3dfd5d2e
14 changed files with 325 additions and 81 deletions

View File

@ -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()

View File

@ -7,6 +7,7 @@
android:required="true" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"

View File

@ -12,8 +12,6 @@ import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import com.codigoparallevar.minicards.toolbox.PartsHolder;
import com.codigoparallevar.minicards.types.functional.Consumer;
import com.codigoparallevar.minicards.types.functional.Producer;
import com.codigoparallevar.minicards.types.functional.Tuple2;
import com.codigoparallevar.minicards.types.functional.Tuple3;
import com.codigoparallevar.minicards.ui_helpers.GetAsync;
@ -67,26 +65,15 @@ public class CardActivity extends AppCompatActivity {
new GetAsync<Tuple2<List<ProgramakerBridgeInfo>, List<ProgramakerBridgeCustomBlockResult>>>()
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
new Tuple3<>(new Producer<Tuple2<List<ProgramakerBridgeInfo>, List<ProgramakerBridgeCustomBlockResult>>>() {
@Override
public Tuple2<List<ProgramakerBridgeInfo>, List<ProgramakerBridgeCustomBlockResult>> get() {
return new Tuple2<>(
new Tuple3<>(() ->
new Tuple2<>(
CardActivity.this.ProgramakerApi.fetchConnectedBridges(),
CardActivity.this.ProgramakerApi.fetchCustomBlocks());
}
}, new Consumer<Tuple2<List<ProgramakerBridgeInfo>,List<ProgramakerBridgeCustomBlockResult>>>() {
@Override
public void apply(Tuple2<List<ProgramakerBridgeInfo>,List<ProgramakerBridgeCustomBlockResult>> result) {
CardActivity.this.ProgramakerApi.fetchCustomBlocks()),
result -> {
partsHolder.addCustomBlocks(result.item1, result.item2);
Log.d("CARDActivity", "custom blocks: " + result.toString());
}
}, new Consumer<Throwable> () {
@Override
public void apply(Throwable exception) {
Log.e("CARDActivity", "error retrieving custom blocks: " + exception.toString());
}
}));
},
exception -> Log.e("CARDActivity", "error retrieving custom blocks: " + exception.toString())));
// Hide action bar
ActionBar actionBar = getSupportActionBar();

View File

@ -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);
}
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);
}
}

View File

@ -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,8 +97,7 @@ public class DeckPreviewActivity extends ReloadableAppCompatActivity {
new Tuple3<>(
loginUsernameText.getText().toString(),
loginPasswordText.getText().toString(),
new Consumer<String>() {
public void apply(String token) {
token -> {
if (token == null) {
messageLabel.setText(R.string.invalid_user_pass);
} else {
@ -103,12 +107,11 @@ public class DeckPreviewActivity extends ReloadableAppCompatActivity {
loginToProgramakerButton.setVisibility(View.GONE);
// Re-check... just in case
new CheckNeededLoginButton().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
new Tuple2<>(DeckPreviewActivity.this.ProgramakerApi,
loginToProgramakerButton));
checkNeededLoginButton(
DeckPreviewActivity.this.ProgramakerApi,
loginToProgramakerButton);
dialog.cancel();
}
}
}));
}
});
@ -119,15 +122,14 @@ public class DeckPreviewActivity extends ReloadableAppCompatActivity {
@Override
protected Tuple2<String, Consumer<String>> doInBackground(Tuple3<String, String, Consumer<String>>... 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<String, Consumer<String>>(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<Tuple2<ProgramakerApi, Button>, Void, Tuple2<Boolean, Button>>{
@Override
protected Tuple2<Boolean, Button> doInBackground(Tuple2<ProgramakerApi, Button>... tuples) {
return new Tuple2<>(tuples[0].item1.check(), tuples[0].item2);
}
@Override
protected void onPostExecute(Tuple2<Boolean, Button> 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<Boolean>().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<String>().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();

View File

@ -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<ProgramakerBridgeConfigurationBlock> 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();
}
}

View File

@ -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()) {

View File

@ -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<String>): 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)
}
}

View File

@ -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")
}
}
}

View File

@ -1,5 +1,6 @@
package com.programaker.api.exceptions
class ProgramakerProtocolException : Throwable() {
import java.lang.Exception
class ProgramakerProtocolException() : Exception() {
}

View File

@ -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"
}
}

View File

@ -0,0 +1,35 @@
package com.programaker.bridge
import org.json.JSONObject
class ProgramakerBridgeConfiguration(
val service_name: String,
val blocks: List<ProgramakerBridgeConfigurationBlock>
// 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<JSONObject>()
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()
}
}

View File

@ -0,0 +1,11 @@
package com.programaker.bridge
import org.json.JSONObject
class ProgramakerBridgeConfigurationBlock {
fun serialize() : JSONObject {
val obj = JSONObject();
return obj;
}
}

View File

@ -2,6 +2,7 @@
buildscript {
ext.kotlin_version = '1.3.61'
ext.ktor_version = '1.3.0'
repositories {
google()