Implement support for enum/callback arguments.

This commit is contained in:
Sergio Martínez Portela 2020-06-02 13:12:25 +02:00
parent 4ecd4936bd
commit 75421d4c95
12 changed files with 371 additions and 50 deletions

View File

@ -47,10 +47,10 @@ public class ProgramakerBridgeService extends Service {
@Override
public void onCreate() {
setBridgeStatusNotification(getString(R.string.bridge_service_not_started), ServiceState.LOADING);
setBridgeStatusNotification(getString(R.string.bridge_service_not_started), ServiceState.LOADING, null);
}
private void setBridgeStatusNotification(String title, ServiceState state) {
private void setBridgeStatusNotification(String title, ServiceState state, String description) {
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
assert notificationManager != null;
@ -94,6 +94,10 @@ public class ProgramakerBridgeService extends Service {
}
}
if (description != null) {
builder.setContentText(description);
}
Notification notification = builder.build();
if (stopped) {
@ -164,7 +168,7 @@ public class ProgramakerBridgeService extends Service {
bridgeId);
ProgramakerBridgeService.this.bridge.start(
() -> { // On ready
setBridgeStatusNotification(getString(R.string.bridge_service_online), ServiceState.RUNNING);
setBridgeStatusNotification(getString(R.string.bridge_service_online), ServiceState.RUNNING, null);
if (config.getBridgeConnectionId() == null) {
new DoAsync().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
new Tuple2<>(
@ -183,25 +187,29 @@ public class ProgramakerBridgeService extends Service {
}
},
() -> { // On completed
ProgramakerBridgeService.this.bridge = null;
onBridgeFailedAfterConnected();
});
}
catch (Throwable ex) {
Log.e(LogTag, "Error on bridge", ex);
ProgramakerBridgeService.this.bridge = null;
setBridgeStatusNotification(getString(R.string.bridge_service_starting),
ServiceState.STOPPED,
getString(R.string.error_establishing_connection));
}
}, "ServiceStartArguments");
thread.setPriority(Process.THREAD_PRIORITY_BACKGROUND);
setBridgeStatusNotification(getString(R.string.bridge_service_starting), ServiceState.LOADING);
setBridgeStatusNotification(getString(R.string.bridge_service_starting), ServiceState.LOADING, null);
thread.start();
}
private void onBridgeFailedAfterConnected() {
ProgramakerBridgeService.this.bridge = null;
if (!stopped) {
Log.e(LogTag, "Bridge stopped after connected. Waiting 10s then restarting");
setBridgeStatusNotification(getString(R.string.bridge_service_failed_restarting), ServiceState.LOADING);
setBridgeStatusNotification(getString(R.string.bridge_service_failed_restarting), ServiceState.LOADING, null);
try {
Thread.sleep(WAIT_TIME_BEFORE_RESTART_MILLIS);
} catch (InterruptedException e) {
@ -224,6 +232,6 @@ public class ProgramakerBridgeService extends Service {
if (bridge != null) {
bridge.stop();
}
setBridgeStatusNotification(getString(R.string.bridge_service_failed_stopping), ServiceState.STOPPED);
setBridgeStatusNotification(getString(R.string.bridge_service_failed_stopping), ServiceState.STOPPED, null);
}
}

View File

@ -187,7 +187,7 @@ public class ProgramakerCustomBlockPart implements Part, ProgramakerSignalListen
savedTo = arg;
}
else {
ConnectorTypeInfo typeInfo = ConnectorTypeInfo.FromTypeName(arg.getType());
ConnectorTypeInfo typeInfo = ConnectorTypeInfo.FromArgument(arg);
inputs.add(new Tuple2<>(typeInfo,
new ProgramakerCustomBlockInputConnector(this, _partGrid,
0, 0,
@ -199,7 +199,7 @@ public class ProgramakerCustomBlockPart implements Part, ProgramakerSignalListen
Tuple2<ConnectorTypeInfo, AnyRoundOutputConnector> saveToOutput = null;
if (savedTo != null) {
saveToOutput = new Tuple2<>(ConnectorTypeInfo.FromTypeName(savedTo.getType()),
saveToOutput = new Tuple2<>(ConnectorTypeInfo.FromTypeName(savedTo.getComputedType()),
new AnyRoundOutputConnector(this, this._partGrid, 0, 0, IO_RADIUS));
outputs.add(new Tuple2<>(saveToOutput.item1, saveToOutput.item2));
}

View File

@ -1,6 +1,7 @@
package com.codigoparallevar.minicards.parts.connectors;
import com.codigoparallevar.minicards.parts.style.CardTheme;
import com.programaker.api.data.ProgramakerCustomBlockArgument;
import org.json.JSONException;
import org.json.JSONObject;
@ -8,6 +9,9 @@ import org.json.JSONObject;
public class ConnectorTypeInfo {
private final Type _type;
private static final String SERIALIZED_TYPE_KEY = "type";
private static final String SERIALIZED_ARGUMENT_KEY = "argument";
private ProgramakerCustomBlockArgument _blockArgument = null;
public static ConnectorTypeInfo FromTypeName(String type) {
if (type == null) {
@ -66,7 +70,20 @@ public class ConnectorTypeInfo {
}
public static ConnectorTypeInfo deserialize(JSONObject jsonTypeInfo) {
return ConnectorTypeInfo.FromTypeName(jsonTypeInfo.optString(SERIALIZED_TYPE_KEY));
ConnectorTypeInfo type = ConnectorTypeInfo.FromTypeName(jsonTypeInfo.optString(SERIALIZED_TYPE_KEY));
JSONObject arg = jsonTypeInfo.optJSONObject(SERIALIZED_ARGUMENT_KEY);
if (arg != null) {
type._blockArgument = ProgramakerCustomBlockArgument.deserialize(arg);
}
return type;
}
public static ConnectorTypeInfo FromArgument(ProgramakerCustomBlockArgument arg) {
ConnectorTypeInfo type = ConnectorTypeInfo.FromTypeName(arg.getComputedType());
type._blockArgument = arg;
return type;
}
public int getOuterColor() {
@ -120,12 +137,20 @@ public class ConnectorTypeInfo {
JSONObject obj = new JSONObject();
try {
obj.put(SERIALIZED_TYPE_KEY, ConnectorTypeInfo.typeToString(this._type));
if (_blockArgument != null) {
obj.put(SERIALIZED_ARGUMENT_KEY, _blockArgument.serialize());
}
} catch (JSONException e) {
e.printStackTrace();
}
return obj;
}
public ProgramakerCustomBlockArgument getBlockArgument() {
return _blockArgument;
}
public enum Type {
PULSE,
BOOLEAN,

View File

@ -1,14 +1,18 @@
package com.codigoparallevar.minicards.parts.values;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.AsyncTask;
import android.text.InputType;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.ProgressBar;
import com.codigoparallevar.minicards.R;
import com.codigoparallevar.minicards.ScrolledCanvas;
@ -21,8 +25,14 @@ import com.codigoparallevar.minicards.types.PartGrid;
import com.codigoparallevar.minicards.types.connectors.Wiring.AnyWire;
import com.codigoparallevar.minicards.types.connectors.input.InputConnector;
import com.codigoparallevar.minicards.types.connectors.output.OutputConnector;
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.types.wireData.WireDataType;
import com.codigoparallevar.minicards.ui_helpers.GetAsync;
import com.codigoparallevar.minicards.utils.Serializations;
import com.programaker.api.data.ProgramakerCustomBlockArgument;
import com.programaker.api.data.ProgramakerCustomBlockArgumentValue;
import org.json.JSONArray;
import org.json.JSONException;
@ -51,12 +61,14 @@ public class StaticValuePart implements Part {
private int _left;
private int _top;
private String _value = null;
private String _valueId = null;
public StaticValuePart(String id, PartGrid grid, int centerx, int centery, ConnectorTypeInfo typeInfo, String value) {
public StaticValuePart(String id, PartGrid grid, int centerx, int centery, ConnectorTypeInfo typeInfo, String value, String valueId) {
this._id = id;
this._typeInfo = typeInfo;
this._grid = grid;
this._value = value;
this._valueId = valueId;
this.updateWidthHeight();
@ -66,6 +78,9 @@ public class StaticValuePart implements Part {
this._left + _width / 2,
this._top + _height, IO_RADIUS);
}
public StaticValuePart(String id, PartGrid grid, int centerx, int centery, ConnectorTypeInfo typeInfo, String value) {
this(id, grid, centerx, centery, typeInfo, value, null);
}
public StaticValuePart(String id, PartGrid grid, int centerx, int centery, ConnectorTypeInfo typeInfo) {
this(id, grid, centerx, centery, typeInfo, null);
@ -80,9 +95,10 @@ public class StaticValuePart implements Part {
JSONObject serialized = new JSONObject();
serialized.put("id", _id);
serialized.put("left", _left);
serialized.put("top", _top);
serialized.put("center_x", _left + _width / 2);
serialized.put("center_y", _top + _height / 2);
serialized.put("value", _value == null ? JSONObject.NULL : _value);
serialized.put("value_id", _valueId == null ? JSONObject.NULL : _valueId);
JSONObject jsonTypeInfo = _typeInfo.serialize();
serialized.put("type_info", jsonTypeInfo);
@ -109,13 +125,14 @@ public class StaticValuePart implements Part {
public static Tuple2<Part, List<PartConnection>> deserialize(PartGrid grid, JSONObject data) throws JSONException {
String id = data.getString("id");
int left = data.getInt("left");
int top = data.getInt("top");
String value = data.optString("value");
int centerx = data.getInt("center_x");
int centery = data.getInt("center_y");
String value = Serializations.getString(data, "value");
String valueId = Serializations.getString(data, "value_id");
JSONObject jsonTypeInfo = data.getJSONObject("type_info");
ConnectorTypeInfo typeInfo = ConnectorTypeInfo.deserialize(jsonTypeInfo);
StaticValuePart part = new StaticValuePart(id, grid, left, top, typeInfo, value);
StaticValuePart part = new StaticValuePart(id, grid, centerx, centery, typeInfo, value, valueId);
List<PartConnection> connections = new LinkedList<>();
@ -177,6 +194,14 @@ public class StaticValuePart implements Part {
}
private String getMessage() {
if (_value == null) {
return "-";
} else {
return _value;
}
}
private String getMessageForEdit() {
ConnectorTypeInfo.Type type = _typeInfo.get_type();
if (type == ConnectorTypeInfo.Type.INTEGER || type == ConnectorTypeInfo.Type.FLOAT) {
if (_value == null) {
@ -192,10 +217,9 @@ public class StaticValuePart implements Part {
}
else {
if (_value == null) {
return "-";
} else {
return _value;
return "";
}
return _value;
}
}
@ -212,52 +236,131 @@ public class StaticValuePart implements Part {
return; // Can't show dialog if can't retrieve context
}
final Context ctx = ((View) this._grid).getContext();
ConnectorTypeInfo.Type type = this._typeInfo.get_type();
if (type == ConnectorTypeInfo.Type.PULSE) {
return; // Nothing to do here
}
Context ctx = ((View) this._grid).getContext();
AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
builder.setTitle(R.string.set_value);
Runnable prepareOnAccept = () -> {};
switch (type) {
case BOOLEAN:
break;
case INTEGER:
case FLOAT:
{
case FLOAT: {
final EditText input = new EditText(ctx);
input.setText(getMessage());
input.setText(getMessageForEdit());
input.setInputType(InputType.TYPE_CLASS_NUMBER);
builder.setView(input);
prepareOnAccept = () -> {
dialogAskUserForValue(ctx, input,
() -> {
_value = input.getText().toString();
};
});
break;
}
default:
case ENUM:
// TODO: Show enumerated values
case ANY:
case UNKNOWN:
case STRING:
{
case STRING: {
final EditText input = new EditText(ctx);
input.setText(getMessage());
input.setText(getMessageForEdit());
input.setInputType(InputType.TYPE_CLASS_TEXT);
builder.setView(input);
prepareOnAccept = () -> {
dialogAskUserForValue(ctx, input,
() -> {
_value = input.getText().toString();
};
});
break;
}
case ENUM:
ProgramakerCustomBlockArgument blockArg = _typeInfo.getBlockArgument();
String callback = null;
if (blockArg != null) {
callback = blockArg.getCallback();
if (callback.equals("null")) { // TODO: Fix this on server
callback = null;
}
}
if (callback == null) {
new AlertDialog.Builder(ctx)
.setMessage(R.string.error_recovering_allowed_values)
.create()
.show();
break;
}
Dialog loadingDialog = createLoadingDialog(ctx);
loadingDialog.show();
new GetAsync<List<ProgramakerCustomBlockArgumentValue>>().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
new Tuple3<>(
() -> _grid.getApi().fetchAllowedValues(blockArg),
results -> {
Log.i(LogTag, "Result: " + results);
loadingDialog.cancel();
String[] names = new String[results.size()];
int i = 0;
for (ProgramakerCustomBlockArgumentValue value : results) {
names[i] = value.getName();
i++;
}
Dialog d = createItemDialog(ctx, names, idx -> {
StaticValuePart.this._value = results.get(idx).getName();
StaticValuePart.this._valueId = results.get(idx).getId();
StaticValuePart.this._grid.update();
});
d.show();
},
ex -> {
Log.e(LogTag, "Error retrieving values: " + ex, ex);
new AlertDialog.Builder(ctx)
.setMessage(R.string.error_recovering_allowed_values)
.create()
.show();
}
));
break;
}
}
final Runnable onAccept = prepareOnAccept;
private Dialog createItemDialog(Context ctx, CharSequence[] itemNames, Consumer<Integer> onSelected) {
final AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
builder.setTitle(R.string.loading_available_values);
final ProgressBar progressBar = new ProgressBar(ctx);
progressBar.setIndeterminate(true);
builder.setItems(itemNames, (DialogInterface.OnClickListener) (dialog, which) -> {
try {
onSelected.apply(which);
} catch (Exception e) {
e.printStackTrace();
}
});
AlertDialog dialog = builder.create();
return dialog;
}
private Dialog createLoadingDialog(Context ctx) {
final AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
builder.setTitle(R.string.loading_available_values);
final ProgressBar progressBar = new ProgressBar(ctx);
progressBar.setIndeterminate(true);
builder.setView(progressBar);
AlertDialog dialog = builder.create();
return dialog;
}
private void dialogAskUserForValue(Context ctx, EditText input, Runnable onAccept) {
final AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
builder.setTitle(R.string.set_value);
builder
.setPositiveButton(R.string.ok_accept_changes, (dialog, which) -> {
onAccept.run();
@ -266,10 +369,12 @@ public class StaticValuePart implements Part {
})
.setNegativeButton(R.string.cancel_discard_changes, (dialog, which) -> {
// No change
} );
})
.setView(input);
AlertDialog dialog = builder.create();
dialog.show();
}
@Override
@ -321,6 +426,10 @@ public class StaticValuePart implements Part {
@Override
public Object query(Object lastValue) {
if (this._valueId != null) {
return this._valueId;
}
return this._value;
}

View File

@ -72,4 +72,18 @@ public class Serializations {
return obj;
}
}
public static String getString(JSONObject map, String key) {
if (!map.has(key)) {
return null;
}
if (map.opt(key) == JSONObject.NULL) {
return null;
}
try {
return map.getString(key);
} catch (JSONException e) {
return null;
}
}
}

View File

@ -4,9 +4,7 @@ import android.os.Build
import android.util.Log
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.programaker.api.data.ProgramakerBridgeInfo
import com.programaker.api.data.ProgramakerCustomBlock
import com.programaker.api.data.ProgramakerFunctionCallResult
import com.programaker.api.data.*
import com.programaker.api.data.api_results.*
import com.programaker.api.exceptions.ProgramakerLoginRequiredException
import com.programaker.api.exceptions.ProgramakerProtocolException
@ -138,6 +136,29 @@ class ProgramakerApi(private val ApiRoot: String="https://programaker.com/api")
return result.result
}
fun fetchAllowedValues(blockArg: ProgramakerCustomBlockArgument): List<ProgramakerCustomBlockArgumentValue>? {
val conn = URL(getArgumentValuesUrl(blockArg)).openConnection() as HttpURLConnection
addAuthHeader(conn)
var results: List<ProgramakerCustomBlockArgumentValue>? = null
try {
val serialized: HashMap<String,*> = parseJson(conn.inputStream, HashMap::class.java)
val returnInfo = ProgramakerCustomBlockArgumentValuesReturnInfo.deserialize(serialized)
if (!returnInfo.success) {
return null
}
results = returnInfo.values
} catch (ex: FileNotFoundException) {
ex.logError(LogTag)
return null
} catch(ex: Exception) {
Log.e(LogTag, ex.toString(), ex)
return null
}
return results
}
fun callBlock(block: ProgramakerCustomBlock, arguments: List<String>): ProgramakerFunctionCallResult {
val conn = URL(getBlockUrl(block)).openConnection() as HttpURLConnection
conn.setRequestProperty("Content-Type", "application/json")
@ -278,6 +299,11 @@ class ProgramakerApi(private val ApiRoot: String="https://programaker.com/api")
return "$ApiRoot/v0/users/id/$userId/bridges/id/$bridgeId/signals/$key"
}
private fun getArgumentValuesUrl(blockArg: ProgramakerCustomBlockArgument): String {
this.withUserId()
return "$ApiRoot/v0/users/id/$userId/bridges/id/${blockArg.bridge_id}/callback/${blockArg.callback}"
}
fun getUserId(): String? {
this.withUserId()
return userId;

View File

@ -11,16 +11,28 @@ class ProgramakerCustomBlockArgument(
// val class: String,
val callback: String?
) {
var bridge_id: String? = null
fun serialize(): JSONObject {
val serialized = hashMapOf<String, String?>(
"type" to type,
"default_value" to default_value,
"callback" to callback
"callback" to callback,
"bridge_id" to bridge_id
)
return JSONObject(serialized as Map<*, *>)
}
fun getComputedType(): String? {
if (callback != null && callback != "null") {
return "ENUM";
}
else {
return type;
}
}
companion object {
@JvmStatic
fun deserialize(arguments: JSONArray?): List<ProgramakerCustomBlockArgument> {
@ -45,12 +57,16 @@ class ProgramakerCustomBlockArgument(
return results
}
private fun deserialize(arguments: JSONObject): ProgramakerCustomBlockArgument {
@JvmStatic
fun deserialize(arguments: JSONObject): ProgramakerCustomBlockArgument {
val type: String? = arguments.optString("type")
val default_value: String? = arguments.optString("default_value")
val callback: String? = arguments.optString("callback")
val bridge_id: String? = arguments.optString("bridge_id")
return ProgramakerCustomBlockArgument(type, default_value, callback)
val arg = ProgramakerCustomBlockArgument(type, default_value, callback)
arg.bridge_id = bridge_id
return arg
}
}

View File

@ -0,0 +1,67 @@
package com.programaker.api.data
import com.programaker.api.optGet
import java.util.*
class ProgramakerCustomBlockArgumentValue (
val id: String,
val name: String
) {
companion object {
@JvmStatic
fun deserializeList(value: Object): List<ProgramakerCustomBlockArgumentValue> {
if (value is Map<*, *>) {
return deserialize(value as Map<*, *>)
} else if (value is List<*>) {
return deserialize(value as List<*>)
} else {
throw IllegalArgumentException("Error deserializing: $value")
}
}
@JvmStatic
fun deserialize(value: Map<*, *>): List<ProgramakerCustomBlockArgumentValue> {
val results = LinkedList<ProgramakerCustomBlockArgumentValue>()
for (entry in value.asIterable()) {
var value = entry.value.toString()
if (entry.value is Map<*, *>) {
if ((entry.value as Map<String, *>).containsKey("name")) {
value = (entry.value as Map<*, *>)["name"].toString()
}
}
results.add(ProgramakerCustomBlockArgumentValue(entry.key.toString(), value))
}
return results
}
@JvmStatic
fun deserialize(value: List<*>): List<ProgramakerCustomBlockArgumentValue> {
val results = LinkedList<ProgramakerCustomBlockArgumentValue>()
for (entry in value) {
if (entry is Map<*,*>) {
var name = (entry as Map<String, *>).optGet("name")
var id = (entry as Map<String, *>).optGet("id")
if (name == null && id == null) {
throw IllegalArgumentException("Error deserializing: $entry")
}
else if (name == null) {
name = id
}
else if (id == null) {
id = name
}
results.add(ProgramakerCustomBlockArgumentValue(id.toString(), name.toString()))
}
else {
throw IllegalArgumentException("Error deserializing: $entry")
}
}
return results
}
}
}

View File

@ -0,0 +1,29 @@
package com.programaker.api.data
import com.programaker.api.optGet
import java.util.*
class ProgramakerCustomBlockArgumentValuesReturnInfo(
val success: Boolean,
val values: List<ProgramakerCustomBlockArgumentValue>
) {
companion object {
@JvmStatic
fun deserialize(value: HashMap<String,*>): ProgramakerCustomBlockArgumentValuesReturnInfo {
val _success: Boolean? = value.optGet("success") as Boolean?
val _result: Object? = value.optGet("result") as Object
var success = false
if (_success != null) {
success = _success
}
var result: List<ProgramakerCustomBlockArgumentValue> = Collections.emptyList<ProgramakerCustomBlockArgumentValue>()
if (_result != null) {
result = ProgramakerCustomBlockArgumentValue.deserializeList(_result)
}
return ProgramakerCustomBlockArgumentValuesReturnInfo(success, result)
}
}
}

View File

@ -23,6 +23,11 @@ internal class ProgramakerGetCustomBlocksResultTypeAdapter : JsonSerializer<Prog
for (value in value1.asJsonArray) {
val block = gson.fromJson(value, ProgramakerCustomBlock::class.java)
block.bridge_id = bridgeId
if (block.arguments != null) {
for (arg in block.arguments) {
arg.bridge_id = bridgeId
}
}
blocks.add(block)
}

View File

@ -0,0 +1,19 @@
package com.programaker.api
internal fun <K, V> java.util.HashMap<K, V>.optGet(key: K): V? {
if (this.containsKey(key)) {
return this.get(key)
}
else {
return null
}
}
internal fun <K, V> Map<K, V>.optGet(key: K): V? {
if (this.containsKey(key)) {
return this.get(key)
}
else {
return null
}
}

View File

@ -29,4 +29,7 @@
<string name="set_value">Set block value</string>
<string name="ok_accept_changes">OK</string>
<string name="cancel_discard_changes">Cancel</string>
<string name="error_establishing_connection">Error establishing connection</string>
<string name="error_recovering_allowed_values">Error recovering allowed values</string>
<string name="loading_available_values">Loading available values...</string>
</resources>