diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..0689d7c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,45 @@
+
+ROUND_ICONS=app/src/main/res/mipmap-mdpi/ic_launcher_round.png \
+ app/src/main/res/mipmap-hdpi/ic_launcher_round.png \
+ app/src/main/res/mipmap-xhdpi/ic_launcher_round.png \
+ app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
+
+RECT_ICONS=app/src/main/res/mipmap-mdpi/ic_launcher.png \
+ app/src/main/res/mipmap-hdpi/ic_launcher.png \
+ app/src/main/res/mipmap-xhdpi/ic_launcher.png \
+ app/src/main/res/mipmap-xxhdpi/ic_launcher.png
+
+ICONS=$(ROUND_ICONS) $(RECT_ICONS)
+RECT_BASE=app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
+ROUND_BASE=app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
+BASE_ICONS=$(RECT_BASE) $(ROUND_BASE)
+
+.PHONY: all
+
+all: $(ICONS)
+
+app/src/main/res/mipmap-mdpi/ic_launcher_round.png: $(ROUND_BASE)
+ convert $< -resize 48x48 $@
+
+app/src/main/res/mipmap-hdpi/ic_launcher_round.png: $(ROUND_BASE)
+ convert $< -resize 72x72 $@
+
+app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: $(ROUND_BASE)
+ convert $< -resize 96x96 $@
+
+app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: $(ROUND_BASE)
+ convert $< -resize 144x144 $@
+
+app/src/main/res/mipmap-mdpi/ic_launcher.png: $(RECT_BASE)
+ convert $< -resize 48x48 $@
+
+app/src/main/res/mipmap-hdpi/ic_launcher.png: $(RECT_BASE)
+ convert $< -resize 72x72 $@
+
+app/src/main/res/mipmap-xhdpi/ic_launcher.png: $(RECT_BASE)
+ convert $< -resize 96x96 $@
+
+app/src/main/res/mipmap-xxhdpi/ic_launcher.png: $(RECT_BASE)
+ convert $< -resize 144x144 $@
+
+
diff --git a/app/build.gradle b/app/build.gradle
index 5f303e0..f2090bd 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,15 +1,17 @@
apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android-extensions'
+apply plugin: 'kotlin-android'
android {
- compileSdkVersion 25
+ compileSdkVersion 29
buildToolsVersion "27.0.1"
defaultConfig {
applicationId "com.codigoparallevar.minicards"
minSdkVersion 15
- targetSdkVersion 25
+ targetSdkVersion 29
versionCode 1
versionName "1.0"
- testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
@@ -17,18 +19,31 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
+ compileOptions {
+ sourceCompatibility = 1.8
+ targetCompatibility = 1.8
+ }
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
- androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
+ implementation 'androidx.annotation:annotation:1.0.0'
+ implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
+ androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', {
exclude group: 'com.android.support', module: 'support-annotations'
})
- implementation 'com.android.support:appcompat-v7:25.4.0'
+ implementation 'androidx.appcompat:appcompat:1.0.0'
testImplementation 'junit:junit:4.12'
- implementation 'com.android.support.constraint:constraint-layout:1.0.2'
- implementation 'com.android.support:design:25.4.0'
- compile 'com.getbase:floatingactionbutton:1.10.1'
- compile 'com.larswerkman:HoloColorPicker:1.5'
- implementation 'com.android.support:cardview-v7:25.4.0'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'com.google.android.material:material:1.0.0'
+ implementation 'com.getbase:floatingactionbutton:1.10.1'
+ implementation 'com.larswerkman:HoloColorPicker:1.5'
+ 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()
}
diff --git a/app/src/androidTest/java/com/codigoparallevar/minicards/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/codigoparallevar/minicards/ExampleInstrumentedTest.java
index 462e2a4..96d21a8 100644
--- a/app/src/androidTest/java/com/codigoparallevar/minicards/ExampleInstrumentedTest.java
+++ b/app/src/androidTest/java/com/codigoparallevar/minicards/ExampleInstrumentedTest.java
@@ -1,8 +1,8 @@
package com.codigoparallevar.minicards;
import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1b40fe2..a664cc1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,11 +3,14 @@
package="com.codigoparallevar.minicards">
-
+
+
+
+
+
+
+
@@ -26,10 +35,12 @@
android:theme="@style/AppTheme.NoActionBar">
+
+
diff --git a/app/src/main/java/com/codigoparallevar/minicards/CanvasView.java b/app/src/main/java/com/codigoparallevar/minicards/CanvasView.java
index 0a395d3..b94e38b 100644
--- a/app/src/main/java/com/codigoparallevar/minicards/CanvasView.java
+++ b/app/src/main/java/com/codigoparallevar/minicards/CanvasView.java
@@ -4,27 +4,29 @@ import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.codigoparallevar.minicards.motion.MotionMode;
import com.codigoparallevar.minicards.types.Part;
import com.codigoparallevar.minicards.types.PartConnection;
import com.codigoparallevar.minicards.types.PartGrid;
import com.codigoparallevar.minicards.types.Position;
import com.codigoparallevar.minicards.types.Selectable;
-import com.codigoparallevar.minicards.types.Tuple2;
-import com.codigoparallevar.minicards.types.Tuple4;
import com.codigoparallevar.minicards.types.connectors.input.AnyInputConnector;
import com.codigoparallevar.minicards.types.connectors.input.BooleanInputConnector;
import com.codigoparallevar.minicards.types.connectors.input.InputConnector;
import com.codigoparallevar.minicards.types.connectors.input.SignalInputConnector;
import com.codigoparallevar.minicards.types.connectors.input.StringInputConnector;
import com.codigoparallevar.minicards.types.connectors.output.OutputConnector;
+import com.codigoparallevar.minicards.types.functional.Tuple2;
+import com.codigoparallevar.minicards.types.functional.Tuple4;
+import com.programaker.api.ProgramakerApi;
import java.io.IOException;
import java.util.ArrayList;
@@ -32,7 +34,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
-class CanvasView extends View implements PartGrid {
+public class CanvasView extends View implements PartGrid {
@NonNull
List parts = new ArrayList<>();
@@ -51,6 +53,7 @@ class CanvasView extends View implements PartGrid {
@NonNull
private String name = "default";
+ private ProgramakerApi api = null;
private final static float touchTimeForLongTouchInMillis = 500;
private boolean _isDragging = false;
private CardActivity parentActivity = null;
@@ -142,7 +145,7 @@ class CanvasView extends View implements PartGrid {
part.draw(scrolledCanvas, _devMode);
}
- Log.d("Render time", System.currentTimeMillis() - renderStartTime + "ms");
+ // Log.d("Render time", System.currentTimeMillis() - renderStartTime + "ms");
}
private void drawBackground(ScrolledCanvas canvas) {
@@ -236,6 +239,7 @@ class CanvasView extends View implements PartGrid {
break;
}
+ Log.d("CanvasView", "Moving part="+selectedPart);
if (selectedPart == null){
int xMovement = _mouseDownPoint.item1 - xInScreen;
int yMovement = _mouseDownPoint.item2 - yInScreen;
@@ -246,7 +250,7 @@ class CanvasView extends View implements PartGrid {
_mouseDownPoint = new Tuple2(xInScreen, yInScreen);
}
else {
- Log.i("Canvas", "X: " + xInScreen + " Y: " + yInScreen
+ Log.d("Canvas", "X: " + xInScreen + " Y: " + yInScreen
+ " in drop zone " + _dropToRemoveZone + " : "
+ inDropZone(xInScreen, yInScreen));
@@ -323,15 +327,22 @@ class CanvasView extends View implements PartGrid {
for (int i = parts.size() - 1; i >= 0; i--){
final Part part = parts.get(i);
// First try with output connectors
- for (OutputConnector outputConnector : part.getOutputConnectors()){
- if (outputConnector.containsPoint(x, y)){
- return outputConnector;
+ List outputConnectors = part.getOutputConnectors();
+ if (outputConnectors != null) {
+ for (OutputConnector outputConnector : outputConnectors) {
+ if (outputConnector.containsPoint(x, y)) {
+ return outputConnector;
+ }
}
}
+
// Then with input ones
- for (InputConnector inputConnector : part.getInputConnectors()){
- if (inputConnector.containsPoint(x, y)){
- return inputConnector;
+ List inputConnectors = part.getInputConnectors();
+ if (inputConnectors != null) {
+ for (InputConnector inputConnector : inputConnectors) {
+ if (inputConnector.containsPoint(x, y)) {
+ return inputConnector;
+ }
}
}
}
@@ -339,6 +350,15 @@ class CanvasView extends View implements PartGrid {
return null;
}
+ @Override
+ public ProgramakerApi getApi() {
+ if (api == null) {
+ api = new ProgramakerApi();
+ api.setToken(new ConfigManager(this.getContext()).getToken());
+ }
+ return api;
+ }
+
@Override
@Nullable
public SignalInputConnector getSignalInputConnectorOn(int x, int y) {
diff --git a/app/src/main/java/com/codigoparallevar/minicards/CardActivity.java b/app/src/main/java/com/codigoparallevar/minicards/CardActivity.java
index 301a64b..9a3dbfe 100644
--- a/app/src/main/java/com/codigoparallevar/minicards/CardActivity.java
+++ b/app/src/main/java/com/codigoparallevar/minicards/CardActivity.java
@@ -2,15 +2,27 @@ package com.codigoparallevar.minicards;
import android.content.Context;
import android.content.Intent;
-import android.support.design.widget.FloatingActionButton;
-import android.support.v7.app.ActionBar;
-import android.support.v7.app.AppCompatActivity;
+import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.codigoparallevar.minicards.toolbox.PartsHolder;
+import com.codigoparallevar.minicards.types.functional.Tuple2;
+import com.codigoparallevar.minicards.types.functional.Tuple3;
+import com.codigoparallevar.minicards.ui_helpers.GetAsync;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import com.programaker.api.ProgramakerApi;
+import com.programaker.api.data.ProgramakerBridgeInfo;
+import com.programaker.api.data.api_results.ProgramakerBridgeCustomBlockResult;
+import com.programaker.api.data.api_results.ProgramakerGetCustomBlocksResult;
+
import java.io.IOException;
+import java.util.List;
public class CardActivity extends AppCompatActivity {
@@ -18,7 +30,7 @@ public class CardActivity extends AppCompatActivity {
public static final String CARD_PATH_KEY = "CARD_PATH";
public static final String VISUALIZATION_MODE_KEY = "VISUALIZATION_MODE";
public static final String DEVELOPER_VISUALIZATION_MODE = "DEVELOPER_VISUALIZATION_MODE";
-
+ public static final String USER_VISUALIZATION_MODE = "USER_VISUALIZATION_MODE";
CanvasView canvasView;
com.getbase.floatingactionbutton.AddFloatingActionButton AddPartButton;
@@ -34,11 +46,35 @@ public class CardActivity extends AppCompatActivity {
boolean devMode = false;
FloatingActionButton removePartFab;
private PartsHolder partsHolder;
+ private ProgramakerApi ProgramakerApi;
+ private ConfigManager Config;
+ private ProgramakerGetCustomBlocksResult CustomBlocks = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ this.Config = new ConfigManager(this);
+ this.ProgramakerApi = new ProgramakerApi();
+ String token = this.Config.getToken();
+ if (token != null) {
+ this.ProgramakerApi.setToken(token);
+ }
+
+ partsHolder = new PartsHolder(this);
+
+ new GetAsync, List>>()
+ .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
+ new Tuple3<>(() ->
+ new Tuple2<>(
+ CardActivity.this.ProgramakerApi.fetchConnectedBridges(),
+ CardActivity.this.ProgramakerApi.fetchCustomBlocks()),
+ result -> {
+ partsHolder.addCustomBlocks(result.item1, result.item2);
+ Log.d("CARDActivity", "custom blocks: " + result.toString());
+ },
+ exception -> Log.e("CARDActivity", "error retrieving custom blocks: " + exception.toString())));
+
// Hide action bar
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
@@ -52,7 +88,6 @@ public class CardActivity extends AppCompatActivity {
canvasView.setParentActivity(this);
// Initialize auxiliary elements
- partsHolder = new PartsHolder(this);
removePartFab = (FloatingActionButton) findViewById(R.id.remove_part_fab);
canvasView.setDropZone(
@@ -97,8 +132,7 @@ public class CardActivity extends AppCompatActivity {
ShowDeckFromDevModeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- Intent i = new Intent(DeckPreviewActivity.INTENT);
- CardActivity.this.startActivity(i);
+ finish();
}
});
@@ -107,8 +141,7 @@ public class CardActivity extends AppCompatActivity {
ShowDeckFromUserModeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- Intent i = new Intent(DeckPreviewActivity.INTENT);
- CardActivity.this.startActivity(i);
+ finish();
}
});
@@ -196,12 +229,12 @@ public class CardActivity extends AppCompatActivity {
if (canvasView.isDragging()){
devFabMenu.setVisibility(View.GONE);
userFabMenu.setVisibility(View.GONE);
- removePartFab.setVisibility(View.VISIBLE);
+ ((View)removePartFab).setVisibility(View.VISIBLE);
Log.d("Main", "Changing visibility!");
}
else {
this.setDevMode(devMode);
- removePartFab.setVisibility(View.GONE);
+ ((View)removePartFab).setVisibility(View.GONE);
Log.d("Main", "Now changing visibility!");
}
canvasView.setDropZone(
@@ -211,8 +244,16 @@ public class CardActivity extends AppCompatActivity {
}
public static void openCard(Context context, CardFile cardfile, String visualizationMode) {
+ openCardByPath(context, cardfile.getPath(), visualizationMode);
+ }
+
+ public static void openCard(Context context, PreviewCard cardfile, String visualizationMode) {
+ openCardByPath(context, cardfile.getPath(), visualizationMode);
+ }
+
+ private static void openCardByPath(Context context, String path, String visualizationMode) {
Intent i = new Intent(INTENT);
- i.putExtra(CARD_PATH_KEY, cardfile.getPath());
+ i.putExtra(CARD_PATH_KEY, path);
if (visualizationMode != null) {
i.putExtra(VISUALIZATION_MODE_KEY, visualizationMode);
}
diff --git a/app/src/main/java/com/codigoparallevar/minicards/CardFile.java b/app/src/main/java/com/codigoparallevar/minicards/CardFile.java
index ee796a7..0aa786f 100644
--- a/app/src/main/java/com/codigoparallevar/minicards/CardFile.java
+++ b/app/src/main/java/com/codigoparallevar/minicards/CardFile.java
@@ -4,9 +4,11 @@ import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
-import android.support.annotation.NonNull;
import android.util.Log;
+import androidx.annotation.NonNull;
+
+import com.codigoparallevar.minicards.parts.ProgramakerCustomBlockPart;
import com.codigoparallevar.minicards.parts.buttons.RoundButton;
import com.codigoparallevar.minicards.parts.logic.Ticker;
import com.codigoparallevar.minicards.parts.logic.Toggle;
@@ -16,7 +18,7 @@ import com.codigoparallevar.minicards.parts.strings.ConvertToString;
import com.codigoparallevar.minicards.types.Part;
import com.codigoparallevar.minicards.types.PartConnection;
import com.codigoparallevar.minicards.types.PartGrid;
-import com.codigoparallevar.minicards.types.Tuple2;
+import com.codigoparallevar.minicards.types.functional.Tuple2;
import org.json.JSONArray;
import org.json.JSONException;
@@ -34,7 +36,7 @@ import java.util.UUID;
public class CardFile {
- static final int DEFAULT_BACKGROUND_COLOR = Color.parseColor("#044563");
+ static final int DEFAULT_BACKGROUND_COLOR = Color.parseColor("#4485A3");
static final String PATH_SEPARATOR = "/";
@NonNull
private final String cardsDirectory;
@@ -284,6 +286,13 @@ public class CardFile {
return convertToStringInfo;
}
+ else if (type.equals(ProgramakerCustomBlockPart.class.getName())){
+ Tuple2> customBlockPartInfo = ProgramakerCustomBlockPart.deserialize(
+ grid,
+ jsonObject.getJSONObject("_data"));
+
+ return customBlockPartInfo;
+ }
else {
throw new JSONException("Expected known class, found " + type);
}
diff --git a/app/src/main/java/com/codigoparallevar/minicards/CardPreviewArrayAdapter.java b/app/src/main/java/com/codigoparallevar/minicards/CardPreviewArrayAdapter.java
index 28a0997..8e2aab6 100644
--- a/app/src/main/java/com/codigoparallevar/minicards/CardPreviewArrayAdapter.java
+++ b/app/src/main/java/com/codigoparallevar/minicards/CardPreviewArrayAdapter.java
@@ -4,9 +4,9 @@ import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
-import android.support.annotation.NonNull;
-import android.support.v7.app.AlertDialog;
-import android.support.v7.widget.CardView;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.cardview.widget.CardView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -17,6 +17,8 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
import java.io.IOException;
class CardPreviewArrayAdapter extends ArrayAdapter {
@@ -41,16 +43,18 @@ class CardPreviewArrayAdapter extends ArrayAdapter {
final View row = inflater.inflate(R.layout.card_preview, parent, false);
final PreviewCard card = this.cards[position];
- row.setOnClickListener(new View.OnClickListener() {
+ TextView nameView = row.findViewById(R.id.card_preview_name);
+ final CardView cardView = row.findViewById(R.id.card_preview_card);
+ final FloatingActionButton settingsButton = row.findViewById(R.id.card_preview_settings_button);
+
+ cardView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- Intent i = new Intent(CardActivity.INTENT);
- i.putExtra(CardActivity.CARD_PATH_KEY, card.getPath());
- CardPreviewArrayAdapter.this.getContext().startActivity(i);
+ CardActivity.openCard(getContext(), card, CardActivity.DEVELOPER_VISUALIZATION_MODE);
}
});
- final ImageView settingsButton = (ImageView) row.findViewById(R.id.card_preview_settings_button);
+
settingsButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@@ -58,19 +62,8 @@ class CardPreviewArrayAdapter extends ArrayAdapter {
}
});
- CardView cardView = (CardView) row.findViewById(R.id.card_preview_card);
- TextView nameView = (TextView) row.findViewById(R.id.card_preview_name);
- int backgroundColor = card.getColor();
- cardView.setBackgroundColor(backgroundColor);
nameView.setText(card.getName());
- if (backgroundColor == CardFile.DEFAULT_BACKGROUND_COLOR){
- nameView.setTextColor(0xFFF0F0F0);
- }
- else {
- nameView.setTextColor(0xFFFFFF ^ backgroundColor);
- }
-
return row;
}
@@ -80,7 +73,7 @@ class CardPreviewArrayAdapter extends ArrayAdapter {
final View openCardOptions = (LayoutInflater.from(getContext())
.inflate(R.layout.card_settings_dialog, null));
- final EditText cardNameEditText = (EditText) openCardOptions.findViewById(R.id.card_setting_name_edit_text);
+ final EditText cardNameEditText = openCardOptions.findViewById(R.id.card_setting_name_edit_text);
cardNameEditText.setText(card.getName());
builder.setTitle("Card settings")
@@ -115,7 +108,7 @@ class CardPreviewArrayAdapter extends ArrayAdapter {
final Dialog dialog = builder.create();
- final TextView deleteCardLink = (TextView) openCardOptions.findViewById(R.id.card_setting_delete_card);
+ final TextView deleteCardLink = openCardOptions.findViewById(R.id.card_setting_delete_card);
deleteCardLink.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
diff --git a/app/src/main/java/com/codigoparallevar/minicards/ConfigManager.java b/app/src/main/java/com/codigoparallevar/minicards/ConfigManager.java
new file mode 100644
index 0000000..825dc5d
--- /dev/null
+++ b/app/src/main/java/com/codigoparallevar/minicards/ConfigManager.java
@@ -0,0 +1,66 @@
+package com.codigoparallevar.minicards;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import static android.content.Context.MODE_PRIVATE;
+
+public class ConfigManager {
+ private final Context context;
+ 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;
+ }
+
+ public void setToken(String token) {
+ SharedPreferences preferences = this.context.getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE);
+ SharedPreferences.Editor edit = preferences.edit();
+
+ edit.putString(TOKEN_KEY, token);
+ edit.apply();
+ }
+
+ public String getToken() {
+ SharedPreferences preferences = this.context.getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE);
+ 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);
+ }
+
+ 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);
+ }
+}
diff --git a/app/src/main/java/com/codigoparallevar/minicards/DeckPreviewActivity.java b/app/src/main/java/com/codigoparallevar/minicards/DeckPreviewActivity.java
index eeb23a1..7916c56 100644
--- a/app/src/main/java/com/codigoparallevar/minicards/DeckPreviewActivity.java
+++ b/app/src/main/java/com/codigoparallevar/minicards/DeckPreviewActivity.java
@@ -2,36 +2,158 @@ package com.codigoparallevar.minicards;
import android.app.Dialog;
import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.AsyncTask;
import android.os.Bundle;
-import android.support.design.widget.FloatingActionButton;
-import android.support.v7.app.AlertDialog;
-import android.support.v7.widget.Toolbar;
+import android.text.Editable;
+import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
+import android.widget.Button;
import android.widget.EditText;
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.ProgramakerBridgeService;
+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.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;
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;
+
+ protected void openLoginDialog(View view) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+
+ final View loginDialog = (LayoutInflater.from(this)
+ .inflate(R.layout.login_dialog_view, null));
+
+ final EditText loginUsernameText = loginDialog.findViewById(R.id.login_username_text);
+ final EditText loginPasswordText = loginDialog.findViewById(R.id.login_password_text);
+ final Button loginButton = loginDialog.findViewById(R.id.login_dialog_login_button);
+ final Button cancelButton = loginDialog.findViewById(R.id.login_dialog_cancel_button);
+ final TextView messageLabel = loginDialog.findViewById(R.id.login_message_label);
+
+ builder.setTitle("Login").setView(loginDialog);
+ final Dialog dialog = builder.create();
+ dialog.show();
+
+ cancelButton.setOnClickListener(new View.OnClickListener(){
+ @Override
+ public void onClick(View v) {
+ dialog.cancel();
+ }
+ });
+
+ final TextWatcher watcher = (new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ if ((messageLabel.getVisibility() != View.VISIBLE) &&
+ (loginUsernameText.getText().length() > 0) &&
+ (loginPasswordText.getText().length() > 0)) {
+ loginButton.setEnabled(true);
+ } else {
+ loginButton.setEnabled(false);
+ }
+ }
+ });
+
+ loginButton.setEnabled(false);
+ loginUsernameText.addTextChangedListener(watcher);
+ loginPasswordText.addTextChangedListener(watcher);
+
+ loginButton.setOnClickListener(new View.OnClickListener(){
+ @Override
+ public void onClick(View v) {
+ messageLabel.setVisibility(View.VISIBLE);
+ messageLabel.setText(R.string.loading);
+ new CheckLogin().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
+ new Tuple3<>(
+ loginUsernameText.getText().toString(),
+ loginPasswordText.getText().toString(),
+ token -> {
+ if (token == null) {
+ messageLabel.setText(R.string.invalid_user_pass);
+ } else {
+ DeckPreviewActivity.this.Config.setToken(token);
+ DeckPreviewActivity.this.ProgramakerApi.setToken(token);
+ final Button loginToProgramakerButton = findViewById(R.id.login_in_programaker_button);
+
+ loginToProgramakerButton.setVisibility(View.GONE);
+ // Re-check... just in case
+ checkNeededLoginButton(
+ DeckPreviewActivity.this.ProgramakerApi,
+ loginToProgramakerButton);
+ dialog.cancel();
+ }
+ }));
+ }
+ });
+ }
+
+ static class CheckLogin extends AsyncTask>,
+ Void, Tuple2>>{
+ @Override
+ protected Tuple2> doInBackground(Tuple3>... tuples) {
+ ProgramakerApi api = new ProgramakerApi();
+ String token = null;
+ try {
+ token = api.login(tuples[0]._x, tuples[0]._y);
+ }
+ catch (Exception e) {
+ Log.e("Login to PrograMaker", e.toString(), e);
+ }
+ return new Tuple2<>(token, tuples[0]._z);
+ }
+
+ @Override
+ protected void onPostExecute(Tuple2> result) {
+ try {
+ result.item2.apply(result.item1);
+ }
+ catch (Throwable ex) {
+ Log.e(LogTag, "Error on login UI update", ex);
+ }
+ }
+ }
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+
+ this.Config = new ConfigManager(this);
+ this.ProgramakerApi = new ProgramakerApi();
+
setContentView(R.layout.activity_deck_preview);
- Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
- FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.create_new_card_fab);
+ FloatingActionButton fab = findViewById(R.id.create_new_card_fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
@@ -39,7 +161,43 @@ public class DeckPreviewActivity extends ReloadableAppCompatActivity {
}
});
- listView = (ListView) findViewById(R.id.card_deck_list);
+ final Button loginButton = findViewById(R.id.login_in_programaker_button);
+ loginButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ DeckPreviewActivity.this.openLoginDialog(v);
+ }
+ });
+
+ listView = findViewById(R.id.card_deck_list);
+ String token = this.Config.getToken();
+ if (token == null) {
+ loginButton.setVisibility(View.VISIBLE);
+ }
+ else {
+
+ this.ProgramakerApi.setToken(token);
+ loginButton.setVisibility(View.GONE);
+ // Double check that is not needed, token might have been deleted
+ checkNeededLoginButton(this.ProgramakerApi, loginButton);
+ }
+ }
+
+ private void checkNeededLoginButton(ProgramakerApi api, Button loginButton) {
+ new GetAsync().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
+ new Tuple3<>(
+ () -> api.check(),
+ result -> {
+ if (!result) {
+ loginButton.setVisibility(View.VISIBLE);
+ DeckPreviewActivity.this.Config.removeBridgeId();
+ } else {
+ Intent intent = new Intent(this, ProgramakerBridgeService.class);
+ startService(intent);
+ }
+ },
+ ex -> Log.e(LogTag, "Error checking API:" + ex, ex)
+ ));
}
@Override
@@ -61,7 +219,7 @@ public class DeckPreviewActivity extends ReloadableAppCompatActivity {
final View openCardOptions = (LayoutInflater.from(this)
.inflate(R.layout.create_new_card_view, null));
- final EditText cardNameEditText = (EditText) openCardOptions.findViewById(R.id.card_name_edit_text);
+ final EditText cardNameEditText = openCardOptions.findViewById(R.id.card_name_edit_text);
builder.setTitle("Create a new card")
.setView(openCardOptions)
diff --git a/app/src/main/java/com/codigoparallevar/minicards/PartInstantiator.java b/app/src/main/java/com/codigoparallevar/minicards/PartInstantiator.java
index 9328e4d..830195e 100644
--- a/app/src/main/java/com/codigoparallevar/minicards/PartInstantiator.java
+++ b/app/src/main/java/com/codigoparallevar/minicards/PartInstantiator.java
@@ -2,7 +2,7 @@ package com.codigoparallevar.minicards;
import com.codigoparallevar.minicards.types.Part;
import com.codigoparallevar.minicards.types.PartGrid;
-import com.codigoparallevar.minicards.types.Tuple2;
+import com.codigoparallevar.minicards.types.functional.Tuple2;
public abstract class PartInstantiator {
protected abstract Part instantiate(PartGrid grid, Tuple2 center);
diff --git a/app/src/main/java/com/codigoparallevar/minicards/PartsHolder.java b/app/src/main/java/com/codigoparallevar/minicards/PartsHolder.java
deleted file mode 100644
index 4ccf298..0000000
--- a/app/src/main/java/com/codigoparallevar/minicards/PartsHolder.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package com.codigoparallevar.minicards;
-
-import android.app.Dialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.support.v7.app.AlertDialog;
-import android.util.Log;
-
-import com.codigoparallevar.minicards.parts.buttons.RoundButton;
-import com.codigoparallevar.minicards.parts.logic.Ticker;
-import com.codigoparallevar.minicards.parts.logic.Toggle;
-import com.codigoparallevar.minicards.parts.samples.ColorBox;
-import com.codigoparallevar.minicards.parts.strings.ConvertToString;
-import com.codigoparallevar.minicards.types.Part;
-import com.codigoparallevar.minicards.types.Tuple2;
-
-import java.util.List;
-import java.util.Vector;
-
-class PartsHolder {
- private final Context context;
-
- private final static List> BuiltInParts =
- new Vector>(){{
- add(new Tuple2<>("Round button", RoundButton.getInstantiator()));
- add(new Tuple2<>("Ticker", Ticker.getInstantiator()));
- add(new Tuple2<>("Red/Green box", ColorBox.getInstantiator()));
- add(new Tuple2<>("Toggle", Toggle.getInstantiator()));
- add(new Tuple2<>("ToString", ConvertToString.getInstantiator()));
- }};
-
- public PartsHolder(Context context) {
- this.context = context;
- }
-
- public void openAddPartModal(final CanvasView canvasView) {
- AlertDialog.Builder builder = new AlertDialog.Builder(context);
- builder.setTitle("Choose part type")
- .setItems(getPartTypes(), new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) {
- if ((which >= 0) && (which < BuiltInParts.size())){
- Log.d("Minicards partsHolder",
- "Spawning " + BuiltInParts.get(which).item1);
- PartInstantiator instantiator = BuiltInParts.get(which).item2;
- PartsHolder.this.runInstantiator(instantiator, canvasView);
- }
- }
- });
-
- Dialog dialog = builder.create();
- dialog.show();
- }
-
- private void runInstantiator(PartInstantiator instantiator, CanvasView canvasView) {
- Part part = instantiator.build(canvasView);
- canvasView.addPart(part);
- }
-
- private static String[] getPartTypes() {
- String[] partTypes = new String[BuiltInParts.size()];
- for (int i = 0; i < BuiltInParts.size(); i++){
- partTypes[i] = BuiltInParts.get(i).item1;
- }
-
- return partTypes;
- }
-}
diff --git a/app/src/main/java/com/codigoparallevar/minicards/ReloadableAppCompatActivity.java b/app/src/main/java/com/codigoparallevar/minicards/ReloadableAppCompatActivity.java
index c9082af..24c5a8d 100644
--- a/app/src/main/java/com/codigoparallevar/minicards/ReloadableAppCompatActivity.java
+++ b/app/src/main/java/com/codigoparallevar/minicards/ReloadableAppCompatActivity.java
@@ -1,6 +1,6 @@
package com.codigoparallevar.minicards;
-import android.support.v7.app.AppCompatActivity;
+import androidx.appcompat.app.AppCompatActivity;
abstract class ReloadableAppCompatActivity extends AppCompatActivity {
public abstract void reload();
diff --git a/app/src/main/java/com/codigoparallevar/minicards/ScrolledCanvas.java b/app/src/main/java/com/codigoparallevar/minicards/ScrolledCanvas.java
index 46e650e..90a3858 100644
--- a/app/src/main/java/com/codigoparallevar/minicards/ScrolledCanvas.java
+++ b/app/src/main/java/com/codigoparallevar/minicards/ScrolledCanvas.java
@@ -4,9 +4,9 @@ import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
-import android.support.annotation.NonNull;
+import androidx.annotation.NonNull;
-import com.codigoparallevar.minicards.types.Tuple2;
+import com.codigoparallevar.minicards.types.functional.Tuple2;
public class ScrolledCanvas {
private final Canvas canvas;
diff --git a/app/src/main/java/com/codigoparallevar/minicards/StubPartGrid.java b/app/src/main/java/com/codigoparallevar/minicards/StubPartGrid.java
index fab7739..2d75446 100644
--- a/app/src/main/java/com/codigoparallevar/minicards/StubPartGrid.java
+++ b/app/src/main/java/com/codigoparallevar/minicards/StubPartGrid.java
@@ -2,10 +2,11 @@ package com.codigoparallevar.minicards;
import com.codigoparallevar.minicards.types.PartGrid;
import com.codigoparallevar.minicards.types.Selectable;
-import com.codigoparallevar.minicards.types.Tuple2;
+import com.codigoparallevar.minicards.types.functional.Tuple2;
import com.codigoparallevar.minicards.types.connectors.input.BooleanInputConnector;
import com.codigoparallevar.minicards.types.connectors.input.SignalInputConnector;
import com.codigoparallevar.minicards.types.connectors.input.StringInputConnector;
+import com.programaker.api.ProgramakerApi;
class StubPartGrid implements PartGrid {
@Override
@@ -13,6 +14,11 @@ class StubPartGrid implements PartGrid {
return null;
}
+ @Override
+ public ProgramakerApi getApi() {
+ return null;
+ }
+
@Override
public SignalInputConnector getSignalInputConnectorOn(int x, int y) {
return null;
diff --git a/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerAndroidBridge.java b/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerAndroidBridge.java
new file mode 100644
index 0000000..5047618
--- /dev/null
+++ b/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerAndroidBridge.java
@@ -0,0 +1,51 @@
+package com.codigoparallevar.minicards.bridge;
+
+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;
+
+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;
+ }
+
+ // 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(Runnable onReady, Runnable onComplete) {
+ ProgramakerBridgeConfiguration configuration = new ProgramakerBridgeConfiguration(
+ ProgramakerAndroidBridge.GetBridgeName(this.ctx),
+ new ConfigManager(this.ctx),
+ DefaultAndroidBlocks.GetBuilder(this.ctx).Build()
+ );
+
+ this.bridgeRunner = new ProgramakerBridge(this.bridgeId, this.userId, configuration, onReady, onComplete);
+ this.bridgeRunner.run();
+ }
+
+ public void stop() {
+ bridgeRunner.stop();
+ }
+}
diff --git a/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerBridgeService.java b/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerBridgeService.java
new file mode 100644
index 0000000..3fcbfa5
--- /dev/null
+++ b/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerBridgeService.java
@@ -0,0 +1,213 @@
+package com.codigoparallevar.minicards.bridge;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+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 androidx.core.app.NotificationCompat;
+
+import com.codigoparallevar.minicards.ConfigManager;
+import com.codigoparallevar.minicards.R;
+import com.codigoparallevar.minicards.types.functional.Tuple2;
+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";
+ public static String BridgeUserLedsNotificationChannel = "PROGRAMAKER_BRIDGE_USER_LEDS_NOTIFICATION";
+ public static CharSequence BridgeUserLedsNotificationChannelName = "User LEDs";
+
+ private static String BridgeStatusNotificationChannel = "PROGRAMAKER_BRIDGE_STATUS_NOTIFICATION";
+ private static CharSequence BridgeStatusNotificationChannelName = "Programaker bridge status";
+
+ private static final int BridgeStatusNotificationId = 9999;
+ public static final String COMMAND_PROGRAMAKER_BRIDGE_STOP = "com.programaker.bridge.commands.stop";
+ public static final String COMMAND_PROGRAMAKER_BRIDGE_START = "com.programaker.bridge.commands.start";
+
+ private static final long WAIT_TIME_BEFORE_RESTART_MILLIS = 10000; // 10s
+ private ProgramakerAndroidBridge bridge = null;
+ private static final String LogTag = "PM BridgeService";
+ private boolean stopped = false;
+
+ @Override
+ public void onCreate() {
+ setBridgeStatusNotification(getString(R.string.bridge_service_not_started), true);
+ }
+
+ private void setBridgeStatusNotification(String title, boolean stopped) {
+ NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ assert notificationManager != null;
+
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ NotificationChannel channel = new NotificationChannel(
+ ProgramakerBridgeService.BridgeStatusNotificationChannel,
+ ProgramakerBridgeService.BridgeStatusNotificationChannelName,
+ NotificationManager.IMPORTANCE_DEFAULT);
+ notificationManager.createNotificationChannel(channel);
+ }
+
+ Intent stopIntent = new Intent(this, ProgramakerBridgeService.class);
+ stopIntent.setAction(COMMAND_PROGRAMAKER_BRIDGE_STOP);
+ PendingIntent stopPendingIntent =
+ PendingIntent.getService(this, 0, stopIntent, 0);
+
+ Intent startIntent = new Intent(this, ProgramakerBridgeService.class);
+ startIntent.setAction(COMMAND_PROGRAMAKER_BRIDGE_START);
+ PendingIntent startPendingIntent =
+ PendingIntent.getService(this, 0, startIntent, 0);
+
+ NotificationCompat.Builder builder = new NotificationCompat
+ .Builder(this, ProgramakerBridgeService.BridgeStatusNotificationChannel)
+ .setContentTitle(title)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT);
+
+
+ if (stopped) {
+ builder.addAction(R.drawable.ic_start, getString(R.string.start_bridge), startPendingIntent)
+ .setSmallIcon(R.drawable.ic_vector_stopped_icon);
+ }
+ else {
+ builder.addAction(R.drawable.ic_cancel, getString(R.string.stop_bridge), stopPendingIntent)
+ .setSmallIcon(R.drawable.ic_vector_icon);
+ }
+
+ Notification notification = builder.build();
+
+ if (stopped) {
+ notificationManager.notify(BridgeStatusNotificationId, notification);
+ }
+ else {
+ this.startForeground(BridgeStatusNotificationId, notification);
+ }
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ String action = intent.getAction();
+ if (action == null) {
+ action = "";
+ }
+
+ if (action.equals(COMMAND_PROGRAMAKER_BRIDGE_STOP)) {
+ this.stopSelf();
+ }
+ else {
+ connectBridge();
+ }
+
+ // If we get killed, after returning from here, restart
+ return START_STICKY;
+ }
+
+ private void connectBridge() {
+ stopped = false;
+ ConfigManager config = new ConfigManager(this);
+
+ String token = config.getToken();
+ if (token == null) {
+ Toast.makeText(this, "Cannot start bridge", Toast.LENGTH_SHORT).show();
+ Log.e(LogTag, "Cannot start bridge: Token is null");
+ this.stopSelf();
+ }
+
+ if (ProgramakerBridgeService.this.bridge != null) {
+ // Toast.makeText(this, "Bridge already started", Toast.LENGTH_SHORT).show();
+ Log.w(LogTag, "Bridge already started (not null)");
+ }
+
+ // Start up the thread running the service. Note that we create a
+ // separate thread because the service normally runs in the process's
+ // main thread, which we don't want to block. We also make it
+ // background priority so CPU-intensive work doesn't disrupt our UI.
+ Thread thread = new Thread(() -> {
+ try {
+ ProgramakerApi api = new ProgramakerApi();
+ 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(
+ () -> { // On ready
+ setBridgeStatusNotification(getString(R.string.bridge_service_online), false);
+ 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);
+ ProgramakerBridgeService.this.stopSelf();
+ }
+ )
+ );
+ }
+ },
+ () -> { // On completed
+ ProgramakerBridgeService.this.bridge = null;
+ onBridgeFailedAfterConnected();
+ });
+ }
+ catch (Throwable ex) {
+ Log.e(LogTag, "Error on bridge", ex);
+ ProgramakerBridgeService.this.bridge = null;
+ }
+ }, "ServiceStartArguments");
+ thread.setPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+ setBridgeStatusNotification(getString(R.string.bridge_service_starting), false);
+ thread.start();
+ }
+
+ private void onBridgeFailedAfterConnected() {
+ if (!stopped) {
+ Log.e(LogTag, "Bridge stopped after connected. Waiting 10s then restarting");
+ setBridgeStatusNotification(getString(R.string.bridge_service_failed_restarting), false);
+ try {
+ Thread.sleep(WAIT_TIME_BEFORE_RESTART_MILLIS);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ connectBridge();
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ // We don't provide binding, so return null
+ return null;
+ }
+
+ @Override
+ public void onDestroy() {
+ stopped = true;
+ ProgramakerAndroidBridge bridge = this.bridge;
+ if (bridge != null) {
+ bridge.stop();
+ }
+ setBridgeStatusNotification(getString(R.string.bridge_service_failed_stopping), true);
+ }
+}
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..a6c43bf
--- /dev/null
+++ b/app/src/main/java/com/codigoparallevar/minicards/bridge/blocks/DefaultAndroidBlocks.java
@@ -0,0 +1,138 @@
+package com.codigoparallevar.minicards.bridge.blocks;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.graphics.Color;
+import android.os.Build;
+
+import androidx.core.app.NotificationCompat;
+
+import com.codigoparallevar.minicards.R;
+import com.codigoparallevar.minicards.bridge.ProgramakerBridgeService;
+import com.codigoparallevar.minicards.types.functional.Box;
+
+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();
+ int ledsNotificationId = notificationRandom.nextInt();
+ Box currentLedArg = new Box<>(0x00000000);
+
+ NotificationChannel channelPreparation = null;
+ NotificationChannel ledsChannelPreparation = null;
+
+ String notificationChannelId = ProgramakerBridgeService.BridgeUserNotificationChannel;
+ String ledsChannelId = ProgramakerBridgeService.BridgeUserLedsNotificationChannel;
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ channelPreparation = new NotificationChannel(
+ ProgramakerBridgeService.BridgeUserNotificationChannel,
+ ProgramakerBridgeService.BridgeUserNotificationChannelName,
+ NotificationManager.IMPORTANCE_DEFAULT);
+ notificationManager.createNotificationChannel(channelPreparation);
+
+ ledsChannelPreparation = new NotificationChannel(
+ ledsChannelId,
+ ProgramakerBridgeService.BridgeUserLedsNotificationChannelName,
+ NotificationManager.IMPORTANCE_DEFAULT);
+
+ ledsChannelPreparation.enableLights(true);
+
+ notificationManager.createNotificationChannel(ledsChannelPreparation);
+ }
+
+ final NotificationChannel notificationChannel = channelPreparation;
+ final NotificationChannel ledsChannel = ledsChannelPreparation;
+ 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();
+ }
+ )
+ // Leds
+ .addOperation(
+ "leds_turn_on",
+ "Set leds to (r:%1,g:%2,b:%3)",
+ new LinkedList() {{
+ add(new BlockArgumentDefinition(BlockArgumentDefinition.Type.INT, "255"));
+ add(new BlockArgumentDefinition(BlockArgumentDefinition.Type.INT, "255"));
+ add(new BlockArgumentDefinition(BlockArgumentDefinition.Type.INT, "255"));
+ }},
+ params -> {
+ if (params.size() != 3) {
+ throw new Exception("Expected three (3) arguments, found " + params.size());
+ }
+ String rStr = params.get(0).toString();
+ String gStr = params.get(1).toString();
+ String bStr = params.get(2).toString();
+
+ int r = Math.min(255, Math.max(0, Integer.parseInt(rStr)));
+ int g = Math.min(255, Math.max(0, Integer.parseInt(gStr)));
+ int b = Math.min(255, Math.max(0, Integer.parseInt(bStr)));
+
+ int ledColor = 0x7f * (1<<24) + r * (1 << 16) + g * (1 << 8) + b;
+
+ Notification notif = new NotificationCompat
+ .Builder(ctx, ledsChannelId)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setContentTitle(ctx.getString(R.string.leds_notification))
+ .setSmallIcon(R.drawable.ic_center_focus_weak_black) // TODO: Change icon
+ .setLights(Color.RED, 0, 0)
+ .build();
+
+ notif.ledARGB = Color.RED;
+ notif.ledOnMS = 300;
+ notif.ledOffMS = 300;
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ ledsChannel.setLightColor(Color.RED);
+ }
+
+ notificationManager.notify(ledsNotificationId, notif);
+ }
+ )
+ ;
+ }
+}
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/parts/ProgramakerCustomBlockPart.java b/app/src/main/java/com/codigoparallevar/minicards/parts/ProgramakerCustomBlockPart.java
new file mode 100644
index 0000000..1749c1a
--- /dev/null
+++ b/app/src/main/java/com/codigoparallevar/minicards/parts/ProgramakerCustomBlockPart.java
@@ -0,0 +1,609 @@
+package com.codigoparallevar.minicards.parts;
+
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import com.codigoparallevar.minicards.ScrolledCanvas;
+import com.codigoparallevar.minicards.parts.connectors.AnyRoundInputConnector;
+import com.codigoparallevar.minicards.parts.connectors.ConnectorTypeInfo;
+import com.codigoparallevar.minicards.parts.connectors.RoundOutputConnector;
+import com.codigoparallevar.minicards.types.Moveable;
+import com.codigoparallevar.minicards.types.Part;
+import com.codigoparallevar.minicards.types.PartConnection;
+import com.codigoparallevar.minicards.types.PartGrid;
+import com.codigoparallevar.minicards.types.connectors.input.InputConnector;
+import com.codigoparallevar.minicards.types.connectors.output.OutputConnector;
+import com.codigoparallevar.minicards.types.functional.Tuple2;
+import com.codigoparallevar.minicards.types.wireData.Signal;
+import com.codigoparallevar.minicards.types.wireData.WireDataType;
+import com.codigoparallevar.minicards.ui_helpers.DoAsync;
+import com.programaker.api.ProgramakerApi;
+import com.programaker.api.data.ProgramakerCustomBlock;
+import com.programaker.api.data.ProgramakerCustomBlockArgument;
+import com.programaker.api.data.ProgramakerCustomBlockSaveTo;
+import com.programaker.api.data.ProgramakerFunctionCallResult;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+public class ProgramakerCustomBlockPart implements Part {
+ private static final int WIDTH_PADDING = 50;
+ private static final int HEIGHT_PADDING = 50;
+ private static final int IO_RADIUS = 50;
+ private static final int IO_PADDING = 20;
+ private static final String LogTag = "PM Custom block part";
+
+ private List> inputConnectors = null;
+ private List> outputConnectors = null;
+
+ private final PartGrid _partGrid;
+ private final String _id;
+ private final ProgramakerCustomBlock _block;
+
+ private int _left;
+ private int _top;
+ private int width = 100;
+ private int height = 100;
+ private String token = null;
+ private Object[] lastValues;
+ private boolean active = true;
+ private Tuple2 saveToOutput;
+ private RoundOutputConnector pulseOutput;
+
+
+ public ProgramakerCustomBlockPart(String id, PartGrid grid, Tuple2 center, ProgramakerCustomBlock block) {
+ this._id = id;
+ this._partGrid = grid;
+ this._block = block;
+
+ this.updateWidthHeight();
+
+ this._left = center.item1 - width / 2;
+ this._top = center.item2 - height / 2;
+ this.initialize();
+ }
+
+ private ProgramakerCustomBlockPart(String id, PartGrid grid, int left, int top, ProgramakerCustomBlock block) {
+ this._id = id;
+ this._partGrid = grid;
+ this._block = block;
+
+ this.updateWidthHeight();
+
+ this._left = left;
+ this._top = top;
+ this.initialize();
+ }
+
+ public ProgramakerCustomBlockPart(PartGrid grid, Tuple2 center, ProgramakerCustomBlock block) {
+ this(UUID.randomUUID().toString(), grid, center, block);
+ }
+
+ @Override
+ public int get_left() {
+ return _left;
+ }
+
+ @Override
+ public int get_right() {
+ return _left + width;
+ }
+
+ @Override
+ public int get_top() {
+ return _top;
+ }
+
+ @Override
+ public int get_bottom() {
+ return _top + height;
+ }
+
+ private Paint getTextPaint() {
+ Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
+ p.setColor(Color.BLACK);
+ p.setTextSize(50);
+ return p;
+ }
+
+ private void updateWidthHeight() {
+ Paint p = getTextPaint();
+ String message = getMessage();
+ Rect bounds = new Rect();
+ p.getTextBounds(message, 0, message.length(), bounds);
+
+ this.height = bounds.height() + HEIGHT_PADDING * 2;
+ this.width = bounds.width() + WIDTH_PADDING * 2;
+ }
+
+ private void initialize() {
+ this.updatePorts();
+ lastValues = new Object[this.inputConnectors.size()];
+ }
+
+ private void updatePorts() {
+ final String type = this._block.getBlock_type() == null ? "" : this._block.getBlock_type();
+ final boolean has_pulse_input = type.equals("operation");
+ final boolean has_pulse_output = has_pulse_input || type.equals("trigger");
+ final boolean hasImplicitOutput = type.equals("getter");
+
+ final List> inputs = new LinkedList<>();
+ final List> outputs = new LinkedList<>();
+
+ RoundOutputConnector pulseOutput = null;
+
+ // Add pulses
+ if (has_pulse_input) {
+ inputs.add(new Tuple2<>(new ConnectorTypeInfo(ConnectorTypeInfo.Type.PULSE),
+ new AnyRoundInputConnector(this, 0, 0,
+ IO_RADIUS))
+ );
+ }
+ if (has_pulse_output) {
+ pulseOutput = new RoundOutputConnector(this, this._partGrid, 0, 0,
+ IO_RADIUS);
+ outputs.add(new Tuple2<>(new ConnectorTypeInfo(ConnectorTypeInfo.Type.PULSE),
+ pulseOutput)
+ );
+ }
+
+ // Add block IO
+ ProgramakerCustomBlockArgument savedTo = null;
+
+ if (_block.getArguments() != null) {
+ ProgramakerCustomBlockSaveTo saveTo = _block.getSave_to();
+ int skip = -1;
+
+ if (saveTo != null) {
+ if (saveTo.getType() != null
+ && saveTo.getType().equals("argument")) {
+ skip = saveTo.getIndex();
+ }
+ else {
+ Log.w(LogTag, "Unknown save-to type=" + saveTo.getType());
+ }
+ }
+
+ int index = -1;
+ for (ProgramakerCustomBlockArgument arg : _block.getArguments()) {
+ index++;
+ if (skip == index) {
+ savedTo = arg;
+ }
+ else {
+ inputs.add(new Tuple2<>(ConnectorTypeInfo.FromTypeName(arg.getType()),
+ new AnyRoundInputConnector(this, 0, 0, IO_RADIUS)));
+ }
+ }
+ }
+
+ Tuple2 saveToOutput = null;
+
+ if (savedTo != null) {
+ saveToOutput = new Tuple2<>(ConnectorTypeInfo.FromTypeName(savedTo.getType()),
+ new RoundOutputConnector(this, this._partGrid, 0, 0, IO_RADIUS));
+ outputs.add(saveToOutput);
+ }
+
+ if (hasImplicitOutput) {
+ outputs.add(new Tuple2<>(ConnectorTypeInfo.FromTypeName(_block.getBlock_result_type()),
+ new RoundOutputConnector(this, this._partGrid, 0, 0, IO_RADIUS)));
+ }
+
+ // Tie everything
+ inputConnectors = inputs;
+ outputConnectors = outputs;
+ this.saveToOutput = saveToOutput;
+ this.pulseOutput = pulseOutput;
+
+ this.updatePortPositions();
+ }
+
+ private void updatePortPositions() {
+ {
+ // Update inputs
+ int y = get_top();
+ int x = get_left() + IO_PADDING;
+ for (Tuple2 entry : inputConnectors) {
+ InputConnector input = entry.item2;
+ int new_x = x + IO_PADDING + IO_RADIUS / 2;
+ input.updatePosition(new_x, y);
+ x = x + IO_RADIUS * 2 + IO_PADDING * 2;
+ }
+ }
+ {
+ // Update outputs
+ int y = get_bottom();
+ int x = get_left() + IO_PADDING;
+ for (Tuple2 entry : outputConnectors) {
+ OutputConnector output = entry.item2;
+ int new_x = x + IO_PADDING + IO_RADIUS / 2;
+ output.updatePosition(new_x, y);
+ x = x + IO_RADIUS * 2 + IO_PADDING * 2;
+ }
+ }
+ }
+
+ private String getMessage() {
+ return this._block.getMessage();
+ }
+
+ @Override
+ public void touched() {
+ Log.i(LogTag, "Part touched (block_fun=" + this._block.getFunction_name() + ")");
+ }
+
+ @Override
+ public List getInputConnectors() {
+ List result = new ArrayList<>(inputConnectors.size());
+ for (Tuple2 entry : inputConnectors) {
+ result.add(entry.item2);
+ }
+
+ return result;
+ }
+
+ @Override
+ public List getOutputConnectors() {
+ List result = new ArrayList<>(outputConnectors.size());
+ for (Tuple2 entry : outputConnectors) {
+ result.add(entry.item2);
+ }
+
+ return result;
+ }
+
+ @Override
+ public JSONObject serialize() throws JSONException {
+ JSONObject serialized = new JSONObject();
+
+ serialized.put("id", _id);
+ serialized.put("left", _left);
+ serialized.put("top", _top);
+
+ JSONObject blockData = _block.serialize();
+ serialized.put("block", blockData);
+
+ serialized.put("on_string_output_connector", serializeConnectionEndpoints());
+
+ return serialized;
+ }
+
+ private JSONArray serializeConnectionEndpoints() {
+ JSONArray serializedData = new JSONArray();
+
+ for (OutputConnector output : getOutputConnectors()) {
+ JSONArray elements = new JSONArray();
+
+ for (Tuple2 endpoint : (List>) output.getConnectionEndpoints()) {
+ elements.put(PartConnection.serializeToJson(endpoint.item1, endpoint.item2));
+ }
+
+ serializedData.put(elements);
+ }
+ return serializedData;
+ }
+
+ public static Tuple2> deserialize(PartGrid grid, JSONObject data) throws JSONException {
+ String id = data.getString("id");
+ int left = data.getInt("left");
+ int top = data.getInt("top");
+
+ JSONObject el = data.getJSONObject("block");
+ ProgramakerCustomBlock block = ProgramakerCustomBlock.deserialize(el);
+ ProgramakerCustomBlockPart part = new ProgramakerCustomBlockPart(id, grid, left, top, block);
+
+ List connections = new LinkedList<>();
+
+ JSONArray allConnectorOuts = data.optJSONArray("on_string_output_connector");
+ if (allConnectorOuts == null) {
+ allConnectorOuts = new JSONArray();
+ }
+
+ for (int i = 0; i < allConnectorOuts.length(); i++) {
+ JSONArray connectorOuts = allConnectorOuts.getJSONArray(i);
+ Tuple2 connector = part.outputConnectors.get(i);
+ for (int j = 0; j < connectorOuts.length(); j++) {
+ connections.add(PartConnection.deserialize(
+ connector.item2, connectorOuts.getJSONObject(j)));
+ }
+ }
+
+ return new Tuple2<>(part, connections);
+ }
+
+
+ @Override
+ public void send(InputConnector inputConnector, WireDataType signal) {
+ // Log.i(LogTag, "Received signal from Input="+inputConnector);
+ ConnectorTypeInfo info = getConnectorInfo(inputConnector);
+ if (info == null) {
+ Log.e(LogTag, "Unknown connector" + inputConnector);
+ return;
+ }
+
+ if (info.get_type() == ConnectorTypeInfo.Type.PULSE) {
+ wrappedRunOperation();
+ }
+ else {
+ int index = getConnectorIndex(inputConnector);
+ if (index < 0) {
+ return;
+ }
+ this.lastValues[index] = signal.get();
+ }
+ }
+
+ private void wrappedRunOperation() {
+ Log.d(LogTag, "Running block function=" + this._block.getFunction_name());
+ String token = this.prepareStart();
+
+ if (token != null) {
+ try {
+ new DoAsync().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
+ new Tuple2<>(() -> {
+ ProgramakerCustomBlockPart.this.runBlockOperation();
+
+ ProgramakerCustomBlockPart.this.freeBlock(token);
+ }, ex -> {
+ Log.w(LogTag, "Error executing function=" + this._block.getFunction_name()
+ + "; Error=" + ex, ex);
+ ProgramakerCustomBlockPart.this.freeBlock(token);
+ }));
+ }
+ catch (Exception ex) {
+ Log.e(LogTag, "Error executing function=" + this._block.getFunction_name()
+ + "; Error=" + ex, ex);
+ this.freeBlock(token);
+ }
+ } else {
+ // An execution is in progress, so nothing is done
+ }
+ }
+
+ private void runBlockOperation() {
+ if (!this.active) {
+ Log.w(LogTag, "Trying to run inactive block function=" + this._block.getFunction_name());
+ return;
+ }
+
+ ProgramakerApi api = this._partGrid.getApi();
+ List arguments = new LinkedList<>();
+
+ int index = -1;
+ for (Tuple2 entry : inputConnectors) {
+ index++;
+ if (entry.item1.get_type() == ConnectorTypeInfo.Type.PULSE) {
+ continue;
+ }
+
+ arguments.add(lastValues[index].toString()); // TODO: Do proper type formatting
+ }
+
+ ProgramakerFunctionCallResult result = api.callBlock(this._block, arguments);
+ Log.d(LogTag, "Execution result="+result.getResult());
+
+ onExecutionCompleted(result);
+ }
+
+ private void onExecutionCompleted(ProgramakerFunctionCallResult result) {
+ Tuple2 savedTo = this.saveToOutput;
+ if (savedTo != null) {
+ // TODO: Fix output typing
+ // savedTo.item2.send((WireDataType