diff --git a/app/src/main/java/com/codigoparallevar/minicards/DeckPreviewActivity.java b/app/src/main/java/com/codigoparallevar/minicards/DeckPreviewActivity.java index ac6e9dd..7916c56 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/DeckPreviewActivity.java +++ b/app/src/main/java/com/codigoparallevar/minicards/DeckPreviewActivity.java @@ -133,7 +133,12 @@ public class DeckPreviewActivity extends ReloadableAppCompatActivity { @Override protected void onPostExecute(Tuple2> result) { - result.item2.apply(result.item1); + try { + result.item2.apply(result.item1); + } + catch (Throwable ex) { + Log.e(LogTag, "Error on login UI update", ex); + } } } diff --git a/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerAndroidBridge.java b/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerAndroidBridge.java index 048832e..f401bdb 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerAndroidBridge.java +++ b/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerAndroidBridge.java @@ -4,11 +4,10 @@ import android.content.Context; import android.provider.Settings; import com.codigoparallevar.minicards.ConfigManager; +import com.codigoparallevar.minicards.bridge.blocks.DefaultAndroidBlocks; 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; @@ -24,12 +23,7 @@ public class ProgramakerAndroidBridge { return serviceName; } -// public static ProgramakerBridgeConfiguration GetConfiguration(Context ctx) { -// List blocks = new LinkedList<>(); -// return new ProgramakerBridgeConfiguration(serviceName, blocks); -// } // Builder - private final Context ctx; private final String userId; private final String bridgeId; @@ -44,7 +38,7 @@ public class ProgramakerAndroidBridge { ProgramakerBridgeConfiguration configuration = new ProgramakerBridgeConfiguration( ProgramakerAndroidBridge.GetBridgeName(this.ctx), new ConfigManager(this.ctx), - Collections.emptyList() + DefaultAndroidBlocks.GetBuilder(this.ctx).Build() ); this.bridgeRunner = new ProgramakerBridge(this.bridgeId, this.userId, configuration, onReady, onComplete); diff --git a/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerBridgeService.java b/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerBridgeService.java index 2871b23..dfd70c4 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerBridgeService.java +++ b/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerBridgeService.java @@ -14,6 +14,8 @@ import com.codigoparallevar.minicards.ui_helpers.DoAsync; import com.programaker.api.ProgramakerApi; public class ProgramakerBridgeService extends Service { + public static final String BridgeUserNotificationChannel = "PROGRAMAKER_BRIDGE_USER_NOTIFICATION"; + public static final CharSequence BridgeUserNotificationChannelName = "User notifications"; private ProgramakerAndroidBridge bridge = null; private static final String LogTag = "PM BridgeService"; diff --git a/app/src/main/java/com/codigoparallevar/minicards/bridge/blocks/BlockArgumentDefinition.java b/app/src/main/java/com/codigoparallevar/minicards/bridge/blocks/BlockArgumentDefinition.java new file mode 100644 index 0000000..9e759c9 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/bridge/blocks/BlockArgumentDefinition.java @@ -0,0 +1,51 @@ +package com.codigoparallevar.minicards.bridge.blocks; + +import android.util.Log; + +import org.jetbrains.annotations.NotNull; +import org.json.JSONException; +import org.json.JSONObject; + +class BlockArgumentDefinition { + private final static String LogTag = "PM BlockArgument"; + private final Type type; + private final String defaultValue; + + public enum Type { + STRING, + INT, + FLOAT, + BOOL, + } + + BlockArgumentDefinition(BlockArgumentDefinition.Type type, String defaultValue) { + this.type = type; + this.defaultValue = defaultValue; + } + + @NotNull + public JSONObject serialize() throws JSONException { + JSONObject obj = new JSONObject(); + + obj.put("type", BlockArgumentDefinition.TypeToString(type)); + obj.put("default", this.defaultValue); + + return obj; + } + + private static String TypeToString(Type type) { + switch (type) { + case STRING: + return "string"; + case INT: + return "integer"; + case FLOAT: + return "float"; + case BOOL: + return "bool"; + default: + Log.e(LogTag, "Unknown type: " + type); + return "string"; + } + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/bridge/blocks/BridgeBlockListBuilder.java b/app/src/main/java/com/codigoparallevar/minicards/bridge/blocks/BridgeBlockListBuilder.java new file mode 100644 index 0000000..2a503b3 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/bridge/blocks/BridgeBlockListBuilder.java @@ -0,0 +1,33 @@ +package com.codigoparallevar.minicards.bridge.blocks; + +import com.codigoparallevar.minicards.types.functional.Consumer; +import com.programaker.bridge.ProgramakerBridgeConfigurationBlock; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public class BridgeBlockListBuilder { + private final LinkedList blockList; + + public BridgeBlockListBuilder() { + this.blockList = new LinkedList<>(); + } + + + public List Build() { + return blockList; + } + + public BridgeBlockListBuilder addOperation(String id, String message, + List arguments, + Consumer> operation) { + return this.addOperation(new OperationBlockDefinition(id, message, arguments, operation)); + } + + private BridgeBlockListBuilder addOperation(ProgramakerBridgeConfigurationBlock block) { + this.blockList.add(block); + + return this; + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/bridge/blocks/DefaultAndroidBlocks.java b/app/src/main/java/com/codigoparallevar/minicards/bridge/blocks/DefaultAndroidBlocks.java new file mode 100644 index 0000000..fe61797 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/bridge/blocks/DefaultAndroidBlocks.java @@ -0,0 +1,75 @@ +package com.codigoparallevar.minicards.bridge.blocks; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; + +import androidx.core.app.NotificationCompat; + +import com.codigoparallevar.minicards.R; +import com.codigoparallevar.minicards.bridge.ProgramakerBridgeService; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; + +public class DefaultAndroidBlocks { + + public static BridgeBlockListBuilder GetBuilder(Context ctx) { + // System services + String notifServiceId = Context.NOTIFICATION_SERVICE; + NotificationManager notificationManager = (NotificationManager) ctx.getSystemService(notifServiceId); + assert notificationManager != null; + + Random notificationRandom = new Random(); + + String notificationChannelId = ProgramakerBridgeService.BridgeUserNotificationChannel; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + ProgramakerBridgeService.BridgeUserNotificationChannel, + ProgramakerBridgeService.BridgeUserNotificationChannelName, + NotificationManager.IMPORTANCE_DEFAULT); + notificationManager.createNotificationChannel(channel); + } + + + return new BridgeBlockListBuilder() + // Notifications + .addOperation( + "notifications_new", + "Create notification. Title: %1, text: %2", + new LinkedList() {{ + add(new BlockArgumentDefinition(BlockArgumentDefinition.Type.STRING, "My notification")); + add(new BlockArgumentDefinition(BlockArgumentDefinition.Type.STRING, "Sample description")); + }}, + (List params) -> { + if (params.size() != 2) { + throw new Exception("Expected two (2) arguments, found " + params.size()); + } + String title = params.get(0).toString(); + String description = params.get(1).toString(); + + Notification notif = new NotificationCompat + .Builder(ctx, ProgramakerBridgeService.BridgeUserNotificationChannel) + .setContentTitle(title) + .setContentText(description) + .setSmallIcon(R.drawable.ic_center_focus_weak_black) // TODO: Change icon + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .build(); + + int notificationId = notificationRandom.nextInt(); + notificationManager.notify(notificationId, notif); + } + ) + .addOperation( + "notifications_clear", + "Clear notifications", + Collections.emptyList(), + (List params) -> { + notificationManager.cancelAll(); + } + ); + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/bridge/blocks/OperationBlockDefinition.java b/app/src/main/java/com/codigoparallevar/minicards/bridge/blocks/OperationBlockDefinition.java new file mode 100644 index 0000000..4d48d5c --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/bridge/blocks/OperationBlockDefinition.java @@ -0,0 +1,65 @@ +package com.codigoparallevar.minicards.bridge.blocks; + +import android.util.Log; + +import com.codigoparallevar.minicards.types.functional.Consumer; +import com.programaker.bridge.ProgramakerBridgeConfigurationBlock; + +import org.jetbrains.annotations.NotNull; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.List; + +class OperationBlockDefinition implements ProgramakerBridgeConfigurationBlock { + private final static String LogTag = "PM OpBlockDefinition"; + + private final String id; + private final String message; + private final List args; + private final Consumer> operation; + + public OperationBlockDefinition(String id, String message, List args, Consumer> operation) { + this.id = id; + this.message = message; + this.args = args; + this.operation = operation; + } + + @NotNull + @Override + public JSONObject serialize() { + JSONObject obj = new JSONObject(); + + try { + JSONArray arguments = new JSONArray(); + for (BlockArgumentDefinition arg : this.args) { + arguments.put(arg.serialize()); + } + + obj.put("id", this.id); + obj.put("function_name", this.id); + obj.put("message", this.message); + obj.put("block_type", "operation"); + obj.put("block_result_type", JSONObject.NULL); + obj.put("arguments", arguments); + obj.put("save_to", JSONObject.NULL); + } catch (JSONException ex) { + Log.e(LogTag, "Error serializing block definition: " + ex, ex); + } + + return obj; + } + + @NotNull + @Override + public String getFunctionName() { + return this.id; + } + + @Override + public void call(@NotNull List arguments) throws Exception { + this.operation.apply(arguments); + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/functional/Consumer.java b/app/src/main/java/com/codigoparallevar/minicards/types/functional/Consumer.java index d90a981..5c57f5d 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/types/functional/Consumer.java +++ b/app/src/main/java/com/codigoparallevar/minicards/types/functional/Consumer.java @@ -1,5 +1,5 @@ package com.codigoparallevar.minicards.types.functional; public interface Consumer { - void apply(T param); + void apply(T param) throws Exception; } diff --git a/app/src/main/java/com/codigoparallevar/minicards/ui_helpers/DoAsync.java b/app/src/main/java/com/codigoparallevar/minicards/ui_helpers/DoAsync.java index cb0eefe..44536b5 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/ui_helpers/DoAsync.java +++ b/app/src/main/java/com/codigoparallevar/minicards/ui_helpers/DoAsync.java @@ -1,6 +1,7 @@ package com.codigoparallevar.minicards.ui_helpers; import android.os.AsyncTask; +import android.util.Log; import com.codigoparallevar.minicards.types.functional.Action; import com.codigoparallevar.minicards.types.functional.Consumer; @@ -15,7 +16,11 @@ public class DoAsync extends AsyncTask>, data[0].item1.run(); } catch (Throwable ex) { - data[0].item2.apply(ex); + try { + data[0].item2.apply(ex); + } catch (Throwable subEx) { + Log.e("DoAsync", "Error handling exception", subEx); + } } return null; } diff --git a/app/src/main/java/com/codigoparallevar/minicards/ui_helpers/GetAsync.java b/app/src/main/java/com/codigoparallevar/minicards/ui_helpers/GetAsync.java index 500b17e..02394b7 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/ui_helpers/GetAsync.java +++ b/app/src/main/java/com/codigoparallevar/minicards/ui_helpers/GetAsync.java @@ -19,7 +19,12 @@ public class GetAsync extends AsyncTask, Consumer, Cons return new Tuple2<>(result, data[0]._y); } catch (Throwable ex) { - data[0]._z.apply(ex); + try { + data[0]._z.apply(ex); + } + catch (Throwable subEx) { + Log.d("GetAsync", "Error handling exception", subEx); + } return null; } } @@ -30,7 +35,12 @@ public class GetAsync extends AsyncTask, Consumer, Cons return; } else { - result.item2.apply(result.item1); + try { + result.item2.apply(result.item1); + } + catch (Throwable ex) { + Log.e("GetAsync", "Error on UI thread", ex); + } } } } diff --git a/app/src/main/java/com/programaker/bridge/ProgramakerBridge.kt b/app/src/main/java/com/programaker/bridge/ProgramakerBridge.kt index 4be269d..dfd8fc1 100644 --- a/app/src/main/java/com/programaker/bridge/ProgramakerBridge.kt +++ b/app/src/main/java/com/programaker/bridge/ProgramakerBridge.kt @@ -97,6 +97,7 @@ class ProgramakerBridge( when (type) { "GET_HOW_TO_SERVICE_REGISTRATION" -> handleGetServiceRegistrationInfo(webSocket, value, messageId, userId, extraData) "REGISTRATION" -> handlePerformRegistration(webSocket, value, messageId, userId, extraData) + "FUNCTION_CALL" -> handleFunctionCall(webSocket, value, messageId, userId, extraData) else -> { Log.w(LogTag, "Unknown command type: $type") @@ -104,6 +105,38 @@ class ProgramakerBridge( } } + private fun handleFunctionCall(webSocket: WebSocket, value: Map<*, *>, messageId: String, userId: String?, extraData: Map<*, *>?) { + val functionName = value.get("function_name") as String + val arguments = value.get("arguments") as List<*> + + try { + config.callFunction(functionName, arguments) + + val result = JSONObject.NULL + webSocket.send( + JSONObject( + hashMapOf( + "message_id" to messageId, + "success" to true, + "result" to result + ) as Map<*, *> + ).toString() + ) + } + catch (ex: Throwable) { + Log.w(LogTag, "Error on bridge call to $functionName", ex) + webSocket.send( + JSONObject( + hashMapOf( + "message_id" to messageId, + "success" to false + ) as Map<*, *> + ).toString() + ) + } + + } + private fun handleGetServiceRegistrationInfo(webSocket: WebSocket, value: Map<*, *>, messageId: String?, userId: String?, extraData: Map<*, *>?) { // Registration is automatic webSocket.send( diff --git a/app/src/main/java/com/programaker/bridge/ProgramakerBridgeConfiguration.kt b/app/src/main/java/com/programaker/bridge/ProgramakerBridgeConfiguration.kt index 924085d..6b1a1d1 100644 --- a/app/src/main/java/com/programaker/bridge/ProgramakerBridgeConfiguration.kt +++ b/app/src/main/java/com/programaker/bridge/ProgramakerBridgeConfiguration.kt @@ -12,6 +12,7 @@ class ProgramakerBridgeConfiguration( // 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() if (blocks != null) { @@ -34,4 +35,14 @@ class ProgramakerBridgeConfiguration( return wrapper.toString() } + + fun callFunction(functionName: String, arguments: List<*>) { + for (block in blocks) { + if (block.getFunctionName() == functionName) { + return block.call(arguments) + } + } + + throw IllegalArgumentException("Bridge function (name=$functionName) not found") + } } \ No newline at end of file diff --git a/app/src/main/java/com/programaker/bridge/ProgramakerBridgeConfigurationBlock.kt b/app/src/main/java/com/programaker/bridge/ProgramakerBridgeConfigurationBlock.kt index 83a909a..440dcfc 100644 --- a/app/src/main/java/com/programaker/bridge/ProgramakerBridgeConfigurationBlock.kt +++ b/app/src/main/java/com/programaker/bridge/ProgramakerBridgeConfigurationBlock.kt @@ -2,10 +2,10 @@ package com.programaker.bridge import org.json.JSONObject -class ProgramakerBridgeConfigurationBlock { - fun serialize() : JSONObject { - val obj = JSONObject(); - return obj; - } +interface ProgramakerBridgeConfigurationBlock { + fun serialize() : JSONObject; + fun getFunctionName(): String; + @Throws(Exception::class) + fun call(arguments: List<*>) }