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..ccc5fce 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 - buildToolsVersion "27.0.1" + compileSdkVersion 29 + buildToolsVersion '29.0.2' 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..f395f2c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,11 +2,25 @@ - + + + + + + + + + + + + + + + + + + @@ -26,10 +46,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..3c1adf9 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/CanvasView.java +++ b/app/src/main/java/com/codigoparallevar/minicards/CanvasView.java @@ -4,27 +4,33 @@ 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.parts.connectors.ProgramakerCustomBlockInputConnector; 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.Wiring.SignalWire; +import com.codigoparallevar.minicards.types.connectors.Wiring.Wire; import com.codigoparallevar.minicards.types.connectors.input.AnyInputConnector; import com.codigoparallevar.minicards.types.connectors.input.BooleanInputConnector; +import com.codigoparallevar.minicards.types.connectors.input.ImageInputConnector; 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 +38,8 @@ 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 { + private static final String LogTag = "Canvas view"; @NonNull List parts = new ArrayList<>(); @@ -51,6 +58,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; @@ -63,6 +71,7 @@ class CanvasView extends View implements PartGrid { @Nullable private Tuple2 _mouseDownPoint = null; private int cardBackgroundColor; + private SignalListenerManager listenerManager = null; public CanvasView(Context context) { super(context); @@ -88,7 +97,7 @@ class CanvasView extends View implements PartGrid { Map partsById = buildPartsById(); for (PartConnection connection : connections){ if (!partsById.containsKey(connection.inputPartId)){ - Log.e("Canvas view", "Key '" + connection.inputPartId + Log.e(LogTag, "Key '" + connection.inputPartId + "' not found on deserialization"); continue; } @@ -97,14 +106,14 @@ class CanvasView extends View implements PartGrid { InputConnector inputConnector = inputPart.getConnectorWithId(connection.inputConnectorId); if (inputConnector == null){ - Log.e("Canvas view", "Connector ID '" + connection.inputConnectorId + Log.e(LogTag, "Connector ID '" + connection.inputConnectorId + "' not found on deserialization"); continue; } OutputConnector outputConnector = connection.outputConnector; if (inputConnector == null){ - Log.e("Canvas view", "Connector not found on connection"); + Log.e(LogTag, "Connector not found on connection"); continue; } @@ -116,7 +125,7 @@ class CanvasView extends View implements PartGrid { outputConnector.connectTo(inputConnector); } catch (ClassCastException e) { - Log.e("Minicards - Canvas view", "Malformed connection", e); + Log.e(LogTag, "Malformed connection", e); } } } @@ -125,7 +134,7 @@ class CanvasView extends View implements PartGrid { Map partsById = new HashMap<>(parts.size()); for (Part part : parts) { partsById.put(part.get_id(), part); - Log.w("CanvasView", "Added part ID: " + part.get_id() + " - " + part); + Log.w(LogTag, "Added part ID: " + part.get_id() + " - " + part); } return partsById; @@ -142,7 +151,7 @@ class CanvasView extends View implements PartGrid { part.draw(scrolledCanvas, _devMode); } - Log.d("Render time", System.currentTimeMillis() - renderStartTime + "ms"); + Log.v(LogTag, "Render time: " + (System.currentTimeMillis() - renderStartTime) + "ms"); } private void drawBackground(ScrolledCanvas canvas) { @@ -177,7 +186,15 @@ class CanvasView extends View implements PartGrid { final int xInCanvas = xInScreen + _viewOrigin.item1; final int yInCanvas = yInScreen + _viewOrigin.item2; - switch (event.getAction()){ + int action = event.getAction(); + if ((action == MotionEvent.ACTION_MOVE) + && _devMode + && (selectedPart instanceof Wire)) { + // Wires cannot be moved, so go into "move canvas" mode + selectedPart = null; + } + + switch (action){ case MotionEvent.ACTION_DOWN: { _mouseDownPoint = new Tuple2<>(xInScreen, yInScreen); @@ -186,11 +203,10 @@ class CanvasView extends View implements PartGrid { lastTouchedPosition.to(xInScreen, yInScreen); lastTouchedTime = System.currentTimeMillis(); if (selectedPart == null) { - Log.d("Touched part", "not found"); + Log.d(LogTag, "Touched part not found"); return false; } - - Log.d("Touched part", "Part: " + selectedPart); + Log.d(LogTag, "Touched part FOUND. Part: " + selectedPart); } break; @@ -204,14 +220,28 @@ class CanvasView extends View implements PartGrid { if (selectedPart instanceof Part){ ((Part) selectedPart).touched(); } + else if (selectedPart instanceof Wire) { + // TODO: No drag or long-touch, just show the "cut" option + Log.d(LogTag, "Touched Wire"); + Wire selectedWire = (Wire) selectedPart; + selectedPart = null; + + selectedWire.unlink(); + } + else if (selectedPart instanceof ProgramakerCustomBlockInputConnector) { + ((ProgramakerCustomBlockInputConnector) selectedPart).touched(); + } } } else if (motionMode == MotionMode.Type.LongTouch && _devMode) { + if (selectedPart instanceof Wire) { + // Wires cannot be moved, so go into the + } if (selectedPart != null) { selectedPart.getMoveable().drop(xInCanvas, yInCanvas); if (inDropZone(xInScreen, yInScreen)) { - Log.d("Canvas", "Deleting element" + selectedPart); + Log.d(LogTag, "Deleting element" + selectedPart); parts.remove(selectedPart); selectedPart.unlink(); } @@ -226,7 +256,7 @@ class CanvasView extends View implements PartGrid { try { saveState(); } catch (IOException e) { - Log.w("PartCanvasView", e.getMessage()); + Log.w(LogTag, e.getMessage()); } break; @@ -236,6 +266,7 @@ class CanvasView extends View implements PartGrid { break; } + // Log.v(LogTag, "Moving part="+selectedPart); if (selectedPart == null){ int xMovement = _mouseDownPoint.item1 - xInScreen; int yMovement = _mouseDownPoint.item2 - yInScreen; @@ -246,7 +277,7 @@ class CanvasView extends View implements PartGrid { _mouseDownPoint = new Tuple2(xInScreen, yInScreen); } else { - Log.i("Canvas", "X: " + xInScreen + " Y: " + yInScreen + Log.d(LogTag, "X: " + xInScreen + " Y: " + yInScreen + " in drop zone " + _dropToRemoveZone + " : " + inDropZone(xInScreen, yInScreen)); @@ -268,7 +299,7 @@ class CanvasView extends View implements PartGrid { default: { - Log.d("PartCanvasView", "Unhandled action: " + event.getAction()); + Log.d(LogTag, "Unhandled action: " + event.getAction()); } } @@ -323,15 +354,42 @@ 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; + } + } + } + } + + // Finally, try with the wires + for (int i = parts.size() - 1; i >= 0; i--) { + final Part part = parts.get(i); + List outputConnectors = part.getOutputConnectors(); + if (outputConnectors != null) { + for (OutputConnector outputConnector : outputConnectors) { + List wires = outputConnector.getWires(); + if (wires == null) { + continue; + } + for (Wire wire : wires) { + if (wire.containsPoint(x, y)) { + Log.d(LogTag, "Point in wire " + wire); + return wire; + } + } } } } @@ -339,6 +397,23 @@ 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 + public SignalListenerManager getListenerManager() { + if (listenerManager == null) { + listenerManager = new SignalListenerManager(getApi()); + } + return listenerManager; + } + @Override @Nullable public SignalInputConnector getSignalInputConnectorOn(int x, int y) { @@ -371,11 +446,9 @@ class CanvasView extends View implements PartGrid { @Override @Nullable public BooleanInputConnector getBooleanInputConnectorOn(int x, int y) { - // If no part was found, do the same for connectors for (int i = parts.size() - 1; i >= 0; i--){ final Part part = parts.get(i); - // Then with input ones for (InputConnector inputConnector : part.getInputConnectors()){ BooleanInputConnector booleanInputConnector; if (inputConnector instanceof BooleanInputConnector){ @@ -397,6 +470,25 @@ class CanvasView extends View implements PartGrid { return null; } + @Override + public AnyInputConnector getAnyInputConnectorOn(int x, int y) { + for (int i = parts.size() - 1; i >= 0; i--){ + final Part part = parts.get(i); + + for (InputConnector inputConnector : part.getInputConnectors()){ + if (!(inputConnector instanceof AnyInputConnector)) { + continue; + } + + if (inputConnector.containsPoint(x, y)){ + return (AnyInputConnector) inputConnector; + } + } + } + + return null; + } + @Override @Nullable public StringInputConnector getStringInputConnectorOn(int x, int y) { @@ -426,6 +518,31 @@ class CanvasView extends View implements PartGrid { return null; } + @Override + public ImageInputConnector getImageInputConnectorOn(int x, int y) { + // If no part was found, do the same for connectors + for (int i = parts.size() - 1; i >= 0; i--){ + final Part part = parts.get(i); + + // Then with input ones + for (InputConnector inputConnector : part.getInputConnectors()){ + ImageInputConnector imageInputConnector; + if (inputConnector instanceof ImageInputConnector){ + imageInputConnector = (ImageInputConnector) inputConnector; + } + else { + continue; + } + + if (imageInputConnector.containsPoint(x, y)){ + return imageInputConnector; + } + } + } + + return null; + } + public void addPart(Part part) { parts.add(part); @@ -465,11 +582,8 @@ class CanvasView extends View implements PartGrid { @Override public void update() { - parentActivity.runOnUiThread(new Runnable() { - @Override - public void run() { - CanvasView.this.invalidate(); - } + parentActivity.runOnUiThread(() -> { + CanvasView.this.invalidate(); }); } @@ -490,7 +604,11 @@ class CanvasView extends View implements PartGrid { public void pause() { for (Part part : parts) { - part.resume(); + part.pause(); } } + + public void requestPermission(String permission, Runnable ifAccepted) { + this.parentActivity.requestPermissions(permission, ifAccepted); + } } diff --git a/app/src/main/java/com/codigoparallevar/minicards/CardActivity.java b/app/src/main/java/com/codigoparallevar/minicards/CardActivity.java index 301a64b..8ee0dca 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/CardActivity.java +++ b/app/src/main/java/com/codigoparallevar/minicards/CardActivity.java @@ -2,15 +2,32 @@ 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.content.pm.PackageManager; +import android.os.AsyncTask; +import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.MotionEvent; import android.view.View; +import androidx.annotation.NonNull; +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.HashMap; +import java.util.List; +import java.util.Map; public class CardActivity extends AppCompatActivity { @@ -18,7 +35,10 @@ 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"; + private int permissionRequestLatest = 1; + private Map permissionRequestCallbacks = new HashMap<>(); CanvasView canvasView; com.getbase.floatingactionbutton.AddFloatingActionButton AddPartButton; @@ -34,11 +54,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 +96,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 +140,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 +149,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,13 +237,11 @@ public class CardActivity extends AppCompatActivity { if (canvasView.isDragging()){ devFabMenu.setVisibility(View.GONE); userFabMenu.setVisibility(View.GONE); - removePartFab.setVisibility(View.VISIBLE); - Log.d("Main", "Changing visibility!"); + ((View)removePartFab).setVisibility(View.VISIBLE); } else { this.setDevMode(devMode); - removePartFab.setVisibility(View.GONE); - Log.d("Main", "Now changing visibility!"); + ((View)removePartFab).setVisibility(View.GONE); } canvasView.setDropZone( removePartFab.getX(), removePartFab.getX() + removePartFab.getWidth(), @@ -211,11 +250,54 @@ 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); } context.startActivity(i); } + + public void requestPermissions(String permission, Runnable ifAccepted) { + permissionRequestLatest++; + int request_code = permissionRequestLatest; + permissionRequestCallbacks.put(request_code, ifAccepted); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + this.requestPermissions(new String[]{permission}, request_code); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 && + grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Permission is granted. Continue the action or workflow + // in your app. + if (permissionRequestCallbacks.containsKey(requestCode)) { + Runnable callback = permissionRequestCallbacks.get(requestCode); + permissionRequestCallbacks.remove(requestCode); + callback.run(); + } + } else { + // TODO: + // Explain to the user that the feature is unavailable because + // the features requires a permission that the user has denied. + // At the same time, respect the user's decision. Don't link to + // system settings in an effort to convince the user to change + // their decision. + if (permissionRequestCallbacks.containsKey(requestCode)) { + permissionRequestCallbacks.remove(requestCode); + } + } + } } diff --git a/app/src/main/java/com/codigoparallevar/minicards/CardFile.java b/app/src/main/java/com/codigoparallevar/minicards/CardFile.java index ee796a7..c4e32d9 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/CardFile.java +++ b/app/src/main/java/com/codigoparallevar/minicards/CardFile.java @@ -4,19 +4,23 @@ 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.android.CameraStreamer; 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.samples.Placeholder; import com.codigoparallevar.minicards.parts.strings.ConvertToString; +import com.codigoparallevar.minicards.parts.values.StaticValuePart; +import com.codigoparallevar.minicards.parts.viewers.ImageFrame; 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 +38,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; @@ -237,7 +241,7 @@ public class CardFile { card.addConnections(info.item2); } } catch (JSONException e) { - throw new ErrorLoadingCardException(ErrorLoadingCardException.Reason.UNKNOWN_FORMAT); + throw new ErrorLoadingCardException(ErrorLoadingCardException.Reason.UNKNOWN_FORMAT); } return card; @@ -255,8 +259,8 @@ public class CardFile { return buttonInfo; } - else if (type.equals(Placeholder.class.getName())) { - return new Tuple2<>(Placeholder.deserialize(grid, jsonObject.getJSONObject("_data")), + else if (type.equals(ImageFrame.class.getName())) { + return new Tuple2<>(ImageFrame.deserialize(grid, jsonObject.getJSONObject("_data")), Collections.emptyList()); } else if (type.equals(ColorBox.class.getName())){ @@ -284,6 +288,27 @@ public class CardFile { return convertToStringInfo; } + else if (type.equals(ProgramakerCustomBlockPart.class.getName())){ + Tuple2> customBlockPartInfo = ProgramakerCustomBlockPart.deserialize( + grid, + jsonObject.getJSONObject("_data")); + + return customBlockPartInfo; + } + else if (type.equals(StaticValuePart.class.getName())){ + Tuple2> staticValuePartInfo = StaticValuePart.deserialize( + grid, + jsonObject.getJSONObject("_data")); + + return staticValuePartInfo; + } + else if (type.equals(CameraStreamer.class.getName())){ + Tuple2> staticValuePartInfo = CameraStreamer.deserialize( + grid, + jsonObject.getJSONObject("_data")); + + return staticValuePartInfo; + } 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..ba59713 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/ConfigManager.java @@ -0,0 +1,80 @@ +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_AUTHENTICATION_TOKEN_KEY = "BRIDGE_AUTHENTICATION_TOKEN"; + 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); + } + + public void setBridgeAuthenticationToken(String bridgeAuthToken) { + SharedPreferences preferences = this.context.getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE); + SharedPreferences.Editor edit = preferences.edit(); + + edit.putString(BRIDGE_AUTHENTICATION_TOKEN_KEY, bridgeAuthToken); + edit.commit(); + } + + public String getBridgeAuthenticationToken() { + SharedPreferences preferences = this.context.getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE); + return preferences.getString(BRIDGE_AUTHENTICATION_TOKEN_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..1bc5cad 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/ScrolledCanvas.java +++ b/app/src/main/java/com/codigoparallevar/minicards/ScrolledCanvas.java @@ -1,12 +1,15 @@ package com.codigoparallevar.minicards; +import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; -import android.support.annotation.NonNull; +import android.graphics.RectF; -import com.codigoparallevar.minicards.types.Tuple2; +import androidx.annotation.NonNull; + +import com.codigoparallevar.minicards.types.functional.Tuple2; public class ScrolledCanvas { private final Canvas canvas; @@ -42,7 +45,6 @@ public class ScrolledCanvas { canvas.drawPath(offsetPath, paint); } - public void drawCenteredText(String text, int x, int y, Paint paint) { paint.setTextAlign(Paint.Align.LEFT); Rect r = new Rect(); @@ -56,4 +58,17 @@ public class ScrolledCanvas { public void drawText(String text, int x, int y, Paint paint) { canvas.drawText(text, x - xOrig, y - yOrig, paint); } + + public void drawRoundRect(RectF rect, float rx, float ry, Paint paint) { + rect.offset(-xOrig, -yOrig); + canvas.drawRoundRect(rect, rx, ry, paint); + } + + public void drawBitmap(Bitmap bitmap, Rect rect) { + rect.offset(-xOrig, -yOrig); + canvas.drawBitmap(bitmap, + new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()), + rect, + null); + } } \ No newline at end of file diff --git a/app/src/main/java/com/codigoparallevar/minicards/SignalListenerManager.java b/app/src/main/java/com/codigoparallevar/minicards/SignalListenerManager.java new file mode 100644 index 0000000..97a4fcb --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/SignalListenerManager.java @@ -0,0 +1,117 @@ +package com.codigoparallevar.minicards; + +import android.util.Log; + +import com.codigoparallevar.minicards.types.functional.Tuple2; +import com.programaker.api.ProgramakerApi; +import com.programaker.api.ProgramakerListeningChannel; +import com.programaker.api.ProgramakerSignalListener; + +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public class SignalListenerManager implements ProgramakerSignalListener { + private final ProgramakerApi api; + private final Map, List> channelToListener = new LinkedHashMap<>(); + private final Map>> listenerChannels = new LinkedHashMap<>(); + private final Map, ProgramakerListeningChannel> idToChannel = new LinkedHashMap<>(); + private long RECONNECT_SLEEP_TIME = 2000; // 2seconds + private final static String LogTag = "Signal Listener Manager"; + + public SignalListenerManager(ProgramakerApi api) { + this.api = api; + } + + public void registerSignalListener(String bridgeId, String key, ProgramakerSignalListener listener) { + Tuple2 id = new Tuple2<>(bridgeId, key); + if (!idToChannel.containsKey(id)) { + // Channel has to be opened + idToChannel.put(id, this.api.openChannelTo(bridgeId, key, this, + () -> { + SignalListenerManager.this.onDisconnect(bridgeId, key); + })); + } + + if (!channelToListener.containsKey(id)) { + channelToListener.put(id, new LinkedList<>()); + } + List listeners = channelToListener.get(id); + listeners.add(listener); + + if (!listenerChannels.containsKey(listener)) { + listenerChannels.put(listener, new LinkedList<>()); + } + listenerChannels.get(listener).add(id); + } + + public void unregisterSignalListener(ProgramakerSignalListener listener) { + List> channels = listenerChannels.get(listener); + listenerChannels.remove(listener); + for (Tuple2 id : channels) { + List remainingListeners = channelToListener.get(id); + + remainingListeners.remove(listener); + if (remainingListeners.size() == 0) { + ProgramakerListeningChannel channel = idToChannel.get(id); + idToChannel.remove(id); + channelToListener.remove(id); + + channel.stop(); + } + } + } + + private void onDisconnect(String bridgeId, String key) { + Tuple2 id = new Tuple2<>(bridgeId, key); + + // Check that there is still someone listening + if (!this.channelToListener.containsKey(id)) { + return; + } + if (this.channelToListener.get(id).size() == 0) { + this.channelToListener.remove(id); + return; + } + + // On disconnect disable the connection, wait 2 seconds and retry + idToChannel.put(id, null); + + new Thread(() -> { + try { + Thread.sleep(RECONNECT_SLEEP_TIME); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + idToChannel.put(id, this.api.openChannelTo(bridgeId, key, this, + () -> { + SignalListenerManager.this.onDisconnect(bridgeId, key); + })); + }).start(); + } + + @Override + public void onNewSignal(@NotNull String bridgeId, @NotNull String key, @NotNull HashMap signal) { + Tuple2 id = new Tuple2<>(bridgeId, key); + + if (!channelToListener.containsKey(id)) { + Log.e(LogTag, "Got signal to unlistened channel (bridgeId=" + bridgeId + ",key=" + key + ")"); + return; + } + + for (ProgramakerSignalListener listener : channelToListener.get(id)) { + try { + listener.onNewSignal(bridgeId, key, signal); + } + catch (Exception ex) { + Log.e(LogTag, "Error passing message (bridge=" + bridgeId + ",key" + key + + ") to " + listener, ex); + } + } + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/StubPartGrid.java b/app/src/main/java/com/codigoparallevar/minicards/StubPartGrid.java index fab7739..2d7ced6 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/StubPartGrid.java +++ b/app/src/main/java/com/codigoparallevar/minicards/StubPartGrid.java @@ -2,10 +2,13 @@ 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.connectors.input.AnyInputConnector; +import com.codigoparallevar.minicards.types.connectors.input.ImageInputConnector; +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 +16,16 @@ class StubPartGrid implements PartGrid { return null; } + @Override + public ProgramakerApi getApi() { + return null; + } + + @Override + public SignalListenerManager getListenerManager() { + return null; + } + @Override public SignalInputConnector getSignalInputConnectorOn(int x, int y) { return null; @@ -23,6 +36,21 @@ class StubPartGrid implements PartGrid { return null; } + @Override + public AnyInputConnector getAnyInputConnectorOn(int x, int y) { + return null; + } + + @Override + public StringInputConnector getStringInputConnectorOn(int xEnd, int yEnd) { + return null; + } + + @Override + public ImageInputConnector getImageInputConnectorOn(int xEnd, int yEnd) { + return null; + } + @Override public Tuple2 getCenteredOn() { return null; @@ -32,8 +60,4 @@ class StubPartGrid implements PartGrid { public void update() { } - @Override - public StringInputConnector getStringInputConnectorOn(int xEnd, int yEnd) { - 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..46b3b92 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerAndroidBridge.java @@ -0,0 +1,59 @@ +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.codigoparallevar.minicards.bridge.helpers.StatusStreamer; +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; + private StatusStreamer statusStreamer = null; + + // Static + public static ProgramakerAndroidBridge configure(Context ctx, String userId, String bridgeId, String bridgeToken) { + return new ProgramakerAndroidBridge(ctx, userId, bridgeId, bridgeToken); + } + + 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 final String bridgeToken; + + private ProgramakerAndroidBridge(Context ctx, String userId, String bridgeId, String bridgeToken) { + this.ctx = ctx; + this.userId = userId; + this.bridgeId = bridgeId; + this.bridgeToken = bridgeToken; + } + + 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, this.bridgeToken, configuration, onReady, onComplete); + this.bridgeRunner.run(); + + this.statusStreamer = new StatusStreamer(this.ctx, this.bridgeRunner); + this.statusStreamer.run(); + } + + public void stop() { + bridgeRunner.stop(); + this.statusStreamer.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..8921d90 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/bridge/ProgramakerBridgeService.java @@ -0,0 +1,247 @@ +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; + +enum ServiceState { + LOADING, + RUNNING, + STOPPED, +} + +public class ProgramakerBridgeService extends Service { + public static final String BridgeUserNotificationChannel = "PROGRAMAKER_BRIDGE_USER_NOTIFICATION"; + public static final CharSequence BridgeUserNotificationChannelName = "User notifications"; + private static final String AUTOGENERATED_TOKEN_NAME = "Auto-generated"; + public static String BridgeUserVibrationNotificationChannel = "PROGRAMAKER_BRIDGE_USER_VIBRATION_NOTIFICATIONS"; + public static CharSequence BridgeUserVibrationNotificationChannelName = "User-triggered vibration"; + + 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), ServiceState.LOADING, null); + } + + private void setBridgeStatusNotification(String title, ServiceState state, String description) { + 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); + channel.enableVibration(false); + 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 (state == ServiceState.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); + + if (state == ServiceState.RUNNING) { + builder.setSmallIcon(R.drawable.ic_vector_icon); + } + else { + builder.setSmallIcon(R.drawable.ic_vector_icon_loading); + } + } + + if (description != null) { + builder.setContentText(description); + } + + 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 = null; + if (intent != null) { // Apparently this (intent=null) can happen... + 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); + } + + String bridgeAuthCheck = config.getBridgeAuthenticationToken(); + if (bridgeAuthCheck == null) { + bridgeAuthCheck = api.createBridgeAuthenticationToken(bridgeIdCheck, ProgramakerBridgeService.AUTOGENERATED_TOKEN_NAME); + config.setBridgeAuthenticationToken(bridgeAuthCheck); + } + + final String bridgeId = bridgeIdCheck; + final String bridgeAuth = bridgeAuthCheck; + + ProgramakerBridgeService.this.bridge = ProgramakerAndroidBridge.configure( + this, + userId, + bridgeId, + bridgeAuth); + ProgramakerBridgeService.this.bridge.start( + () -> { // On ready + setBridgeStatusNotification(getString(R.string.bridge_service_online), ServiceState.RUNNING, null); + 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 + 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, 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, null); + 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), ServiceState.STOPPED, null); + } +} 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..c8e911c --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/bridge/blocks/BridgeBlockListBuilder.java @@ -0,0 +1,54 @@ +package com.codigoparallevar.minicards.bridge.blocks; + +import com.codigoparallevar.minicards.types.functional.Function; +import com.programaker.bridge.ProgramakerBridgeConfigurationBlock; + +import java.util.LinkedList; +import java.util.List; + +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, + Function, Void> operation) { + return this.addOperation(new OperationBlockDefinition(id, message, arguments, operation)); + } + + private BridgeBlockListBuilder addOperation(ProgramakerBridgeConfigurationBlock block) { + this.blockList.add(block); + return this; + } + + public BridgeBlockListBuilder addGetter(String id, String message, + List arguments, + Function, ?> operation) { + return this.addGetter(new GetterBlockDefinition(id, message, arguments, operation)); + } + + private BridgeBlockListBuilder addGetter(ProgramakerBridgeConfigurationBlock block) { + this.blockList.add(block); + return this; + } + + public BridgeBlockListBuilder addTrigger(String id, String message, + List arguments, + String key) { + return this.addTrigger(new TriggerBlockDefinition(id, message, arguments, key)); + } + + private BridgeBlockListBuilder addTrigger(TriggerBlockDefinition 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..e594cfe --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/bridge/blocks/DefaultAndroidBlocks.java @@ -0,0 +1,216 @@ +package com.codigoparallevar.minicards.bridge.blocks; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.net.wifi.SupplicantState; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Build; +import android.os.VibrationEffect; +import android.os.Vibrator; + +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 { + + private static long VIBRATION_REST_TIME = 1000; // Ms + private static long VIBRATION_ACTIVE_TIME = 500; // Ms + + // This means that will take at most ~15 seconds + private static int MAX_VIBRATION_CYCLES = 10; + + public static BridgeBlockListBuilder GetBuilder(Context ctx) { + // System services + NotificationManager notificationManager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE); + Vibrator vibrator = (Vibrator) ctx.getSystemService(Context.VIBRATOR_SERVICE); + + assert notificationManager != null; + assert vibrator != null; + + Random notificationRandom = new Random(); + + 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); + + NotificationChannel vibrationChannel = new NotificationChannel( + ProgramakerBridgeService.BridgeUserVibrationNotificationChannel, + ProgramakerBridgeService.BridgeUserVibrationNotificationChannelName, + NotificationManager.IMPORTANCE_DEFAULT); + + // Vibration is manually handled for Android O (or superior) devices + vibrationChannel.setVibrationPattern(null); + notificationManager.createNotificationChannel(vibrationChannel); + } + + // OPERATIONS + BridgeBlockListBuilder builder = 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) == null ? null : params.get(0).toString(); + String description = params.get(1) == null ? null : params.get(1).toString(); + + NotificationCompat.Builder notifBuilder = new NotificationCompat + .Builder(ctx, ProgramakerBridgeService.BridgeUserNotificationChannel) + .setSmallIcon(R.drawable.ic_vector_icon_user_triggered) + .setPriority(NotificationCompat.PRIORITY_DEFAULT); + + if (title != null) { + notifBuilder.setContentTitle(title); + } + if (description != null) { + notifBuilder.setContentText(description); + } + + Notification notif = notifBuilder.build(); + + int notificationId = notificationRandom.nextInt(); + notificationManager.notify(notificationId, notif); + + return null; + } + ) + .addOperation( + "notifications_clear", + "Clear notifications", + Collections.emptyList(), + (List params) -> { + notificationManager.cancelAll(); + + return null; + } + ) + .addOperation( + "vibration_start", + "Vibrate %1 times", + new LinkedList() {{ + add(new BlockArgumentDefinition(BlockArgumentDefinition.Type.INT, "3")); + }}, + (List params) -> { + if (params.size() != 1) { + throw new Exception("Expected one (1) argument1, found " + params.size()); + } + + int userTimes = Integer.parseInt(params.get(0).toString()); + final int times = Math.max(1, Math.min(MAX_VIBRATION_CYCLES, userTimes)); + + long[] pattern = new long[times * 2]; + for (int i = 0; i < times; i++) { + pattern[i * 2] = VIBRATION_ACTIVE_TIME; + pattern[i * 2 + 1] = VIBRATION_REST_TIME; + } + + Notification notif = new NotificationCompat + .Builder(ctx, ProgramakerBridgeService.BridgeUserVibrationNotificationChannel) + .setContentTitle(ctx.getString(R.string.vibration_activated)) + .setSmallIcon(R.drawable.ic_vector_icon_user_triggered) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true) + .setVibrate(pattern) + .build(); + + int notificationId = notificationRandom.nextInt(); + notificationManager.notify(notificationId, notif); + + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Manually handle vibration if notification channels are in use + vibrator.vibrate(VibrationEffect.createWaveform(pattern, -1)); + } + + new Thread( () -> { + // Wait the activation time + for (int i = 0; i < times; i++) { + try { + Thread.sleep(pattern[i * 2]); + Thread.sleep(pattern[i * 2 + 1]); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + // Remove notification + notificationManager.cancel(notificationId); + }).start(); + + return null; + } + ) + ; + + // GETTERS + builder.addGetter( + "wifi_is_connected", + "Is WIFI connected?", + Collections.emptyList(), + (List params) -> { + WifiManager wifiManager = (WifiManager) ctx.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + assert wifiManager != null; + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + + if (wifiInfo == null) { + return false; + } + + return wifiInfo.getSupplicantState() == SupplicantState.COMPLETED; + }) + .addGetter( + "wifi_get_ssid", + "Get WIFI SSID", + Collections.emptyList(), + (List params) -> { + WifiManager wifiManager = (WifiManager) ctx.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + assert wifiManager != null; + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + + if (wifiInfo == null) { + return null; + } + + if (wifiInfo.getSupplicantState() != SupplicantState.COMPLETED) { + return null; + } + + return wifiInfo.getSSID(); + }); + + // Signal blocks + builder + .addTrigger( + "on_wifi_connected", + "When WIFI connection is ESTABLISHED", + Collections.emptyList(),// TODO: Save content to variable + "on_wifi_connected" + ) + .addTrigger( + "on_wifi_disconnected", + "When WIFI connection is LOST", + Collections.emptyList(), + "on_wifi_disconnected" + ); + + return builder; + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/bridge/blocks/GetterBlockDefinition.java b/app/src/main/java/com/codigoparallevar/minicards/bridge/blocks/GetterBlockDefinition.java new file mode 100644 index 0000000..816ef54 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/bridge/blocks/GetterBlockDefinition.java @@ -0,0 +1,65 @@ +package com.codigoparallevar.minicards.bridge.blocks; + +import android.util.Log; + +import com.codigoparallevar.minicards.types.functional.Function; +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 GetterBlockDefinition implements ProgramakerBridgeConfigurationBlock { + private final static String LogTag = "PM GetterBlockDef"; + + private final String id; + private final String message; + private final List args; + private final Function, ?> operation; + + public GetterBlockDefinition(String id, String message, List args, Function, ?> 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", "getter"); + obj.put("block_result_type", JSONObject.NULL); // TODO: Properly declare type + 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 Object call(@NotNull List arguments) throws Exception { + return this.operation.apply(arguments); + } +} 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..caa4b82 --- /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.Function; +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 OpBlockDef"; + + private final String id; + private final String message; + private final List args; + private final Function, Void> operation; + + public OperationBlockDefinition(String id, String message, List args, Function, Void> 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 Object call(@NotNull List arguments) throws Exception { + return this.operation.apply(arguments); + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/bridge/blocks/TriggerBlockDefinition.java b/app/src/main/java/com/codigoparallevar/minicards/bridge/blocks/TriggerBlockDefinition.java new file mode 100644 index 0000000..53a3a15 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/bridge/blocks/TriggerBlockDefinition.java @@ -0,0 +1,67 @@ +package com.codigoparallevar.minicards.bridge.blocks; + +import android.util.Log; + +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 TriggerBlockDefinition implements ProgramakerBridgeConfigurationBlock { + private final static String LogTag = "PM TriggerBlockDef"; + + private final String id; + private final String message; + private final List args; + private final String key; + + public TriggerBlockDefinition(String id, String message, List args, + String key) { + this.id = id; + this.message = message; + this.args = args; + this.key = key; + } + + @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("arguments", arguments); + obj.put("save_to", JSONObject.NULL); + obj.put("expected_value", JSONObject.NULL); + obj.put("block_type", "trigger"); + obj.put("key", this.key); + obj.put("subkey", 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 Object call(@NotNull List arguments) throws Exception { + throw new IllegalArgumentException("Non executable block"); + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/bridge/helpers/StatusStreamer.java b/app/src/main/java/com/codigoparallevar/minicards/bridge/helpers/StatusStreamer.java new file mode 100644 index 0000000..41b90a9 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/bridge/helpers/StatusStreamer.java @@ -0,0 +1,78 @@ +package com.codigoparallevar.minicards.bridge.helpers; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.wifi.SupplicantState; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Build; + +import com.programaker.bridge.ProgramakerBridge; + +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + +public class StatusStreamer { + private final Context ctx; + private final ProgramakerBridge bridge; + private WifiNetworkCallbackReceiver networkCallback = null; + + public StatusStreamer(Context ctx, ProgramakerBridge bridge) { + this.ctx = ctx; + this.bridge = bridge; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + this.networkCallback = new WifiNetworkCallbackReceiver(this); + } + } + + public void run() { + + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) { + return; + } + + NetworkRequest.Builder builder = new NetworkRequest.Builder(); + builder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI); + + ConnectivityManager connManager = (ConnectivityManager) this.ctx.getSystemService(Context.CONNECTIVITY_SERVICE); + assert connManager != null; + + connManager.registerNetworkCallback(builder.build(), this.networkCallback); + } + + public void stop() { + ConnectivityManager connManager = (ConnectivityManager) this.ctx.getSystemService(Context.CONNECTIVITY_SERVICE); + assert connManager != null; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + connManager.unregisterNetworkCallback(this.networkCallback); + } + } + + public void onWifiNetworkConnected(Network network) { + WifiManager wifiManager = (WifiManager) this.ctx.getSystemService(Context.WIFI_SERVICE); + WifiInfo wifiInfo; + + wifiInfo = wifiManager.getConnectionInfo(); + String ssid = ""; + if (wifiInfo.getSupplicantState() == SupplicantState.COMPLETED) { + // This might not return the SSID in case the user has not granted location permissions to the app + // see: https://stackoverflow.com/a/54446042 + ssid = wifiInfo.getSSID(); + } + + Map data = new HashMap<>(); + data.put("ssid", ssid); + + this.bridge.sendSignal("on_wifi_connected", new JSONObject(data)); + } + + public void onWifiNetworkDisconnected() { + this.bridge.sendSignal("on_wifi_disconnected", new JSONObject()); + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/bridge/helpers/WifiNetworkCallbackReceiver.java b/app/src/main/java/com/codigoparallevar/minicards/bridge/helpers/WifiNetworkCallbackReceiver.java new file mode 100644 index 0000000..b958397 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/bridge/helpers/WifiNetworkCallbackReceiver.java @@ -0,0 +1,34 @@ +package com.codigoparallevar.minicards.bridge.helpers; + +import android.net.ConnectivityManager; +import android.net.Network; +import android.os.Build; + +import androidx.annotation.RequiresApi; + +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) +class WifiNetworkCallbackReceiver extends ConnectivityManager.NetworkCallback { + private final StatusStreamer streamer; + + public WifiNetworkCallbackReceiver(StatusStreamer statusStreamer) { + this.streamer = statusStreamer; + } + + @Override + public void onAvailable(Network network) { + super.onAvailable(network); + this.streamer.onWifiNetworkConnected(network); + } + + @Override + public void onUnavailable() { + super.onUnavailable(); + this.streamer.onWifiNetworkDisconnected(); + } + + @Override + public void onLost(Network network) { + super.onLost(network); + this.streamer.onWifiNetworkDisconnected(); + } +} 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..8520260 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/ProgramakerCustomBlockPart.java @@ -0,0 +1,726 @@ +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.ProgramakerCustomBlockInputConnector; +import com.codigoparallevar.minicards.parts.connectors.AnyRoundOutputConnector; +import com.codigoparallevar.minicards.parts.connectors.ConnectorTypeInfo; +import com.codigoparallevar.minicards.parts.connectors.RoundOutputConnector; +import com.codigoparallevar.minicards.parts.connectors.SignalRoundOutputConnector; +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.AnySignal; +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.ProgramakerSignalListener; +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.jetbrains.annotations.NotNull; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; + +public class ProgramakerCustomBlockPart implements Part, ProgramakerSignalListener { + 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 SignalRoundOutputConnector 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<>(); + + SignalRoundOutputConnector pulseOutput = null; + + // Add pulses + if (has_pulse_input) { + ConnectorTypeInfo typeInfo = new ConnectorTypeInfo(ConnectorTypeInfo.Type.PULSE); + inputs.add(new Tuple2<>(typeInfo, + new ProgramakerCustomBlockInputConnector(this, _partGrid, + 0, 0, + IO_RADIUS, typeInfo)) + ); + } + if (has_pulse_output) { + pulseOutput = new SignalRoundOutputConnector(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 { + ConnectorTypeInfo typeInfo = ConnectorTypeInfo.FromArgument(arg); + inputs.add(new Tuple2<>(typeInfo, + new ProgramakerCustomBlockInputConnector(this, _partGrid, + 0, 0, + IO_RADIUS, typeInfo))); + } + } + } + + Tuple2 saveToOutput = null; + + if (savedTo != null) { + saveToOutput = new Tuple2<>(ConnectorTypeInfo.FromTypeName(savedTo.getComputedType()), + new AnyRoundOutputConnector(this, this._partGrid, 0, 0, IO_RADIUS)); + outputs.add(new Tuple2<>(saveToOutput.item1, saveToOutput.item2)); + } + + if (hasImplicitOutput) { + outputs.add(new Tuple2<>(ConnectorTypeInfo.FromTypeName(_block.getBlock_result_type()), + new AnyRoundOutputConnector(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 ProgramakerFunctionCallResult runBlockOperation() { + if (!this.active) { + Log.w(LogTag, "Trying to run inactive block function=" + this._block.getFunction_name()); + return null; + } + + 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; + } + + Object queriedValue = entry.item2.query(lastValues[index]); + if (queriedValue != null) { + lastValues[index] = queriedValue; + } + + if (lastValues[index] == null) { + arguments.add(null); // TODO: Get default value from block definition + } + else { + arguments.add(lastValues[index].toString()); // TODO: Do proper type formatting + } + } + + ProgramakerFunctionCallResult result = api.callBlock(this._block, arguments); + Log.i(LogTag, "Execution result="+result.getResult()); + + onExecutionCompleted(result); + + return result; + } + + private void onExecutionCompleted(ProgramakerFunctionCallResult result) { + Tuple2 savedTo = this.saveToOutput; + if (savedTo != null) { + Object value = null; + if (result != null) { + result.getResult(); + } + savedTo.item2.send(new AnySignal(value)); + } + + SignalRoundOutputConnector pulseOutput = this.pulseOutput; + if (pulseOutput != null) { + pulseOutput.send(new Signal()); + } + + // Notify screen that there's updates + this._partGrid.update(); + } + + private void freeBlock(String token) { + tryUpdateBlock(token, null); + } + + private String prepareStart() { + String token = UUID.randomUUID().toString(); + if (tryUpdateBlock(null, token)) { + return token; + } + return null; + } + + private synchronized boolean tryUpdateBlock(String oldVal, String newVal) { + if (this.token == oldVal) { + this.token = newVal; + return true; + } + return false; + } + + + @Override + public String get_id() { + return _id; + } + + @Override + public InputConnector getConnectorWithId(String inputConnectorId) { + if (inputConnectorId.startsWith("index=")) { + String[] chunks = inputConnectorId.split("="); + if (chunks.length > 1) { + Integer index = null; + try { + index = Integer.parseInt(chunks[1]); + } + catch (NumberFormatException ex) { + Log.e(LogTag, "Error parsing connector id="+inputConnectorId, ex); + } + + if (index != null && index < inputConnectors.size()) { + return inputConnectors.get(index).item2; + } + } + } + return null; + } + + private int getConnectorIndex(InputConnector inputConnector) { + int index = 0; + for (Tuple2 entry : inputConnectors) { + if (entry.item2 == inputConnector) { + return index; + } + index++; + } + + return -1; + } + + @Override + public String getConnectorId(InputConnector inputConnector) { + int index = getConnectorIndex(inputConnector); + if (index < 0 ) { + return null; + } + + return "index=" + index; + } + + public ConnectorTypeInfo getConnectorInfo(InputConnector inputConnector) { + for (Tuple2 entry : inputConnectors) { + if (entry.item2 == inputConnector) { + return entry.item1; + } + } + + return null; + } + + @Override + public void resume() { + this.active = true; + + String type = _block.getBlock_type(); + if (type != null && (type.equals("trigger"))) { + // Listen to signal + ProgramakerApi api = _partGrid.getApi(); + if (api == null) { + Log.i(LogTag, "Cannot listen to API. API not found, probably on a showcase."); + return; + } + + new DoAsync().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, + new Tuple2<>( + () -> { + _partGrid.getListenerManager().registerSignalListener( + _block.getBridge_id(), _block.getKey(), + this); + }, + ex -> { + Log.e(LogTag, "Error establishing connection to monitor", ex); + } + )); + } + } + + @Override + public void onNewSignal(@NotNull String bridgeId, @NotNull String key, @NotNull HashMap signal) { + // Propagate signal + // Stream object on save_to, then trigger pulse + Object content = signal.get("content"); + if (this.saveToOutput != null) { + this.saveToOutput.item2.send(new AnySignal(content)); + } + + if (this.pulseOutput != null) { + this.pulseOutput.send(new Signal()); + } + _partGrid.update(); + } + + @Override + public void pause() { + this.active = false; + + String type = _block.getBlock_type(); + if (type != null && (type.equals("trigger"))) { + // Release listening of signal + ProgramakerApi api = _partGrid.getApi(); + if (api == null) { + return; + } + + new DoAsync().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, + new Tuple2<>( + () -> { + _partGrid.getListenerManager().unregisterSignalListener(this); + }, + ex -> { + Log.e(LogTag, "Error disconnecting from monitor", ex); + } + )); + } + } + + @Override + public void draw(ScrolledCanvas canvas, boolean devMode) { + + if (!devMode) { + return; // Logic block, don't show on user-mode + } + + boolean operationInProgress = this.token != null; + + drawConnectors(canvas); + drawWires(canvas, devMode); + + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + if (operationInProgress) { + paint.setColor(Color.YELLOW); + } + else { + paint.setColor(Color.WHITE); + } + canvas.drawRect( + new Rect(get_left(), get_top(), + get_right(), get_bottom()), + paint); + + Paint textPaint = getTextPaint(); + canvas.drawText(getMessage(), + get_left() + WIDTH_PADDING, + get_bottom() - HEIGHT_PADDING, + textPaint); + } + + private void drawConnectors(ScrolledCanvas canvas) { + if (inputConnectors != null) { + for (Tuple2 entry : inputConnectors) { + Paint outerInputConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + outerInputConnectorPaint.setColor(entry.item1.getOuterColor()); + + canvas.drawCircle( + entry.item2.getX(), + entry.item2.getY(), + entry.item2.getRadius(), + outerInputConnectorPaint); + + Paint innerInputConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + innerInputConnectorPaint.setColor(entry.item1.getInnerColor()); + + canvas.drawCircle( + entry.item2.getX(), + entry.item2.getY(), + entry.item2.getRadius() / 2, + innerInputConnectorPaint); + } + } + + if (outputConnectors != null) { + for (Tuple2 entry : outputConnectors) { + Paint outerOutputConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + outerOutputConnectorPaint.setColor(entry.item1.getOuterColor()); + + canvas.drawCircle( + entry.item2.getX(), + entry.item2.getY(), + entry.item2.getRadius(), + outerOutputConnectorPaint); + + Paint innerOutputConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + innerOutputConnectorPaint.setColor(entry.item1.getInnerColor()); + + canvas.drawCircle( + entry.item2.getX(), + entry.item2.getY(), + entry.item2.getRadius() / 2, + innerOutputConnectorPaint); + } + } + } + + private void drawWires(ScrolledCanvas canvas, boolean devMode) { + for (OutputConnector outputConnector : getOutputConnectors()){ + outputConnector.drawWires(canvas, devMode); + } + } + + @Override + public void moveEnd(int x, int y) { + _left = x - width / 2; + _top = y - height / 2; + + this.updatePortPositions(); + } + + @Override + public void drop(int x, int y) { + moveEnd(x, y); + } + + @Override + public boolean containsPoint(int x, int y) { + return ((x >= this.get_left()) && (x <= this.get_right()) + && (y >= this.get_top()) && (y <= this.get_bottom())); + } + + @Override + public Moveable getMoveable() { + return this; + } + + @Override + public void unlink() { + pause(); + + for (InputConnector input : getInputConnectors()) { + input.unlink(); + } + } + + @Override + public Object query(Object lastValue) { + String blockType = _block.getBlock_type(); + + if ((blockType == null) || (!blockType.equals("getter"))) { + // Only relevant for getters + return null; + } + + String token = this.prepareStart(); + + if (token != null) { + try { + _partGrid.update(); + ProgramakerFunctionCallResult result = runBlockOperation(); + if (result != null) { + return result.getResult(); + } + } + finally { + this.freeBlock(token); + } + } + + return null; + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/android/CameraStreamer.java b/app/src/main/java/com/codigoparallevar/minicards/parts/android/CameraStreamer.java new file mode 100644 index 0000000..548ead1 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/android/CameraStreamer.java @@ -0,0 +1,465 @@ +package com.codigoparallevar.minicards.parts.android; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.Color; +import android.graphics.ImageFormat; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CaptureRequest; +import android.media.Image; +import android.media.ImageReader; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Log; +import android.view.Surface; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import com.codigoparallevar.minicards.CanvasView; +import com.codigoparallevar.minicards.PartInstantiator; +import com.codigoparallevar.minicards.ScrolledCanvas; +import com.codigoparallevar.minicards.parts.connectors.ImageRoundOutputConnector; +import com.codigoparallevar.minicards.parts.style.CardTheme; +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.ImageSignal; +import com.codigoparallevar.minicards.types.wireData.WireDataType; +import com.codigoparallevar.minicards.utils.Serializations; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) +public class CameraStreamer implements Part { + private static final String LogTag = "CameraStreamer"; + + private static final int DEFAULT_SIDE_SIZE = 200; + private final String _id; + private final PartGrid _partGrid; + private int _left; + private int _top; + private int _right; + private int _bottom; + private List _outputConnectors; + private final ImageRoundOutputConnector _imageRoundOutputConnector; + private final long SLEEP_TIME = 1000; + private ImageReader imageReader = null; + private HandlerThread _thread = null; + private Handler _handler = null; + private static final int CAPTURE_IMAGE_WIDTH = 256; + private static final int CAPTURE_IMAGE_HEIGHT = 256; + + private CameraStreamer(String id, PartGrid partGrid, int left, int top, int right, int bottom) { + _id = id; + _partGrid = partGrid; + _left = left; + _top = top; + _right = right; + _bottom = bottom; + + // Create connectors + _imageRoundOutputConnector = new ImageRoundOutputConnector( + this, + _partGrid, + getOutputConnectorCenterX(), getOutputConnectorCenterY(), + getOutputConnectRadius()); + + _outputConnectors = new LinkedList<>(); + _outputConnectors.add(_imageRoundOutputConnector); + } + + + public CameraStreamer(PartGrid partGrid, int left, int top, int right, int bottom) { + this(UUID.randomUUID().toString(), partGrid, left, top, right, bottom); + } + + + @Override + public void moveEnd(int x, int y) { + final int width = _right - _left; + final int height = _bottom - _top; + + _left = x - width / 2; + _right = _left + width; + + _top = y - height / 2; + _bottom = _top + height; + + _imageRoundOutputConnector.updatePosition( + getOutputConnectorCenterX(), + getOutputConnectorCenterY()); + } + + @Override + public void drop(int x, int y) { + moveEnd(x, y); + } + + @Override + public boolean containsPoint(int x, int y) { + return (x >= get_left()) && (x <= get_right()) + && (y >= get_top()) && (y <= get_bottom()); + } + + @Override + public Moveable getMoveable() { + return this; + } + + @Override + public void unlink() { + pause(); + + for (InputConnector input : getInputConnectors()) { + input.unlink(); + } + } + + @Override + public void draw(ScrolledCanvas canvas, boolean devMode) { + if (devMode){ + drawConnector(canvas); + drawWires(canvas, devMode); + + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setColor(Color.GRAY); + + canvas.drawRect( + new Rect(_left, _top, + _right, _bottom), + paint); + + + // Draw a little camera + Paint iconPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + iconPaint.setStyle(Paint.Style.STROKE); + iconPaint.setColor(Color.YELLOW); + iconPaint.setStrokeWidth(5f); + iconPaint.setStrokeCap(Paint.Cap.ROUND); + iconPaint.setStrokeJoin(Paint.Join.ROUND); + + // TODO: Refactor this into something reasonable + float side = _right - _left; + float mid = side / 2; + float quarter = mid / 2; + float oct = quarter / 2; + float hex = oct / 2; + float shex = hex / 2; + + Path path = new Path(); + // Square top left + path.moveTo(_left + oct + hex + shex, _top + quarter + hex); + // Square top right + path.lineTo(_left + mid + hex + shex, _top + quarter + hex); + // Box-to-projection connection + path.lineTo(_left + mid + hex + shex, _top + mid); + // Projection top right + path.lineTo(_right - quarter - hex + hex + shex, _top + quarter + hex); + // Projection bottom right + path.lineTo(_right - quarter - hex + hex + shex, _bottom - quarter - hex); + // Back to box + path.lineTo(_left + mid + hex + shex, _top + mid); + // Box bottom right + path.lineTo(_left + mid + hex + shex, _bottom - quarter - hex); + // Box bottom left + path.lineTo(_left + oct + hex + shex, _bottom - quarter - hex); + + path.close(); + canvas.drawPath(path, iconPaint); + } + } + + private void drawConnector(ScrolledCanvas canvas) { + Paint outerConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + outerConnectorPaint.setColor(CardTheme.IMAGE_CONNECTOR_COLOR_OUTER); + + canvas.drawCircle(getOutputConnectorCenterX(), + getOutputConnectorCenterY(), + getOutputConnectRadius(), + outerConnectorPaint); + + Paint innerConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + innerConnectorPaint.setColor(CardTheme.IMAGE_CONNECTOR_COLOR_INNER); + + canvas.drawCircle(getOutputConnectorCenterX(), + getOutputConnectorCenterY(), + getOutputConnectRadius() / 2, + innerConnectorPaint); + } + + private void drawWires(ScrolledCanvas canvas, boolean devMode) { + for (OutputConnector outputConnector : _outputConnectors){ + outputConnector.drawWires(canvas, devMode); + } + } + + @Override + public int get_left() { + return _left; + } + + @Override + public int get_right() { + return _right; + } + + @Override + public int get_top() { + return _top; + } + + @Override + public int get_bottom() { + return _bottom; + } + + @Override + public void touched() { + // Just ignore it, as it's a logic component + } + + @Override + public List getInputConnectors() { + return Collections.emptyList(); + } + + @Override + public List getOutputConnectors() { + return _outputConnectors; + } + + @Override + public JSONObject serialize() throws JSONException { + JSONObject serialized = new JSONObject(); + + serialized.put("id", _id); + serialized.put("left", _left); + serialized.put("top", _top); + serialized.put("right", _right); + serialized.put("bottom", _bottom); + serialized.put("on_signal_output_connector", + Serializations.serialize(serializeConnectionEndpoints())); + + return serialized; + } + + public static Tuple2> deserialize(PartGrid partGrid, JSONObject data) throws JSONException { + String id = data.getString("id"); + int left = data.getInt("left"); + int top = data.getInt("top"); + int right = data.getInt("right"); + int bottom = data.getInt("bottom"); + + CameraStreamer cameraStreamer = new CameraStreamer(id, partGrid, left, top, right, bottom); + + List connections = new LinkedList<>(); + + JSONArray connectorOuts = data.getJSONArray("on_signal_output_connector"); + for (int i = 0; i < connectorOuts.length(); i++){ + connections.add(PartConnection.deserialize( + cameraStreamer._imageRoundOutputConnector, + connectorOuts.getJSONObject(i))); + } + + return new Tuple2>(cameraStreamer, connections); + } + + private List> serializeConnectionEndpoints() { + List> serializedData = new LinkedList<>(); + + for (Tuple2 endpoint : _imageRoundOutputConnector.getConnectionEndpoints()){ + serializedData.add(PartConnection.serialize(endpoint.item1, endpoint.item2)); + } + + return serializedData; + } + + @Override + public void send(InputConnector roundInputConnector, WireDataType signal) { + // @TODO: REMOVE THE NEED FOR THIS + } + + private void onNewImage(Image img) { + _imageRoundOutputConnector.send(new ImageSignal(img)); + } + + @Override + public String get_id() { + return _id; + } + + @Override + public InputConnector getConnectorWithId(String inputConnectorId) { + return null; + } + + @Override + public String getConnectorId(InputConnector inputConnector) { + return null; + } + + @Override + public void resume() { + if (!(this._partGrid instanceof CanvasView)) { + return; + } + + CanvasView view = ((CanvasView) this._partGrid); + Context ctx = view.getContext(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (ctx.checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + view.requestPermission(Manifest.permission.CAMERA, () -> { CameraStreamer.this.resume(); }); + return; + } + } + + this.imageReader = ImageReader.newInstance(CAPTURE_IMAGE_WIDTH, CAPTURE_IMAGE_HEIGHT, ImageFormat.JPEG, 30); + this.imageReader.setOnImageAvailableListener((ImageReader.OnImageAvailableListener) newImageReader -> { + Image latestImage = newImageReader.acquireLatestImage(); + + if (latestImage == null) { + return; + } + + CameraStreamer.this.onNewImage(latestImage); + latestImage.close(); + }, new Handler()); + + CameraManager cm = (CameraManager) ctx.getSystemService(Context.CAMERA_SERVICE); + try { + String[] cameraList = cm.getCameraIdList(); + for (String cd : cameraList) { + //get camera characteristics + CameraCharacteristics mCameraCharacteristics = cm.getCameraCharacteristics(cd); + + //check if the camera is in the back - if not, continue to next + if (mCameraCharacteristics.get(CameraCharacteristics.LENS_FACING) != CameraCharacteristics.LENS_FACING_BACK) { + continue; + } + + if (this._thread == null) { + this._thread = new HandlerThread("mCameraHandlerThread"); + this._thread.start(); + } + if (this._handler == null) { + this._handler = new Handler(this._thread.getLooper()); + } + + cm.openCamera(cd, new CameraDevice.StateCallback() { + @Override + public void onOpened(@NonNull CameraDevice camera) { + //make list of surfaces to give to camera + List surfaceList = new ArrayList<>(); + Surface surface = CameraStreamer.this.imageReader.getSurface(); + surfaceList.add(surface); + + try { + camera.createCaptureSession(surfaceList, new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + try { + CaptureRequest.Builder requestBuilder = session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); + requestBuilder.set(CaptureRequest.JPEG_ORIENTATION, mCameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)); + requestBuilder.addTarget(surface); + //set to null - image data will be produced but will not receive metadata + session.setRepeatingRequest(requestBuilder.build(), null, CameraStreamer.this._handler); + } catch (CameraAccessException e) { + Log.e(LogTag, "createCaptureSession threw CameraAccessException.", e); + } + } + + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession session) { + Log.i(LogTag, "Error on camera configuration"); + } + }, CameraStreamer.this._handler); + } catch (CameraAccessException e) { + Log.e(LogTag, "createCaptureSession threw CameraAccessException.", e); + } + } + + @Override + public void onDisconnected(@NonNull CameraDevice camera) { + Log.i(LogTag, "Camera disconnected"); + } + + @Override + public void onError(@NonNull CameraDevice camera, int error) { + Log.i(LogTag, "Error on camera opening: " + error); + } + }, this._handler); + break; + } + + } catch (CameraAccessException e) { + e.printStackTrace(); + } + } + + @Override + public void pause() { + this._handler = null; + if (this._thread != null) { + this._thread.interrupt(); + this._thread = null; + } + if (this.imageReader != null) { + this.imageReader.close(); + this.imageReader = null; + } + } + + @Override + public Object query(Object lastValue) { + return null; // No relevant value (maybe time?) + } + + + private int getOutputConnectorCenterX() { + return (_left + _right) / 2; + } + + private int getOutputConnectorCenterY() { + return _bottom; + } + + private int getOutputConnectRadius() { + return (_right - _left) / 2; + } + + public static PartInstantiator getInstantiator() { + final int halfSideSize = DEFAULT_SIDE_SIZE / 2; + return new PartInstantiator() { + @Override + protected Part instantiate(PartGrid grid, Tuple2 center) { + return new CameraStreamer(grid, + center.item1 - halfSideSize, center.item2 - halfSideSize, + center.item1 + halfSideSize, center.item2 + halfSideSize); + } + }; + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/buttons/RoundButton.java b/app/src/main/java/com/codigoparallevar/minicards/parts/buttons/RoundButton.java index f545071..86cdef8 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/parts/buttons/RoundButton.java +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/buttons/RoundButton.java @@ -6,12 +6,13 @@ import android.util.Log; import com.codigoparallevar.minicards.PartInstantiator; import com.codigoparallevar.minicards.ScrolledCanvas; -import com.codigoparallevar.minicards.parts.connectors.RoundOutputConnector; +import com.codigoparallevar.minicards.parts.connectors.SignalRoundOutputConnector; +import com.codigoparallevar.minicards.parts.style.CardTheme; 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.Tuple2; +import com.codigoparallevar.minicards.types.functional.Tuple2; import com.codigoparallevar.minicards.types.connectors.input.InputConnector; import com.codigoparallevar.minicards.types.connectors.output.OutputConnector; import com.codigoparallevar.minicards.types.wireData.Signal; @@ -38,7 +39,7 @@ public class RoundButton implements Part { private final int _outerRadiusThickness = 10; private final int _pathRunWay = 200; private List _outputConnectors; - private final RoundOutputConnector _pressedOutputConnector; + private final SignalRoundOutputConnector _pressedOutputConnector; private final static int DEFAULT_INNER_RADIUS = 80; private final static int DEFAULT_OUTER_RADIUS = 100; @@ -53,7 +54,7 @@ public class RoundButton implements Part { _outerRadius = outerRadius; // Create connectors - _pressedOutputConnector = new RoundOutputConnector( + _pressedOutputConnector = new SignalRoundOutputConnector( this, _partGrid, getOutputConnectorCenterX(), getOutputConnectorCenterY(), @@ -69,22 +70,22 @@ public class RoundButton implements Part { @Override public int get_left() { - return _xCenter - _outerRadius / 2; + return _xCenter - _outerRadius; } @Override public int get_right() { - return _xCenter + _outerRadius / 2; + return _xCenter + _outerRadius; } @Override public int get_top() { - return _yCenter - _outerRadius / 2; + return _yCenter - _outerRadius; } @Override public int get_bottom() { - return _yCenter + _outerRadius / 2; + return _yCenter + _outerRadius; } @Override @@ -106,20 +107,27 @@ public class RoundButton implements Part { } private void drawConnector(ScrolledCanvas canvas) { - Paint connectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - connectorPaint.setColor(Color.RED); + Paint outerConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + outerConnectorPaint.setColor(CardTheme.PULSE_CONNECTOR_COLOR_OUTER); canvas.drawCircle(getOutputConnectorCenterX(), getOutputConnectorCenterY(), getOutputConnectRadius(), - connectorPaint); + outerConnectorPaint); + + Paint innerConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + innerConnectorPaint.setColor(CardTheme.PULSE_CONNECTOR_COLOR_INNER); + + canvas.drawCircle(getOutputConnectorCenterX(), getOutputConnectorCenterY(), + getOutputConnectRadius() / 2, + innerConnectorPaint); } private int getOutputConnectorCenterX() { - return _xCenter + _outerRadius; + return _xCenter; } private int getOutputConnectorCenterY() { - return _yCenter; + return _yCenter + _outerRadius; } private int getOutputConnectRadius() { @@ -221,7 +229,12 @@ public class RoundButton implements Part { } - public RoundOutputConnector getPressedOutputConnector(){ + @Override + public Object query(Object lastValue) { + return null; // Only pulse output, so no relevant data + } + + public SignalRoundOutputConnector getPressedOutputConnector(){ return _pressedOutputConnector; } diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/AnyRoundOutputConnector.java b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/AnyRoundOutputConnector.java new file mode 100644 index 0000000..3015c7b --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/AnyRoundOutputConnector.java @@ -0,0 +1,196 @@ +package com.codigoparallevar.minicards.parts.connectors; + +import android.util.Log; + +import com.codigoparallevar.minicards.ScrolledCanvas; +import com.codigoparallevar.minicards.types.Drawable; +import com.codigoparallevar.minicards.types.Part; +import com.codigoparallevar.minicards.types.PartGrid; +import com.codigoparallevar.minicards.types.connectors.Wiring.AnyWire; +import com.codigoparallevar.minicards.types.connectors.input.AnyInputConnector; +import com.codigoparallevar.minicards.types.connectors.input.InputConnector; +import com.codigoparallevar.minicards.types.connectors.output.AnyOutputConnector; +import com.codigoparallevar.minicards.types.functional.Tuple2; +import com.codigoparallevar.minicards.types.wireData.AnySignal; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; + +public class AnyRoundOutputConnector implements Drawable, AnyOutputConnector, + RoundOutputConnector { + private PartGrid _partGrid; + private int _centerX; + private int _centerY; + private final int _radius; + private final Part _part; + private AnyWire _currentWire = null; + private final List _wires; + private final HashSet _connections; + + public AnyRoundOutputConnector(Part part, PartGrid partGrid, int centerX, int centerY, int radius) { + _part = part; + _partGrid = partGrid; + _centerX = centerX; + _centerY = centerY; + _radius = radius; + _wires = new LinkedList<>(); + _connections = new HashSet<>(); + } + + @Override + public boolean containsPoint(int x, int y) { + return ((Math.abs(x - _centerX) <= _radius) + && (Math.abs(y - _centerY) <= _radius)); + } + + + @Override + public AnyWire getMoveable() { + if (_currentWire == null) { + startWire(); + } + + return _currentWire; + } + + @Override + public void unlink() { + while (_wires.size() > 0) { + _wires.get(0).unlink(); + } + } + + private void startWire() { + _currentWire = new AnyWire(this, _centerX, _centerY); + } + + public void drop(AnyWire wire, AnyInputConnector resultPoint) { + if (wire != _currentWire) { + Log.w("RoundOutputConnector", + "Asked to drop non matching wire " + + "(expected " + _currentWire + ", got " + wire + ")"); + return ; + } + + _currentWire = null; + + Log.d("RoundOutputConnector", "Dropped wire on " + resultPoint); + + // Not connected + if (resultPoint == null) { + return; + } + + // Already connected + if (_connections.contains(resultPoint)) { + return; + } + _connections.add(resultPoint); + + wire.attachTo(resultPoint, this); + _wires.add(wire); + } + + @Override + public void drop(AnyWire wire) { + + if (wire != _currentWire) { + Log.w("RoundOutputConnector", + "Asked to drop non matching wire " + + "(expected " + _currentWire + ", got " + wire + ")"); + return; + } + + AnyInputConnector resultPoint = _partGrid.getAnyInputConnectorOn( + wire.getXEnd(), wire.getYEnd()); + + drop(wire, resultPoint); + } + + @Override + public void wireUnlinked(AnyWire wire) { + _wires.remove(wire); + _connections.remove(wire.getAttachedTo()); + } + + @Override + public void drawWires(ScrolledCanvas canvas, boolean devMode) { + for (AnyWire wire : _wires) { + wire.draw(canvas, devMode); + } + + if (_currentWire != null) { + _currentWire.draw(canvas, devMode); + } + } + + @Override + public void updatePosition(int x, int y) { + _centerX = x; + _centerY = y; + + for (AnyWire wire : _wires){ + wire.moveStart(x, y); + } + } + + @Override + public List> getConnectionEndpoints() { + List> endpointIds = new LinkedList<>(); + + for (AnyWire wire : _wires) { + InputConnector inputConnector = wire.getAttachedTo(); + Part endPart = inputConnector.getPart(); + endpointIds.add(new Tuple2<>(inputConnector.getId(), endPart.get_id())); + } + + return endpointIds; + } + + @Override + public void connectTo(AnyInputConnector inputConnector) { + if (_connections.contains(inputConnector)) { + return; + } + + _connections.add(inputConnector); + + AnyWire wire = new AnyWire(this, _centerX, _centerY); + wire.attachTo(inputConnector, this); + _wires.add(wire); + } + + @Override + public void draw(ScrolledCanvas canvas, boolean devMode) { + // TODO: Complete this part + } + + public void send(AnySignal signal) { + for (AnyWire wire : _wires){ + wire.send(signal); + } + } + + @Override + public Object query(Object lastValue) { + return _part.query(lastValue); + } + + @Override + public List getWires() { + return _wires; + } + + public int getX() { + return _centerX; + } + + public int getY() { + return _centerY; + } + + public float getRadius() { + return _radius; + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/BooleanRoundInputConnector.java b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/BooleanRoundInputConnector.java index b1d2056..d23a79c 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/BooleanRoundInputConnector.java +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/BooleanRoundInputConnector.java @@ -72,7 +72,7 @@ public class BooleanRoundInputConnector implements BooleanInputConnector { @Override - public void getAttachment(Wire wire) { + public void addAttachment(Wire wire) { _attachments.add(wire); } @@ -92,7 +92,7 @@ public class BooleanRoundInputConnector implements BooleanInputConnector { } @Override - public void unlinkWire(Wire wire) { + public void wireUnlinked(Wire wire) { _attachments.remove(wire); } } diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/BooleanRoundOutputConnector.java b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/BooleanRoundOutputConnector.java index b1b3969..a9b1fb4 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/BooleanRoundOutputConnector.java +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/BooleanRoundOutputConnector.java @@ -7,11 +7,11 @@ import com.codigoparallevar.minicards.types.Drawable; import com.codigoparallevar.minicards.types.Moveable; import com.codigoparallevar.minicards.types.Part; import com.codigoparallevar.minicards.types.PartGrid; -import com.codigoparallevar.minicards.types.Tuple2; import com.codigoparallevar.minicards.types.connectors.Wiring.BooleanWire; import com.codigoparallevar.minicards.types.connectors.input.BooleanInputConnector; import com.codigoparallevar.minicards.types.connectors.input.InputConnector; import com.codigoparallevar.minicards.types.connectors.output.BooleanOutputConnector; +import com.codigoparallevar.minicards.types.functional.Tuple2; import com.codigoparallevar.minicards.types.wireData.BooleanSignal; import java.util.HashSet; @@ -83,7 +83,7 @@ public class BooleanRoundOutputConnector implements Drawable, BooleanOutputConne } _connections.add(resultPoint); - wire.attachTo(resultPoint); + wire.attachTo(resultPoint, this); _wires.add(wire); } else { @@ -94,8 +94,9 @@ public class BooleanRoundOutputConnector implements Drawable, BooleanOutputConne } @Override - public void unlinkWire(BooleanWire wire) { + public void wireUnlinked(BooleanWire wire) { _wires.remove(wire); + _connections.remove(wire.getAttachedTo()); } @Override @@ -141,7 +142,7 @@ public class BooleanRoundOutputConnector implements Drawable, BooleanOutputConne _connections.add(inputConnector); BooleanWire wire = new BooleanWire(this, _centerX, _centerY); - wire.attachTo(inputConnector); + wire.attachTo(inputConnector, this); _wires.add(wire); } @@ -155,4 +156,14 @@ public class BooleanRoundOutputConnector implements Drawable, BooleanOutputConne wire.send(signal); } } + + @Override + public Object query(Object lastValue) { + return _part.query(lastValue); + } + + @Override + public List getWires() { + return _wires; + } } diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/ConnectorTypeInfo.java b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/ConnectorTypeInfo.java new file mode 100644 index 0000000..3dc1033 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/ConnectorTypeInfo.java @@ -0,0 +1,172 @@ +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; + +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) { + return new ConnectorTypeInfo(Type.UNKNOWN); + } + + type = type.toLowerCase(); + if (type.equals("string")) { + return new ConnectorTypeInfo(Type.STRING); + } + else if (type.equals("integer")) { + return new ConnectorTypeInfo(Type.INTEGER); + } + else if (type.equals("float")) { + return new ConnectorTypeInfo(Type.FLOAT); + } + else if (type.equals("boolean")) { + return new ConnectorTypeInfo(Type.BOOLEAN); + } + else if (type.equals("pulse")) { + return new ConnectorTypeInfo(Type.PULSE); + } + else if (type.equals("enum")) { + return new ConnectorTypeInfo(Type.ENUM); + } + else if (type.equals("any")) { + return new ConnectorTypeInfo(Type.ANY); + } + else { + return new ConnectorTypeInfo(Type.UNKNOWN); + } + } + + + private static String typeToString(Type type) { + switch (type) { + case ANY: + return "any"; + case ENUM: + return "enum"; + case FLOAT: + return "float"; + case PULSE: + return "pulse"; + case STRING: + return "string"; + case BOOLEAN: + return "boolean"; + case INTEGER: + return "integer"; + case UNKNOWN: + return "unknown"; + default: + return "unknown"; + } + } + + public static ConnectorTypeInfo deserialize(JSONObject jsonTypeInfo) { + 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() { + switch (_type) { + case ANY: + return CardTheme.ANY_CONNECTOR_COLOR_OUTER; + case ENUM: + return CardTheme.ENUM_CONNECTOR_COLOR_OUTER; + case FLOAT: + return CardTheme.FLOAT_CONNECTOR_COLOR_OUTER; + case PULSE: + return CardTheme.PULSE_CONNECTOR_COLOR_OUTER; + case STRING: + return CardTheme.STRING_CONNECTOR_COLOR_OUTER; + case BOOLEAN: + return CardTheme.BOOLEAN_CONNECTOR_COLOR_OUTER; + case INTEGER: + return CardTheme.INTEGER_CONNECTOR_COLOR_OUTER; + case UNKNOWN: + return CardTheme.UNKNOWN_CONNECTOR_COLOR_OUTER; + default: + return CardTheme.UNKNOWN_CONNECTOR_COLOR_OUTER; + } + } + + + public int getInnerColor() { + switch (_type) { + case ANY: + return CardTheme.ANY_CONNECTOR_COLOR_INNER; + case ENUM: + return CardTheme.ENUM_CONNECTOR_COLOR_INNER; + case FLOAT: + return CardTheme.FLOAT_CONNECTOR_COLOR_INNER; + case PULSE: + return CardTheme.PULSE_CONNECTOR_COLOR_INNER; + case STRING: + return CardTheme.STRING_CONNECTOR_COLOR_INNER; + case BOOLEAN: + return CardTheme.BOOLEAN_CONNECTOR_COLOR_INNER; + case INTEGER: + return CardTheme.INTEGER_CONNECTOR_COLOR_INNER; + case UNKNOWN: + return CardTheme.UNKNOWN_CONNECTOR_COLOR_INNER; + default: + return CardTheme.UNKNOWN_CONNECTOR_COLOR_INNER; + } + } + + public JSONObject serialize() { + 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, + STRING, + ANY, + ENUM, + UNKNOWN, + FLOAT, + INTEGER, + } + + public ConnectorTypeInfo(ConnectorTypeInfo.Type type) { + this._type = type; + } + + public Type get_type() { + return _type; + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/AnyRoundInputConnector.java b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/ImageRoundInputConnector.java similarity index 76% rename from app/src/main/java/com/codigoparallevar/minicards/parts/connectors/AnyRoundInputConnector.java rename to app/src/main/java/com/codigoparallevar/minicards/parts/connectors/ImageRoundInputConnector.java index fbf5888..9789d60 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/AnyRoundInputConnector.java +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/ImageRoundInputConnector.java @@ -5,22 +5,22 @@ import android.util.Log; import com.codigoparallevar.minicards.types.Moveable; import com.codigoparallevar.minicards.types.Part; import com.codigoparallevar.minicards.types.connectors.Wiring.Wire; -import com.codigoparallevar.minicards.types.connectors.input.AnyInputConnector; -import com.codigoparallevar.minicards.types.wireData.WireDataType; +import com.codigoparallevar.minicards.types.connectors.input.ImageInputConnector; +import com.codigoparallevar.minicards.types.wireData.ImageSignal; import java.util.LinkedList; import java.util.List; -public class AnyRoundInputConnector extends AnyInputConnector { +public class ImageRoundInputConnector implements ImageInputConnector { private final Part _part; private int _xposition; private int _yposition; private final int _radius; private final List _attachments = new LinkedList<>(); - public AnyRoundInputConnector(Part part, - int inputConnectorCenterX, int inputConnectorCenterY, - int inputConnectorRadius) { + public ImageRoundInputConnector(Part part, + int inputConnectorCenterX, int inputConnectorCenterY, + int inputConnectorRadius) { _part = part; _xposition = inputConnectorCenterX; _yposition = inputConnectorCenterY; @@ -66,13 +66,13 @@ public class AnyRoundInputConnector extends AnyInputConnector { } @Override - public void send(WireDataType signal) { + public void send(ImageSignal signal) { _part.send(this, signal); } @Override - public void getAttachment(Wire wire) { + public void addAttachment(Wire wire) { _attachments.add(wire); } @@ -92,7 +92,7 @@ public class AnyRoundInputConnector extends AnyInputConnector { } @Override - public void unlinkWire(Wire wire) { + public void wireUnlinked(Wire wire) { _attachments.remove(wire); } } diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/ImageRoundOutputConnector.java b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/ImageRoundOutputConnector.java new file mode 100644 index 0000000..0bac6a7 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/ImageRoundOutputConnector.java @@ -0,0 +1,183 @@ +package com.codigoparallevar.minicards.parts.connectors; + +import android.util.Log; + +import com.codigoparallevar.minicards.ScrolledCanvas; +import com.codigoparallevar.minicards.types.Drawable; +import com.codigoparallevar.minicards.types.Moveable; +import com.codigoparallevar.minicards.types.Part; +import com.codigoparallevar.minicards.types.PartGrid; +import com.codigoparallevar.minicards.types.connectors.Wiring.ImageWire; +import com.codigoparallevar.minicards.types.connectors.input.ImageInputConnector; +import com.codigoparallevar.minicards.types.connectors.input.InputConnector; +import com.codigoparallevar.minicards.types.connectors.output.ImageOutputConnector; +import com.codigoparallevar.minicards.types.functional.Tuple2; +import com.codigoparallevar.minicards.types.wireData.ImageSignal; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; + +public class ImageRoundOutputConnector implements Drawable, ImageOutputConnector, + RoundOutputConnector { + private static final String LogTag = "ImageRoundOutputConnector"; + + private PartGrid _partGrid; + private int _centerX; + private int _centerY; + private final int _radius; + private final Part _part; + private ImageWire _currentWire = null; + private final List _wires; + private final HashSet _connections; + + public ImageRoundOutputConnector(Part part, PartGrid partGrid, int centerX, int centerY, int radius) { + _part = part; + _partGrid = partGrid; + _centerX = centerX; + _centerY = centerY; + _radius = radius; + _wires = new LinkedList<>(); + _connections = new HashSet<>(); + } + + @Override + public boolean containsPoint(int x, int y) { + return ((Math.abs(x - _centerX) <= _radius) + && (Math.abs(y - _centerY) <= _radius)); + } + + + @Override + public Moveable getMoveable() { + if (_currentWire == null) { + startWire(); + } + + return _currentWire; + } + + @Override + public void unlink() {} + + private void startWire() { + _currentWire = new ImageWire(this, _centerX, _centerY); + } + + @Override + public void drop(ImageWire wire) { + + if (wire == _currentWire){ + _currentWire = null; + + ImageInputConnector resultPoint = _partGrid.getImageInputConnectorOn( + wire.getXEnd(), wire.getYEnd()); + + Log.d(LogTag, "Dropped wire on " + resultPoint); + + // Not connected + if (resultPoint == null){ + return; + } + + // Already connected + if (_connections.contains(resultPoint)) { + return; + } + _connections.add(resultPoint); + + wire.attachTo(resultPoint, this); + _wires.add(wire); + } + else { + Log.w(LogTag, + "Asked to drop non matching wire " + + "(expected " + _currentWire + ", got " + wire + ")"); + } + } + + @Override + public void wireUnlinked(ImageWire wire) { + _wires.remove(wire); + _connections.remove(wire.getAttachedTo()); + } + + @Override + public void drawWires(ScrolledCanvas canvas, boolean devMode) { + for (ImageWire wire : _wires) { + wire.draw(canvas, devMode); + } + + if (_currentWire != null) { + _currentWire.draw(canvas, devMode); + } + } + + @Override + public void updatePosition(int x, int y) { + _centerX = x; + _centerY = y; + + for (ImageWire wire : _wires){ + wire.moveStart(x, y); + } + } + + @Override + public List> getConnectionEndpoints() { + List> endpointIds = new LinkedList<>(); + + for (ImageWire wire : _wires) { + InputConnector inputConnector = wire.getAttachedTo(); + Part endPart = inputConnector.getPart(); + endpointIds.add(new Tuple2<>(inputConnector.getId(), endPart.get_id())); + } + + return endpointIds; + } + + @Override + public void connectTo(ImageInputConnector inputConnector) { + if (_connections.contains(inputConnector)) { + return; + } + + _connections.add(inputConnector); + + ImageWire wire = new ImageWire(this, _centerX, _centerY); + wire.attachTo(inputConnector, this); + _wires.add(wire); + } + + @Override + public void draw(ScrolledCanvas canvas, boolean devMode) { + // TODO: Complete this part + } + + public void send(ImageSignal signal) { + for (ImageWire wire : _wires){ + wire.send(signal); + } + } + + @Override + public Object query(Object lastValue) { + return _part.query(lastValue); + } + + public int getX() { + return _centerX; + } + + public int getY() { + return _centerY; + } + + public float getRadius() { + return _radius; + } + + public List getWires() { + return this._wires; + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/ProgramakerCustomBlockInputConnector.java b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/ProgramakerCustomBlockInputConnector.java new file mode 100644 index 0000000..987c5c3 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/ProgramakerCustomBlockInputConnector.java @@ -0,0 +1,140 @@ +package com.codigoparallevar.minicards.parts.connectors; + +import android.util.Log; + +import com.codigoparallevar.minicards.CanvasView; +import com.codigoparallevar.minicards.parts.values.StaticValuePart; +import com.codigoparallevar.minicards.types.Moveable; +import com.codigoparallevar.minicards.types.Part; +import com.codigoparallevar.minicards.types.PartGrid; +import com.codigoparallevar.minicards.types.connectors.RoundConnector; +import com.codigoparallevar.minicards.types.connectors.Wiring.AnyWire; +import com.codigoparallevar.minicards.types.connectors.Wiring.Wire; +import com.codigoparallevar.minicards.types.connectors.input.AnyInputConnector; +import com.codigoparallevar.minicards.types.wireData.AnySignal; + +import java.util.LinkedList; +import java.util.List; + +public class ProgramakerCustomBlockInputConnector extends AnyInputConnector implements RoundConnector { + private static final int STATIC_VALUE_SEPARATION = 200; + private final Part _part; + private final ConnectorTypeInfo _typeInfo; + private final PartGrid _partGrid; + private int _xposition; + private int _yposition; + private final int _radius; + private final List _attachments = new LinkedList<>(); + + public ProgramakerCustomBlockInputConnector(Part part, PartGrid partGrid, + int inputConnectorCenterX, int inputConnectorCenterY, + int inputConnectorRadius, ConnectorTypeInfo typeInfo) { + _part = part; + _partGrid = partGrid; + _xposition = inputConnectorCenterX; + _yposition = inputConnectorCenterY; + _radius = inputConnectorRadius; + _typeInfo = typeInfo; + } + + @Override + public boolean containsPoint(int x, int y) { + return ((Math.abs(x - _xposition) <= _radius) + && (Math.abs(y - _yposition) <= _radius)); + } + + @Override + public Moveable getMoveable() { + return new Wire(this, _xposition, _yposition); + } + + @Override + public void unlink() { + for (Wire wire : _attachments) { + wire.unlink(); + } + } + + @Override + public void updatePosition(int x, int y) { + _xposition = x; + _yposition = y; + + for (Wire wire : _attachments){ + wire.moveEnd(x, y); + } + } + + public Object query(Object lastValue) { + for (Wire linked : _attachments) { + Object returned = linked.query(lastValue); + if (returned != null) { + return returned; + } + } + + return null; + } + + @Override + public int getX() { + return _xposition; + } + + @Override + public int getY() { + return _yposition; + } + + public float getRadius() { + return _radius; + } + + @Override + public void send(AnySignal signal) { + _part.send(this, signal); + } + + @Override + public void addAttachment(Wire wire) { + _attachments.add(wire); + } + + @Override + public Part getPart() { + return _part; + } + + @Override + public String getId() { + return _part.getConnectorId(this); + } + + @Override + public void drop(Wire wire) { + Log.d("InputConnector", "Dropped wire " + wire); + } + + @Override + public void wireUnlinked(Wire wire) { + _attachments.remove(wire); + } + + public void touched() { + // If it doesn't have wires connected, spawn a value block + if (this._attachments.size() > 0) { + return; + } + + final PartGrid grid = _partGrid; + + if (grid instanceof CanvasView) { + StaticValuePart value = new StaticValuePart(_partGrid, + this._xposition, this._yposition - STATIC_VALUE_SEPARATION, + this._typeInfo); + AnyWire wire = value.getOutputWire(); + value.getValueOutput().drop(wire, this); + ((CanvasView) grid).addPart(value); + } + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/RoundInputConnector.java b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/RoundInputConnector.java index 14a0de3..a2968d0 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/RoundInputConnector.java +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/RoundInputConnector.java @@ -65,14 +65,17 @@ public class RoundInputConnector implements SignalInputConnector { return _yposition; } + public float getRadius() { + return _radius; + } + @Override public void send(Signal signal) { _part.send(this, signal); } - @Override - public void getAttachment(Wire wire) { + public void addAttachment(Wire wire) { _attachments.add(wire); } @@ -92,7 +95,7 @@ public class RoundInputConnector implements SignalInputConnector { } @Override - public void unlinkWire(Wire wire) { + public void wireUnlinked(Wire wire) { _attachments.remove(wire); } } diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/RoundOutputConnector.java b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/RoundOutputConnector.java index d6c19a1..53844b3 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/RoundOutputConnector.java +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/RoundOutputConnector.java @@ -1,158 +1,14 @@ package com.codigoparallevar.minicards.parts.connectors; -import android.util.Log; - -import com.codigoparallevar.minicards.ScrolledCanvas; -import com.codigoparallevar.minicards.types.Drawable; -import com.codigoparallevar.minicards.types.Moveable; -import com.codigoparallevar.minicards.types.Part; -import com.codigoparallevar.minicards.types.PartGrid; -import com.codigoparallevar.minicards.types.Tuple2; -import com.codigoparallevar.minicards.types.connectors.Wiring.SignalWire; +import com.codigoparallevar.minicards.types.connectors.RoundConnector; +import com.codigoparallevar.minicards.types.connectors.Wiring.Wire; import com.codigoparallevar.minicards.types.connectors.input.InputConnector; -import com.codigoparallevar.minicards.types.connectors.input.SignalInputConnector; -import com.codigoparallevar.minicards.types.connectors.output.SignalOutputConnector; -import com.codigoparallevar.minicards.types.wireData.Signal; +import com.codigoparallevar.minicards.types.connectors.output.OutputConnector; +import com.codigoparallevar.minicards.types.wireData.WireDataType; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; - -public class RoundOutputConnector implements Drawable, SignalOutputConnector { - private PartGrid _partGrid; - private int _centerX; - private int _centerY; - private final int _radius; - private final Part _part; - private SignalWire _currentWire = null; - private final List _wires; - private final HashSet _connections; - - public RoundOutputConnector(Part part, PartGrid partGrid, int centerX, int centerY, int radius) { - _part = part; - _partGrid = partGrid; - _centerX = centerX; - _centerY = centerY; - _radius = radius; - _wires = new LinkedList<>(); - _connections = new HashSet<>(); - } - - @Override - public boolean containsPoint(int x, int y) { - return ((Math.abs(x - _centerX) <= _radius) - && (Math.abs(y - _centerY) <= _radius)); - } - - - @Override - public Moveable getMoveable() { - if (_currentWire == null) { - startWire(); - } - - return _currentWire; - } - - @Override - public void unlink() {} - - private void startWire() { - _currentWire = new SignalWire(this, _centerX, _centerY); - } - - @Override - public void drop(SignalWire wire) { - - if (wire == _currentWire){ - _currentWire = null; - - SignalInputConnector resultPoint = _partGrid.getSignalInputConnectorOn( - wire.getXEnd(), wire.getYEnd()); - - Log.d("RoundOutputConnector", "Dropped wire on " + resultPoint); - - // Not connected - if (resultPoint == null){ - return; - } - - // Already connected - if (_connections.contains(resultPoint)) { - return; - } - _connections.add(resultPoint); - - wire.attachTo(resultPoint); - _wires.add(wire); - } - else { - Log.w("RoundOutputConnector", - "Asked to drop non matching wire " - + "(expected " + _currentWire + ", got " + wire + ")"); - } - } - - @Override - public void unlinkWire(SignalWire wire) { - _wires.remove(wire); - } - - @Override - public void drawWires(ScrolledCanvas canvas, boolean devMode) { - for (SignalWire wire : _wires) { - wire.draw(canvas, devMode); - } - - if (_currentWire != null) { - _currentWire.draw(canvas, devMode); - } - } - - @Override - public void updatePosition(int x, int y) { - _centerX = x; - _centerY = y; - - for (SignalWire wire : _wires){ - wire.moveStart(x, y); - } - } - - @Override - public List> getConnectionEndpoints() { - List> endpointIds = new LinkedList<>(); - - for (SignalWire wire : _wires) { - InputConnector inputConnector = wire.getAttachedTo(); - Part endPart = inputConnector.getPart(); - endpointIds.add(new Tuple2<>(inputConnector.getId(), endPart.get_id())); - } - - return endpointIds; - } - - @Override - public void connectTo(SignalInputConnector inputConnector) { - if (_connections.contains(inputConnector)) { - return; - } - - _connections.add(inputConnector); - - SignalWire wire = new SignalWire(this, _centerX, _centerY); - wire.attachTo(inputConnector); - _wires.add(wire); - } - - @Override - public void draw(ScrolledCanvas canvas, boolean devMode) { - // TODO: Complete this part - } - - public void send(Signal signal) { - for (SignalWire wire : _wires){ - wire.send(signal); - } - } +public interface RoundOutputConnector< + T1 extends WireDataType, + T2 extends InputConnector, + T3 extends Wire + > extends RoundConnector, OutputConnector { } diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/SignalRoundOutputConnector.java b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/SignalRoundOutputConnector.java new file mode 100644 index 0000000..37d32ba --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/SignalRoundOutputConnector.java @@ -0,0 +1,183 @@ +package com.codigoparallevar.minicards.parts.connectors; + +import android.util.Log; + +import com.codigoparallevar.minicards.ScrolledCanvas; +import com.codigoparallevar.minicards.types.Drawable; +import com.codigoparallevar.minicards.types.Moveable; +import com.codigoparallevar.minicards.types.Part; +import com.codigoparallevar.minicards.types.PartGrid; +import com.codigoparallevar.minicards.types.connectors.Wiring.SignalWire; +import com.codigoparallevar.minicards.types.connectors.input.InputConnector; +import com.codigoparallevar.minicards.types.connectors.input.SignalInputConnector; +import com.codigoparallevar.minicards.types.connectors.output.SignalOutputConnector; +import com.codigoparallevar.minicards.types.functional.Tuple2; +import com.codigoparallevar.minicards.types.wireData.Signal; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; + +public class SignalRoundOutputConnector implements Drawable, SignalOutputConnector, + RoundOutputConnector { + private static final String LogTag = "RoundOutputConnector"; + + private PartGrid _partGrid; + private int _centerX; + private int _centerY; + private final int _radius; + private final Part _part; + private SignalWire _currentWire = null; + private final List _wires; + private final HashSet _connections; + + public SignalRoundOutputConnector(Part part, PartGrid partGrid, int centerX, int centerY, int radius) { + _part = part; + _partGrid = partGrid; + _centerX = centerX; + _centerY = centerY; + _radius = radius; + _wires = new LinkedList<>(); + _connections = new HashSet<>(); + } + + @Override + public boolean containsPoint(int x, int y) { + return ((Math.abs(x - _centerX) <= _radius) + && (Math.abs(y - _centerY) <= _radius)); + } + + + @Override + public Moveable getMoveable() { + if (_currentWire == null) { + startWire(); + } + + return _currentWire; + } + + @Override + public void unlink() {} + + private void startWire() { + _currentWire = new SignalWire(this, _centerX, _centerY); + } + + @Override + public void drop(SignalWire wire) { + + if (wire == _currentWire){ + _currentWire = null; + + SignalInputConnector resultPoint = _partGrid.getSignalInputConnectorOn( + wire.getXEnd(), wire.getYEnd()); + + Log.d(LogTag, "Dropped wire on " + resultPoint); + + // Not connected + if (resultPoint == null){ + return; + } + + // Already connected + if (_connections.contains(resultPoint)) { + return; + } + _connections.add(resultPoint); + + wire.attachTo(resultPoint, this); + _wires.add(wire); + } + else { + Log.w(LogTag, + "Asked to drop non matching wire " + + "(expected " + _currentWire + ", got " + wire + ")"); + } + } + + @Override + public void wireUnlinked(SignalWire wire) { + _wires.remove(wire); + _connections.remove(wire.getAttachedTo()); + } + + @Override + public void drawWires(ScrolledCanvas canvas, boolean devMode) { + for (SignalWire wire : _wires) { + wire.draw(canvas, devMode); + } + + if (_currentWire != null) { + _currentWire.draw(canvas, devMode); + } + } + + @Override + public void updatePosition(int x, int y) { + _centerX = x; + _centerY = y; + + for (SignalWire wire : _wires){ + wire.moveStart(x, y); + } + } + + @Override + public List> getConnectionEndpoints() { + List> endpointIds = new LinkedList<>(); + + for (SignalWire wire : _wires) { + InputConnector inputConnector = wire.getAttachedTo(); + Part endPart = inputConnector.getPart(); + endpointIds.add(new Tuple2<>(inputConnector.getId(), endPart.get_id())); + } + + return endpointIds; + } + + @Override + public void connectTo(SignalInputConnector inputConnector) { + if (_connections.contains(inputConnector)) { + return; + } + + _connections.add(inputConnector); + + SignalWire wire = new SignalWire(this, _centerX, _centerY); + wire.attachTo(inputConnector, this); + _wires.add(wire); + } + + @Override + public void draw(ScrolledCanvas canvas, boolean devMode) { + // TODO: Complete this part + } + + public void send(Signal signal) { + for (SignalWire wire : _wires){ + wire.send(signal); + } + } + + @Override + public Object query(Object lastValue) { + return _part.query(lastValue); + } + + public int getX() { + return _centerX; + } + + public int getY() { + return _centerY; + } + + public float getRadius() { + return _radius; + } + + public List getWires() { + return this._wires; + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/StringRoundInputConnector.java b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/StringRoundInputConnector.java index b22426f..f5a4107 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/StringRoundInputConnector.java +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/StringRoundInputConnector.java @@ -72,7 +72,7 @@ public class StringRoundInputConnector implements StringInputConnector { @Override - public void getAttachment(Wire wire) { + public void addAttachment(Wire wire) { _attachments.add(wire); } @@ -92,7 +92,7 @@ public class StringRoundInputConnector implements StringInputConnector { } @Override - public void unlinkWire(Wire wire) { + public void wireUnlinked(Wire wire) { _attachments.remove(wire); } } diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/StringRoundOutputConnector.java b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/StringRoundOutputConnector.java index 8dfb040..57c3742 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/StringRoundOutputConnector.java +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/connectors/StringRoundOutputConnector.java @@ -7,11 +7,12 @@ import com.codigoparallevar.minicards.types.Drawable; import com.codigoparallevar.minicards.types.Moveable; import com.codigoparallevar.minicards.types.Part; import com.codigoparallevar.minicards.types.PartGrid; -import com.codigoparallevar.minicards.types.Tuple2; import com.codigoparallevar.minicards.types.connectors.Wiring.StringWire; +import com.codigoparallevar.minicards.types.connectors.Wiring.Wire; import com.codigoparallevar.minicards.types.connectors.input.InputConnector; import com.codigoparallevar.minicards.types.connectors.input.StringInputConnector; import com.codigoparallevar.minicards.types.connectors.output.StringOutputConnector; +import com.codigoparallevar.minicards.types.functional.Tuple2; import com.codigoparallevar.minicards.types.wireData.StringSignal; import java.util.HashSet; @@ -83,7 +84,7 @@ public class StringRoundOutputConnector implements Drawable, StringOutputConnect } _connections.add(resultPoint); - wire.attachTo(resultPoint); + wire.attachTo(resultPoint, this); _wires.add(wire); } else { @@ -94,8 +95,9 @@ public class StringRoundOutputConnector implements Drawable, StringOutputConnect } @Override - public void unlinkWire(StringWire wire) { + public void wireUnlinked(StringWire wire) { _wires.remove(wire); + _connections.remove(wire.getAttachedTo()); } @Override @@ -141,7 +143,7 @@ public class StringRoundOutputConnector implements Drawable, StringOutputConnect _connections.add(inputConnector); StringWire wire = new StringWire(this, _centerX, _centerY); - wire.attachTo(inputConnector); + wire.attachTo(inputConnector, this); _wires.add(wire); } @@ -155,4 +157,14 @@ public class StringRoundOutputConnector implements Drawable, StringOutputConnect wire.send(signal); } } + + @Override + public Object query(Object lastValue) { + return _part.query(lastValue); + } + + @Override + public List getWires() { + return _wires; + } } diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/logic/Ticker.java b/app/src/main/java/com/codigoparallevar/minicards/parts/logic/Ticker.java index 8b34e01..68c7ab0 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/parts/logic/Ticker.java +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/logic/Ticker.java @@ -7,12 +7,13 @@ import android.util.Log; import com.codigoparallevar.minicards.PartInstantiator; import com.codigoparallevar.minicards.ScrolledCanvas; -import com.codigoparallevar.minicards.parts.connectors.RoundOutputConnector; +import com.codigoparallevar.minicards.parts.connectors.SignalRoundOutputConnector; +import com.codigoparallevar.minicards.parts.style.CardTheme; 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.Tuple2; +import com.codigoparallevar.minicards.types.functional.Tuple2; import com.codigoparallevar.minicards.types.connectors.input.InputConnector; import com.codigoparallevar.minicards.types.connectors.output.OutputConnector; import com.codigoparallevar.minicards.types.wireData.Signal; @@ -39,7 +40,7 @@ public class Ticker implements Part { private int _right; private int _bottom; private List _outputConnectors; - private final RoundOutputConnector _signalOutputConnector; + private final SignalRoundOutputConnector _signalOutputConnector; private final long SLEEP_TIME = 1000; private Thread _thread = null; @@ -52,7 +53,7 @@ public class Ticker implements Part { _bottom = bottom; // Create connectors - _signalOutputConnector = new RoundOutputConnector( + _signalOutputConnector = new SignalRoundOutputConnector( this, _partGrid, getOutputConnectorCenterX(), getOutputConnectorCenterY(), @@ -124,7 +125,7 @@ public class Ticker implements Part { paint); - // Craw a little clock + // Draw a little clock Paint clockPaint = new Paint(Paint.ANTI_ALIAS_FLAG); clockPaint.setStyle(Paint.Style.STROKE); clockPaint.setColor(Color.YELLOW); @@ -144,12 +145,21 @@ public class Ticker implements Part { } private void drawConnector(ScrolledCanvas canvas) { - Paint connectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - connectorPaint.setColor(Color.RED); + Paint outerConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + outerConnectorPaint.setColor(CardTheme.PULSE_CONNECTOR_COLOR_OUTER); - canvas.drawCircle(_right, getOutputConnectorCenterY(), + canvas.drawCircle(getOutputConnectorCenterX(), + getOutputConnectorCenterY(), getOutputConnectRadius(), - connectorPaint); + outerConnectorPaint); + + Paint innerConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + innerConnectorPaint.setColor(CardTheme.PULSE_CONNECTOR_COLOR_INNER); + + canvas.drawCircle(getOutputConnectorCenterX(), + getOutputConnectorCenterY(), + getOutputConnectRadius() / 2, + innerConnectorPaint); } private void drawWires(ScrolledCanvas canvas, boolean devMode) { @@ -261,20 +271,17 @@ public class Ticker implements Part { @Override public void resume() { - _thread = new Thread(new Runnable() { - @Override - public void run() { - while (Ticker.this._thread == Thread.currentThread()) { - Ticker.this._signalOutputConnector.send(new Signal()); - _partGrid.update(); + _thread = new Thread(() -> { + while (Ticker.this._thread == Thread.currentThread()) { + Ticker.this._signalOutputConnector.send(new Signal()); + _partGrid.update(); - try { - Thread.sleep(SLEEP_TIME); - } catch (InterruptedException e) { - Log.e("Minicards Ticker", "Wait failed", e); - Ticker.this._thread = null; - return; - } + try { + Thread.sleep(SLEEP_TIME); + } catch (InterruptedException e) { + Log.e("Minicards Ticker", "Wait failed", e); + Ticker.this._thread = null; + return; } } }); @@ -287,13 +294,18 @@ public class Ticker implements Part { _thread = null; } + @Override + public Object query(Object lastValue) { + return null; // No relevant value (maybe time?) + } + private int getOutputConnectorCenterX() { - return _right; + return (_left + _right) / 2; } private int getOutputConnectorCenterY() { - return (_top + _bottom) / 2; + return _bottom; } private int getOutputConnectRadius() { diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/logic/Toggle.java b/app/src/main/java/com/codigoparallevar/minicards/parts/logic/Toggle.java index a8e9a57..9f7a19d 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/parts/logic/Toggle.java +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/logic/Toggle.java @@ -9,11 +9,12 @@ import com.codigoparallevar.minicards.PartInstantiator; import com.codigoparallevar.minicards.ScrolledCanvas; import com.codigoparallevar.minicards.parts.connectors.BooleanRoundOutputConnector; import com.codigoparallevar.minicards.parts.connectors.RoundInputConnector; +import com.codigoparallevar.minicards.parts.style.CardTheme; 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.Tuple2; +import com.codigoparallevar.minicards.types.functional.Tuple2; import com.codigoparallevar.minicards.types.connectors.input.InputConnector; import com.codigoparallevar.minicards.types.connectors.input.SignalInputConnector; import com.codigoparallevar.minicards.types.connectors.output.OutputConnector; @@ -128,20 +129,41 @@ public class Toggle implements Part { } private void drawConnector(ScrolledCanvas canvas) { - Paint inputConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - inputConnectorPaint.setColor(Color.YELLOW); + // Input + Paint outerInputConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + outerInputConnectorPaint.setColor(CardTheme.PULSE_CONNECTOR_COLOR_OUTER); canvas.drawCircle( - getInputConnectorCenterX(), getInputConnectorCenterY(), + getInputConnectorCenterX(), + getInputConnectorCenterY(), getInputConnectRadius(), - inputConnectorPaint); + outerInputConnectorPaint); - Paint outputConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - outputConnectorPaint.setColor(Color.RED); + Paint innerInputConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + innerInputConnectorPaint.setColor(CardTheme.PULSE_CONNECTOR_COLOR_INNER); - canvas.drawCircle(_right, getOutputConnectorCenterY(), + canvas.drawCircle( + getInputConnectorCenterX(), + getInputConnectorCenterY(), + getInputConnectRadius() / 2, + innerInputConnectorPaint); + + // Output + Paint outerOutputConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + outerOutputConnectorPaint.setColor(CardTheme.BOOLEAN_CONNECTOR_COLOR_OUTER); + + canvas.drawCircle(getOutputConnectorCenterX(), + getOutputConnectorCenterY(), getOutputConnectRadius(), - outputConnectorPaint); + outerOutputConnectorPaint); + + Paint innerOutputConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + innerOutputConnectorPaint.setColor(CardTheme.BOOLEAN_CONNECTOR_COLOR_INNER); + + canvas.drawCircle(getOutputConnectorCenterX(), + getOutputConnectorCenterY(), + getOutputConnectRadius() / 2, + innerOutputConnectorPaint); } private void drawWires(ScrolledCanvas canvas, boolean devMode) { @@ -266,6 +288,11 @@ public class Toggle implements Part { } + @Override + public Object query(Object lastValue) { + return _activated; + } + public static Tuple2> deserialize(PartGrid partGrid, JSONObject data) throws JSONException { String id = data.getString("id"); int left = data.getInt("left"); @@ -306,7 +333,7 @@ public class Toggle implements Part { } public int getInputConnectorCenterX() { - return get_left(); + return (get_left() + get_right()) / 2; } private int getInputConnectRadius() { @@ -314,15 +341,15 @@ public class Toggle implements Part { } public int getInputConnectorCenterY() { - return (get_top() + get_bottom()) / 2; + return get_top(); } private int getOutputConnectorCenterX() { - return _right; + return (get_left() + get_right()) / 2; } private int getOutputConnectorCenterY() { - return (_top + _bottom) / 2; + return get_bottom(); } private int getOutputConnectRadius() { diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/samples/ColorBox.java b/app/src/main/java/com/codigoparallevar/minicards/parts/samples/ColorBox.java index 6c52a5c..fa0256b 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/parts/samples/ColorBox.java +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/samples/ColorBox.java @@ -8,10 +8,11 @@ import android.util.Log; import com.codigoparallevar.minicards.PartInstantiator; import com.codigoparallevar.minicards.ScrolledCanvas; import com.codigoparallevar.minicards.parts.connectors.BooleanRoundInputConnector; +import com.codigoparallevar.minicards.parts.style.CardTheme; import com.codigoparallevar.minicards.types.Moveable; 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; import com.codigoparallevar.minicards.types.connectors.input.BooleanInputConnector; import com.codigoparallevar.minicards.types.connectors.input.InputConnector; import com.codigoparallevar.minicards.types.connectors.output.OutputConnector; @@ -105,13 +106,23 @@ public class ColorBox implements Part { } private void drawConnector(ScrolledCanvas canvas) { - Paint connectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - connectorPaint.setColor(Color.YELLOW); + Paint outerInputConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + outerInputConnectorPaint.setColor(CardTheme.BOOLEAN_CONNECTOR_COLOR_OUTER); canvas.drawCircle( - getInputConnectorCenterX(), getInputConnectorCenterY(), + getInputConnectorCenterX(), + getInputConnectorCenterY(), getInputConnectRadius(), - connectorPaint); + outerInputConnectorPaint); + + Paint innerInputConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + innerInputConnectorPaint.setColor(CardTheme.BOOLEAN_CONNECTOR_COLOR_INNER); + + canvas.drawCircle( + getInputConnectorCenterX(), + getInputConnectorCenterY(), + getInputConnectRadius() / 2, + innerInputConnectorPaint); } @Override @@ -209,6 +220,11 @@ public class ColorBox implements Part { } + @Override + public Object query(Object lastValue) { + return null; + } + public static Part deserialize(PartGrid partGrid, JSONObject data) throws JSONException { String id = data.getString("id"); int left = data.getInt("left"); @@ -238,7 +254,7 @@ public class ColorBox implements Part { } public int getInputConnectorCenterX() { - return get_left(); + return (get_left() + get_right()) / 2; } private int getInputConnectRadius() { @@ -246,7 +262,7 @@ public class ColorBox implements Part { } public int getInputConnectorCenterY() { - return (get_top() + get_bottom()) / 2; + return get_top(); } public static PartInstantiator getInstantiator() { diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/samples/Placeholder.java b/app/src/main/java/com/codigoparallevar/minicards/parts/samples/Placeholder.java deleted file mode 100644 index d496e96..0000000 --- a/app/src/main/java/com/codigoparallevar/minicards/parts/samples/Placeholder.java +++ /dev/null @@ -1,184 +0,0 @@ -package com.codigoparallevar.minicards.parts.samples; - -import android.graphics.Color; -import android.graphics.Paint; -import android.util.Log; - -import com.codigoparallevar.minicards.ScrolledCanvas; -import com.codigoparallevar.minicards.types.Moveable; -import com.codigoparallevar.minicards.types.Part; -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.wireData.WireDataType; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.Collections; -import java.util.List; -import java.util.UUID; - -public class Placeholder implements Part { - private final String _id; - private final PartGrid _partGrid; - private int _left; - private int _top; - private int _right; - private int _bottom; - - public Placeholder(String id, PartGrid partGrid, int left, int top, int right, int bottom) { - _id = id; - _partGrid = partGrid; - _left = left; - _top = top; - _right = right; - _bottom = bottom; - } - - public Placeholder(PartGrid partGrid, int left, int top, int right, int bottom) { - this(UUID.randomUUID().toString(), partGrid, left, top, right, bottom); - } - - @Override - public int get_left() { - return _left; - } - - @Override - public int get_right() { - return _right; - } - - @Override - public int get_top() { - return _top; - } - - @Override - public int get_bottom() { - return _bottom; - } - - @Override - public void draw(ScrolledCanvas canvas, boolean devMode) { - Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); - paint.setColor(Color.WHITE); - - // Top - canvas.drawLine(_left, _top, _right, _top, paint); - // Bottom - canvas.drawLine(_left, _bottom, _right, _bottom, paint); - // Left - canvas.drawLine(_left, _top, _left, _bottom, paint); - // Right - canvas.drawLine(_right, _top, _right, _bottom, paint); - // Cross, top-left, bottom-right - canvas.drawLine(_left, _top, _right, _bottom, paint); - // Cross, top-right, bottom-left - canvas.drawLine(_right, _top, _left, _bottom, paint); - } - - @Override - public void moveEnd(int x, int y) { - final int width = _right - _left; - final int height = _bottom - _top; - - _left = x - width / 2; - _right = _left + width; - - _top = y - height / 2; - _bottom = _top + height; - } - - @Override - public void drop(int x, int y) { - moveEnd(x, y); - } - - @Override - public void touched() { - Log.d("Placeholder", "Placeholder touched"); - } - - @Override - public List getInputConnectors() { - return Collections.emptyList(); - } - - @Override - public List getOutputConnectors() { - return Collections.emptyList(); - } - - @Override - public JSONObject serialize() throws JSONException { - JSONObject serialized = new JSONObject(); - - serialized.put("id", _id); - serialized.put("left", _left); - serialized.put("top", _top); - serialized.put("right", _right); - serialized.put("bottom", _bottom); - - return serialized; - } - - @Override - public void send(InputConnector roundInputConnector, WireDataType signal) { - // @TODO: REMOVE THE NEED FOR THIS - } - - @Override - public String get_id() { - return _id; - } - - @Override - public InputConnector getConnectorWithId(String inputConnectorId) { - return null; - } - - @Override - public String getConnectorId(InputConnector inputConnector) { - return null; - } - - @Override - public void resume() { - - } - - @Override - public void pause() { - - } - - public static Part deserialize(PartGrid partGrid, JSONObject data) throws JSONException { - String id = data.getString("id"); - int left = data.getInt("left"); - int top = data.getInt("top"); - int right = data.getInt("right"); - int bottom = data.getInt("bottom"); - - return new Placeholder(id, partGrid, left, top, right, bottom); - } - - @Override - public boolean containsPoint(int x, int y) { - return (x >= get_left()) && (x <= get_right()) - && (y >= get_top()) && (y <= get_bottom()); - } - - @Override - public Moveable getMoveable() { - return this; - } - - @Override - public void unlink() { - for (InputConnector input : getInputConnectors()) { - input.unlink(); - } - } -} diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/strings/ConvertToString.java b/app/src/main/java/com/codigoparallevar/minicards/parts/strings/ConvertToString.java index dfa7fac..bf4a3aa 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/parts/strings/ConvertToString.java +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/strings/ConvertToString.java @@ -3,21 +3,22 @@ package com.codigoparallevar.minicards.parts.strings; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; -import android.support.annotation.NonNull; import android.util.Log; import com.codigoparallevar.minicards.PartInstantiator; 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.ProgramakerCustomBlockInputConnector; import com.codigoparallevar.minicards.parts.connectors.StringRoundOutputConnector; +import com.codigoparallevar.minicards.parts.style.CardTheme; 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.Tuple2; import com.codigoparallevar.minicards.types.connectors.input.AnyInputConnector; 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.StringSignal; import com.codigoparallevar.minicards.types.wireData.WireDataType; import com.codigoparallevar.minicards.utils.Serializations; @@ -44,8 +45,6 @@ public class ConvertToString implements Part { private int _bottom; private List inputConnectors; private AnyInputConnector _toggleInputConnector; - - @NonNull private String _lastValue; private ConvertToString(String id, PartGrid partGrid, int left, int top, int right, int bottom) { @@ -55,14 +54,15 @@ public class ConvertToString implements Part { _top = top; _right = right; _bottom = bottom; - _lastValue = "-"; + _lastValue = null; // Input connector - _toggleInputConnector = new AnyRoundInputConnector( - this, + ConnectorTypeInfo typeInfo = new ConnectorTypeInfo(ConnectorTypeInfo.Type.ANY); + _toggleInputConnector = new ProgramakerCustomBlockInputConnector( + this, _partGrid, getInputConnectorCenterX(), getInputConnectorCenterY(), - getInputConnectRadius()); + getInputConnectRadius(), typeInfo); inputConnectors = new LinkedList<>(); inputConnectors.add(_toggleInputConnector); @@ -110,7 +110,7 @@ public class ConvertToString implements Part { drawWires(canvas, devMode); Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); - paint.setColor(Color.WHITE); + paint.setColor(Color.BLACK); canvas.drawRect( new Rect(_left, _top, _right, _bottom), @@ -119,28 +119,55 @@ public class ConvertToString implements Part { Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); textPaint.setColor(Color.GREEN); textPaint.setTextSize(100); - canvas.drawText(_lastValue, - _left, - _top - 5, + + String value = "-"; + if (_lastValue != null) { + value = _lastValue; + } + + canvas.drawText(value, + _left + 10, + _bottom - 10, textPaint); } } private void drawConnector(ScrolledCanvas canvas) { - Paint inputConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - inputConnectorPaint.setColor(Color.YELLOW); + // Input + Paint inputOuterConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + inputOuterConnectorPaint.setColor(CardTheme.ANY_CONNECTOR_COLOR_OUTER); canvas.drawCircle( - getInputConnectorCenterX(), getInputConnectorCenterY(), + getInputConnectorCenterX(), + getInputConnectorCenterY(), getInputConnectRadius(), - inputConnectorPaint); + inputOuterConnectorPaint); - Paint outputConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - outputConnectorPaint.setColor(Color.RED); + Paint inputInnerConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + inputInnerConnectorPaint.setColor(CardTheme.ANY_CONNECTOR_COLOR_INNER); - canvas.drawCircle(_right, getOutputConnectorCenterY(), + canvas.drawCircle( + getInputConnectorCenterX(), + getInputConnectorCenterY(), + getInputConnectRadius() / 2, + inputInnerConnectorPaint); + + // Output + Paint outputOuterConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + outputOuterConnectorPaint.setColor(CardTheme.STRING_CONNECTOR_COLOR_OUTER); + + canvas.drawCircle(getOutputConnectorCenterX(), + getOutputConnectorCenterY(), getOutputConnectRadius(), - outputConnectorPaint); + outputOuterConnectorPaint); + + Paint outputInnerConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + outputInnerConnectorPaint.setColor(CardTheme.STRING_CONNECTOR_COLOR_INNER); + + canvas.drawCircle(getOutputConnectorCenterX(), + getOutputConnectorCenterY(), + getOutputConnectRadius() / 2, + outputInnerConnectorPaint); } private void drawWires(ScrolledCanvas canvas, boolean devMode) { @@ -228,12 +255,11 @@ public class ConvertToString implements Part { public void send(InputConnector roundInputConnector, WireDataType signal) { String encoded = "null"; Object value = signal.get(); - if (value != null){ encoded = value.toString(); } - if (!_lastValue.equals(encoded)) { + if ((_lastValue == null) || !_lastValue.equals(encoded)) { _lastValue = encoded; _stringOutputConnector.send(new StringSignal(encoded)); @@ -275,6 +301,11 @@ public class ConvertToString implements Part { } + @Override + public Object query(Object lastValue) { + return _lastValue; + } + public static Tuple2> deserialize(PartGrid partGrid, JSONObject data) throws JSONException { String id = data.getString("id"); int left = data.getInt("left"); @@ -315,7 +346,7 @@ public class ConvertToString implements Part { } public int getInputConnectorCenterX() { - return get_left(); + return (get_left() + get_right()) / 2; } private int getInputConnectRadius() { @@ -323,15 +354,15 @@ public class ConvertToString implements Part { } public int getInputConnectorCenterY() { - return (get_top() + get_bottom()) / 2; + return get_top(); } private int getOutputConnectorCenterX() { - return _right; + return (get_left() + get_right()) / 2; } private int getOutputConnectorCenterY() { - return (_top + _bottom) / 2; + return get_bottom(); } private int getOutputConnectRadius() { diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/style/CardTheme.java b/app/src/main/java/com/codigoparallevar/minicards/parts/style/CardTheme.java new file mode 100644 index 0000000..559780d --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/style/CardTheme.java @@ -0,0 +1,34 @@ +package com.codigoparallevar.minicards.parts.style; + +import android.graphics.Color; + +public class CardTheme { + public static final int DEFAULT_COLOR_INNER = Color.parseColor("#000000"); + + public static final int PULSE_CONNECTOR_COLOR_OUTER = Color.parseColor("#f0c000"); + public static final int PULSE_CONNECTOR_COLOR_INNER = DEFAULT_COLOR_INNER; + + public static final int BOOLEAN_CONNECTOR_COLOR_OUTER = Color.parseColor("#dd4444"); + public static final int BOOLEAN_CONNECTOR_COLOR_INNER = DEFAULT_COLOR_INNER; + + public static final int STRING_CONNECTOR_COLOR_OUTER = Color.parseColor("#44dd44"); + public static final int STRING_CONNECTOR_COLOR_INNER = DEFAULT_COLOR_INNER; + + public static final int ANY_CONNECTOR_COLOR_OUTER = Color.parseColor("#dd44dd"); + public static final int ANY_CONNECTOR_COLOR_INNER = DEFAULT_COLOR_INNER; + + public static final int ENUM_CONNECTOR_COLOR_OUTER = Color.parseColor("#888888"); + public static final int ENUM_CONNECTOR_COLOR_INNER = DEFAULT_COLOR_INNER; + + public static final int FLOAT_CONNECTOR_COLOR_OUTER = Color.parseColor("#44dddd"); + public static final int FLOAT_CONNECTOR_COLOR_INNER = DEFAULT_COLOR_INNER; + + public static final int INTEGER_CONNECTOR_COLOR_OUTER = Color.parseColor("#4444ff"); + public static final int INTEGER_CONNECTOR_COLOR_INNER = DEFAULT_COLOR_INNER; + + public static final int UNKNOWN_CONNECTOR_COLOR_OUTER = Color.parseColor("#7f7f7f"); + public static final int UNKNOWN_CONNECTOR_COLOR_INNER = DEFAULT_COLOR_INNER; + + public static final int IMAGE_CONNECTOR_COLOR_OUTER = Color.parseColor("#007700"); + public static final int IMAGE_CONNECTOR_COLOR_INNER = DEFAULT_COLOR_INNER; +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/values/StaticValuePart.java b/app/src/main/java/com/codigoparallevar/minicards/parts/values/StaticValuePart.java new file mode 100644 index 0000000..1590eca --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/values/StaticValuePart.java @@ -0,0 +1,519 @@ +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; +import com.codigoparallevar.minicards.parts.connectors.AnyRoundOutputConnector; +import com.codigoparallevar.minicards.parts.connectors.ConnectorTypeInfo; +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.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.ProgramakerApi; +import com.programaker.api.data.ProgramakerCustomBlockArgument; +import com.programaker.api.data.ProgramakerCustomBlockArgumentValue; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; + +public class StaticValuePart implements Part { + private static final int WIDTH_PADDING = 25; + private static final int HEIGHT_PADDING = 25; + private static final int MIN_HEIGHT = 100; + private static final int MIN_WIDTH = 100; + private static final String LogTag = "PM StaticValue"; + private static final int IO_RADIUS = 50; + private static final int IO_PADDING = 20; + + private final String _id; + private final AnyRoundOutputConnector _outputConnector; + private final ConnectorTypeInfo _typeInfo; + private final PartGrid _grid; + private int _width; + private int _height; + 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, String valueId) { + this._id = id; + this._typeInfo = typeInfo; + this._grid = grid; + this._value = value; + this._valueId = valueId; + + this.updateWidthHeight(); + + this._left = centerx - this._width / 2; + this._top = centery - this._height / 2; + this._outputConnector = new AnyRoundOutputConnector(this, grid, + 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); + } + + public StaticValuePart(PartGrid grid, int centerx, int centery, ConnectorTypeInfo typeInfo) { + this(UUID.randomUUID().toString(), grid, centerx, centery, typeInfo); + } + + @Override + public JSONObject serialize() throws JSONException { + JSONObject serialized = new JSONObject(); + + serialized.put("id", _id); + 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); + + 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 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, centerx, centery, typeInfo, value, valueId); + + 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); + OutputConnector connector = part.getOutputConnectors().get(i); + + for (int j = 0; j < connectorOuts.length(); j++) { + connections.add(PartConnection.deserialize( + connector, connectorOuts.getJSONObject(j))); + } + } + + return new Tuple2<>(part, connections); + } + + public AnyWire getOutputWire() { + return this._outputConnector.getMoveable(); + } + + @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 void updateWidthHeight() { + Paint p = getTextPaint(); + String message = getMessage(); + Rect bounds = new Rect(); + p.getTextBounds(message, 0, message.length(), bounds); + + this._height = Math.max(MIN_HEIGHT, bounds.height() + HEIGHT_PADDING * 2); + int newWidth = Math.max(MIN_WIDTH, bounds.width() + WIDTH_PADDING * 2); + + if (this._width > 0) { // Re-center block + this._left -= (newWidth - _width) / 2; + } + this._width = newWidth; + } + + 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) { + return "0"; + } + try { + Integer.parseInt(_value); + return _value; + } catch(NumberFormatException ex) { + Log.w(LogTag, "StaticPart (type=number), value found=" + _value, ex); + return "0"; + } + } + else { + if (_value == null) { + return ""; + } + return _value; + } + } + + private Paint getTextPaint() { + Paint p = new Paint(Paint.ANTI_ALIAS_FLAG); + p.setColor(Color.BLACK); + p.setTextSize(50); + return p; + } + + @Override + public void touched() { + if (!(this._grid instanceof View)) { + 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 + } + + switch (type) { + case BOOLEAN: + break; + case INTEGER: + case FLOAT: { + final EditText input = new EditText(ctx); + + input.setText(getMessageForEdit()); + input.setInputType(InputType.TYPE_CLASS_NUMBER); + + dialogAskUserForValue(ctx, input, + () -> { + _value = input.getText().toString(); + }); + break; + } + default: + case ANY: + case UNKNOWN: + case STRING: { + final EditText input = new EditText(ctx); + + input.setText(getMessageForEdit()); + input.setInputType(InputType.TYPE_CLASS_TEXT); + 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; + } + + ProgramakerApi api = _grid.getApi(); + Dialog loadingDialog = createLoadingDialog(ctx); + if (!api.hasCachedAllowedValues(blockArg)) { + loadingDialog.show(); + } + + new GetAsync>().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, + new Tuple3<>( + () -> api.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; + } + } + + private Dialog createItemDialog(Context ctx, CharSequence[] itemNames, Consumer onSelected) { + final AlertDialog.Builder builder = new AlertDialog.Builder(ctx); + + builder.setTitle(R.string.set_value); + + 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(); + this.updateWidthHeight(); + ((View) this._grid).invalidate(); + }) + .setNegativeButton(R.string.cancel_discard_changes, (dialog, which) -> { + // No change + }) + .setView(input); + + AlertDialog dialog = builder.create(); + dialog.show(); + + } + + @Override + public List getInputConnectors() { + // There is not going to be one + return Collections.emptyList(); + } + + @Override + public List getOutputConnectors() { + return new LinkedList() {{ + add(_outputConnector); + }}; + } + + public AnyRoundOutputConnector getValueOutput() { + return _outputConnector; + } + + @Override + public void send(InputConnector inputConnector, WireDataType signal) { + + } + + @Override + public String get_id() { + return _id; + } + + @Override + public InputConnector getConnectorWithId(String inputConnectorId) { + return null; // No inputs + } + + @Override + public String getConnectorId(InputConnector inputConnector) { + return null; // No inputs + } + + @Override + public void resume() { + // Nothing to do + } + + @Override + public void pause() { + // Nothing to do + } + + @Override + public Object query(Object lastValue) { + if (this._valueId != null) { + return this._valueId; + } + + return this._value; + } + + @Override + public void draw(ScrolledCanvas canvas, boolean devMode) { + this.updateWidthHeight(); // TODO: Remove after calculations stabilize + + if (!devMode) { + return; // Logic block, don't show on user-mode + } + + drawConnectors(canvas); + drawWires(canvas, devMode); + + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setColor(Color.parseColor("#E3FCFC")); + canvas.drawRect( + new Rect(get_left(), get_top(), + get_right(), get_bottom()), + paint); + + Paint textPaint = getTextPaint(); + canvas.drawText(getMessage(), + get_left() + WIDTH_PADDING, + get_bottom() - HEIGHT_PADDING, + textPaint); + } + + private void drawConnectors(ScrolledCanvas canvas) { + Paint outerOutputConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + outerOutputConnectorPaint.setColor(_typeInfo.getOuterColor()); + + canvas.drawCircle( + _outputConnector.getX(), + _outputConnector.getY(), + _outputConnector.getRadius(), + outerOutputConnectorPaint); + + Paint innerOutputConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + innerOutputConnectorPaint.setColor(_typeInfo.getInnerColor()); + + canvas.drawCircle( + _outputConnector.getX(), + _outputConnector.getY(), + _outputConnector.getRadius() / 2, + innerOutputConnectorPaint); + } + + private void drawWires(ScrolledCanvas canvas, boolean devMode) { + _outputConnector.drawWires(canvas, devMode); + } + + @Override + public void moveEnd(int x, int y) { + _left = x - _width / 2; + _top = y - _height / 2; + this._outputConnector.updatePosition( + this._left + _width / 2, + this._top + _height); + } + + @Override + public void drop(int x, int y) { + moveEnd(x, y); + } + + @Override + public boolean containsPoint(int x, int y) { + return ((x >= this.get_left()) && (x <= this.get_right()) + && (y >= this.get_top()) && (y <= this.get_bottom())); + } + + @Override + public Moveable getMoveable() { + return this; + } + + @Override + public void unlink() { + // Nothing to do + _outputConnector.unlink(); + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/parts/viewers/ImageFrame.java b/app/src/main/java/com/codigoparallevar/minicards/parts/viewers/ImageFrame.java new file mode 100644 index 0000000..9e3e86e --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/parts/viewers/ImageFrame.java @@ -0,0 +1,316 @@ +package com.codigoparallevar.minicards.parts.viewers; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.ImageFormat; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.media.Image; +import android.os.Build; +import android.util.Log; + +import com.codigoparallevar.minicards.PartInstantiator; +import com.codigoparallevar.minicards.ScrolledCanvas; +import com.codigoparallevar.minicards.parts.connectors.ImageRoundInputConnector; +import com.codigoparallevar.minicards.parts.style.CardTheme; +import com.codigoparallevar.minicards.types.Moveable; +import com.codigoparallevar.minicards.types.Part; +import com.codigoparallevar.minicards.types.PartGrid; +import com.codigoparallevar.minicards.types.connectors.input.ImageInputConnector; +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.ImageSignal; +import com.codigoparallevar.minicards.types.wireData.WireDataType; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; + +public class ImageFrame implements Part { + private static final String LogTag = "ImageFrame"; + private static final int DEFAULT_SIDE_SIZE = 400; + private static final String IMAGE_INPUT_CONNECTOR_ID = "image_input_connector"; + + private final String _id; + private final PartGrid _partGrid; + private int _left; + private int _top; + private int _right; + private int _bottom; + private List inputConnectors; + private ImageInputConnector _imageInputConnector; + private Bitmap lastImage; + + public ImageFrame(String id, PartGrid partGrid, int left, int top, int right, int bottom) { + _id = id; + _partGrid = partGrid; + _left = left; + _top = top; + _right = right; + _bottom = bottom; + + _imageInputConnector = new ImageRoundInputConnector( + this, + getInputConnectorCenterX(), + getInputConnectorCenterY(), + getInputConnectRadius()); + inputConnectors = new LinkedList<>(); + inputConnectors.add(_imageInputConnector); + } + + public ImageFrame(PartGrid partGrid, int left, int top, int right, int bottom) { + this(UUID.randomUUID().toString(), partGrid, left, top, right, bottom); + } + + @Override + public int get_left() { + return _left; + } + + @Override + public int get_right() { + return _right; + } + + @Override + public int get_top() { + return _top; + } + + @Override + public int get_bottom() { + return _bottom; + } + + @Override + public void draw(ScrolledCanvas canvas, boolean devMode) { + if (devMode){ + drawConnector(canvas); + // drawWires(canvas, devMode); + } + + Rect frameRect = new Rect(_left, _top, _right, _bottom); + if (this.lastImage == null) { + Paint backdropPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + backdropPaint.setColor(Color.parseColor("#444444")); + + canvas.drawRoundRect(new RectF(frameRect), 50, 50, backdropPaint); + + Paint crossPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + crossPaint.setColor(Color.WHITE); + + // Top + canvas.drawLine(_left, _top, _right, _top, crossPaint); + // Bottom + canvas.drawLine(_left, _bottom, _right, _bottom, crossPaint); + // Left + canvas.drawLine(_left, _top, _left, _bottom, crossPaint); + // Right + canvas.drawLine(_right, _top, _right, _bottom, crossPaint); + // Cross, top-left, bottom-right + canvas.drawLine(_left, _top, _right, _bottom, crossPaint); + // Cross, top-right, bottom-left + canvas.drawLine(_right, _top, _left, _bottom, crossPaint); + } + else { + canvas.drawBitmap(this.lastImage, frameRect); + } + } + + private void drawConnector(ScrolledCanvas canvas) { + Paint outerInputConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + outerInputConnectorPaint.setColor(CardTheme.IMAGE_CONNECTOR_COLOR_OUTER); + + canvas.drawCircle( + getInputConnectorCenterX(), + getInputConnectorCenterY(), + getInputConnectRadius(), + outerInputConnectorPaint); + + Paint innerInputConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + innerInputConnectorPaint.setColor(CardTheme.IMAGE_CONNECTOR_COLOR_INNER); + + canvas.drawCircle( + getInputConnectorCenterX(), + getInputConnectorCenterY(), + getInputConnectRadius() / 2, + innerInputConnectorPaint); + } + + @Override + public void moveEnd(int x, int y) { + final int width = _right - _left; + final int height = _bottom - _top; + + _left = x - width / 2; + _right = _left + width; + + _top = y - height / 2; + _bottom = _top + height; + + _imageInputConnector.updatePosition( + getInputConnectorCenterX(), + getInputConnectorCenterY()); + } + + @Override + public void drop(int x, int y) { + moveEnd(x, y); + } + + @Override + public void touched() { + Log.d(LogTag, "Touched"); + } + + @Override + public List getInputConnectors() { + return inputConnectors; + } + + @Override + public List getOutputConnectors() { + return Collections.emptyList(); + } + + @Override + public JSONObject serialize() throws JSONException { + JSONObject serialized = new JSONObject(); + + serialized.put("id", _id); + serialized.put("left", _left); + serialized.put("top", _top); + serialized.put("right", _right); + serialized.put("bottom", _bottom); + + return serialized; + } + + @Override + public void send(InputConnector roundInputConnector, WireDataType signal) { + if (!(signal instanceof ImageSignal)) { + Log.e(LogTag, "Mismatched type, expected {ImageSignal}, found {" + signal + "}" ); + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + // No way to get the format? + return; + } + + ImageSignal imgSignal = (ImageSignal) signal; + Image image = imgSignal.get(); + if (image.getFormat() == ImageFormat.JPEG) { + Image.Plane[] planes = image.getPlanes(); + if (planes.length != 1) { + // TODO: Handle this case (what would it mean?) + return ; + } + + ByteBuffer jpegBuffer = planes[0].getBuffer(); + byte[] jpegData = new byte[jpegBuffer.remaining()]; + jpegBuffer.get(jpegData); + + Bitmap bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length); + this.lastImage = bitmap; + this._partGrid.update(); // Maybe this can be avoided? Directly copying the data on the canvas? + } + } + + @Override + public String get_id() { + return _id; + } + + @Override + public InputConnector getConnectorWithId(String inputConnectorId) { + switch (inputConnectorId){ + case ImageFrame.IMAGE_INPUT_CONNECTOR_ID: + return _imageInputConnector; + + default: + return null; + } + } + + @Override + public String getConnectorId(InputConnector inputConnector) { + if (inputConnector == _imageInputConnector){ + return ImageFrame.IMAGE_INPUT_CONNECTOR_ID; + } + + return null; + } + @Override + public void resume() { + + } + + @Override + public void pause() { + + } + + @Override + public Object query(Object lastValue) { + return null; + } + + public static Part deserialize(PartGrid partGrid, JSONObject data) throws JSONException { + String id = data.getString("id"); + int left = data.getInt("left"); + int top = data.getInt("top"); + int right = data.getInt("right"); + int bottom = data.getInt("bottom"); + + return new ImageFrame(id, partGrid, left, top, right, bottom); + } + + @Override + public boolean containsPoint(int x, int y) { + return (x >= get_left()) && (x <= get_right()) + && (y >= get_top()) && (y <= get_bottom()); + } + + @Override + public Moveable getMoveable() { + return this; + } + + @Override + public void unlink() { + for (InputConnector input : getInputConnectors()) { + input.unlink(); + } + } + + public int getInputConnectorCenterX() { + return (get_left() + get_right()) / 2; + } + + private int getInputConnectRadius() { + return (get_right() - get_left()) / 4; + } + + public int getInputConnectorCenterY() { + return get_top(); + } + + public static PartInstantiator getInstantiator() { + final int halfSideSize = DEFAULT_SIDE_SIZE / 2; + return new PartInstantiator() { + @Override + protected Part instantiate(PartGrid grid, Tuple2 center) { + return new ImageFrame(grid, + center.item1 - halfSideSize, center.item2 - halfSideSize, + center.item1 + halfSideSize, center.item2 + halfSideSize); + } + }; + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/toolbox/PartCategory.java b/app/src/main/java/com/codigoparallevar/minicards/toolbox/PartCategory.java new file mode 100644 index 0000000..aff6e14 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/toolbox/PartCategory.java @@ -0,0 +1,24 @@ +package com.codigoparallevar.minicards.toolbox; + +import com.codigoparallevar.minicards.PartInstantiator; +import com.codigoparallevar.minicards.types.functional.Tuple2; + +import java.util.List; + +class PartCategory { + private final String _name; + private final List> _parts; + + public PartCategory(String name, List> parts) { + this._name = name; + this._parts = parts; + } + + public String getName() { + return _name; + } + + public List> getParts() { + return this._parts; + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/toolbox/PartShowcaseView.java b/app/src/main/java/com/codigoparallevar/minicards/toolbox/PartShowcaseView.java new file mode 100644 index 0000000..5d7fbc1 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/toolbox/PartShowcaseView.java @@ -0,0 +1,101 @@ +package com.codigoparallevar.minicards.toolbox; + +import android.content.Context; +import android.graphics.Canvas; +import android.view.View; + +import com.codigoparallevar.minicards.PartInstantiator; +import com.codigoparallevar.minicards.ScrolledCanvas; +import com.codigoparallevar.minicards.SignalListenerManager; +import com.codigoparallevar.minicards.types.Part; +import com.codigoparallevar.minicards.types.PartGrid; +import com.codigoparallevar.minicards.types.Selectable; +import com.codigoparallevar.minicards.types.connectors.input.AnyInputConnector; +import com.codigoparallevar.minicards.types.connectors.input.BooleanInputConnector; +import com.codigoparallevar.minicards.types.connectors.input.ImageInputConnector; +import com.codigoparallevar.minicards.types.connectors.input.SignalInputConnector; +import com.codigoparallevar.minicards.types.connectors.input.StringInputConnector; +import com.codigoparallevar.minicards.types.functional.Tuple2; +import com.programaker.api.ProgramakerApi; + +class PartShowcaseView extends View implements PartGrid { + private final Context ctx; + private final PartInstantiator instantiator; + private final Part part; + private final int partWidth; + private Tuple2 center = new Tuple2<>(0, 0); + + public PartShowcaseView(Context ctx, PartInstantiator instantiator) { + super(ctx); + + this.ctx = ctx; + this.instantiator = instantiator; + this.part = instantiator.build(this); + this.part.pause(); + int height = this.part.get_bottom() - this.part.get_top(); + partWidth = this.part.get_right() - this.part.get_left(); + + this.setMinimumHeight(height * 2); + this.setMinimumWidth(partWidth); + } + + @Override + public void onDraw(Canvas canvas) { + int centerx = - Math.max(canvas.getWidth(), partWidth) / 2; + int centery = - canvas.getHeight() / 2; + this.center = new Tuple2<>(centerx, centery); + ScrolledCanvas scrolledCanvas = new ScrolledCanvas(canvas, getCenteredOn()); + this.part.draw(scrolledCanvas, true); + } + + // Most interactive functionalities are not implemented, as it's not needed for the showcase + @Override + public Selectable getPartOn(int x, int y) { + return null; + } + + @Override + public ProgramakerApi getApi() { + return null; + } + + @Override + public SignalListenerManager getListenerManager() { + return null; + } + + @Override + public SignalInputConnector getSignalInputConnectorOn(int x, int y) { + return null; + } + + @Override + public BooleanInputConnector getBooleanInputConnectorOn(int x, int y) { + return null; + } + + @Override + public AnyInputConnector getAnyInputConnectorOn(int x, int y) { + return null; + } + + @Override + public StringInputConnector getStringInputConnectorOn(int xEnd, int yEnd) { + return null; + } + + @Override + public ImageInputConnector getImageInputConnectorOn(int xEnd, int yEnd) { + return null; + } + + @Override + public Tuple2 getCenteredOn() { + return center; + } + @Override + + public void update() { + + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/toolbox/PartsHolder.java b/app/src/main/java/com/codigoparallevar/minicards/toolbox/PartsHolder.java new file mode 100644 index 0000000..ad40bce --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/toolbox/PartsHolder.java @@ -0,0 +1,214 @@ +package com.codigoparallevar.minicards.toolbox; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.Color; +import android.os.Build; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import androidx.appcompat.app.AlertDialog; + +import com.codigoparallevar.minicards.CanvasView; +import com.codigoparallevar.minicards.PartInstantiator; +import com.codigoparallevar.minicards.parts.android.CameraStreamer; +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.parts.viewers.ImageFrame; +import com.codigoparallevar.minicards.types.Part; +import com.codigoparallevar.minicards.types.functional.Consumer; +import com.codigoparallevar.minicards.types.functional.Tuple2; +import com.codigoparallevar.minicards.utils.Box; +import com.programaker.api.data.ProgramakerBridgeInfo; +import com.programaker.api.data.ProgramakerCustomBlock; +import com.programaker.api.data.api_results.ProgramakerBridgeCustomBlockResult; + +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Vector; + +public class PartsHolder { + private final Context context; + + private final static List> GuiParts = + new Vector>(){{ + add(new Tuple2<>("Round button", RoundButton.getInstantiator())); + add(new Tuple2<>("Red/Green box", ColorBox.getInstantiator())); + add(new Tuple2<>("Image frame", ImageFrame.getInstantiator())); + }}; + + private final static List> AndroidParts = + new Vector>(){{ + add(new Tuple2<>("Camera", CameraStreamer.getInstantiator())); + }}; + + private final static List> MiscParts = + new Vector>(){{ + add(new Tuple2<>("Ticker", Ticker.getInstantiator())); + add(new Tuple2<>("Toggle", Toggle.getInstantiator())); + }}; + + private final static List> DebuggingParts = + new Vector>(){{ + add(new Tuple2<>("ToString", ConvertToString.getInstantiator())); + }}; + + + private final List Categories = new Vector() {{ + add(new PartCategory("GUI elements", GuiParts)); + add(new PartCategory("Android", AndroidParts)); + add(new PartCategory("Misc", MiscParts)); + add(new PartCategory("Debugging", DebuggingParts)); + }}; + + private Map bridgeInfoMap; + private final static String LogTag = "PartsHolder"; + + public PartsHolder(Context context) { + this.context = context; + } + + public void openAddPartModal(final CanvasView canvasView) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + + // HACK : Nasty trick to make the accordion be able to close a dialog that still doesn't exist + Box openedDialog = new Box<>(); + Consumer onComplete = _void -> { + if (openedDialog.get() == null) { + Log.e(LogTag, "Expected dialog to be opened already"); + } + else { + openedDialog.get().cancel(); + } + }; + + View categoryAccordion = generateCategoryAccordion(canvasView, onComplete); + + builder.setTitle("Choose part") + .setView(categoryAccordion); + + Dialog dialog = builder.create(); + openedDialog.set(dialog); + dialog.show(); + } + + private View generateCategoryAccordion(final CanvasView canvasView, Consumer signalComplete) { + ScrollView view = new ScrollView(context); + LinearLayout layout = new LinearLayout(context); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setDividerPadding(10); + layout.setShowDividers(LinearLayout.SHOW_DIVIDER_BEGINNING | LinearLayout.SHOW_DIVIDER_MIDDLE); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + layout.setDividerDrawable(context.getDrawable(android.R.drawable.divider_horizontal_bright)); + } + + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + + for (int i = 0; i < Categories.size(); i++) { + PartCategory category = Categories.get(i); + + LinearLayout categoryRow = new LinearLayout(context); + LinearLayout showcases = new LinearLayout(context); + + TextView categoryName = new TextView(context); //Creating Button + + // Add category name + categoryName.setId(i); //Setting Id for using in future + categoryName.setText(category.getName()); //Setting text + categoryName.setTextSize(15); //Text Size + categoryName.setPadding(50, 50, 50, 50); //padding + categoryName.setLayoutParams(params); //Setting Layout Params + categoryName.setTextColor(Color.parseColor("#000000")); //Text Color + categoryName.setBackgroundColor(Color.parseColor("#ffffff")); + categoryName.setGravity(Gravity.CENTER); + categoryName.setClickable(true); + categoryName.setOnClickListener(v -> { + if (showcases.getVisibility() == View.GONE) { + showcases.setVisibility(View.VISIBLE); + } + else { + showcases.setVisibility(View.GONE); + } + }); + + showcases.setVisibility(View.GONE); + + showcases.setBackgroundColor(Color.parseColor("#BBBBBB")); + showcases.setOrientation(LinearLayout.VERTICAL); + showcases.setDividerPadding(10); + showcases.setShowDividers(LinearLayout.SHOW_DIVIDER_BEGINNING | LinearLayout.SHOW_DIVIDER_MIDDLE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + showcases.setDividerDrawable(context.getDrawable(android.R.drawable.divider_horizontal_bright)); + } + + // Add showcases + int partHeights = 0; + for (Tuple2 entry : category.getParts()) { + PartShowcaseView showcase = new PartShowcaseView(context, entry.item2); + + showcase.setClickable(true); + showcase.setOnClickListener(v -> { + Part part = entry.item2.build(canvasView); + canvasView.addPart(part); + try { + signalComplete.apply(null); + } catch (Exception e) { + e.printStackTrace(); + } + }); + partHeights += showcase.getHeight(); + + showcases.addView(showcase); + } + showcases.setMinimumHeight(partHeights); + + // Tie everything + categoryRow.setOrientation(LinearLayout.VERTICAL); + categoryRow.addView(categoryName); + showcases.setOrientation(LinearLayout.VERTICAL); + categoryRow.addView(showcases); + + layout.addView(categoryRow); + } + + view.addView(layout); + return view; + } + + public void addCustomBlocks(List bridgeInfo, List customBlocks) { + Map bridgeInfoMap = new LinkedHashMap<>(); + for (ProgramakerBridgeInfo info : bridgeInfo) { + bridgeInfoMap.put(info.getId(), info); + } + + this.bridgeInfoMap = bridgeInfoMap; + + for (ProgramakerBridgeCustomBlockResult res : customBlocks) { + + List> parts = new LinkedList<>(); + for (ProgramakerCustomBlock block : res.getBlocks()) { + parts.add(new Tuple2<>(block.getMessage(), new ProgramakerCustomBlockInstantiator(block))); + } + + if (bridgeInfoMap.containsKey(res.getBridge_id())) { + Categories.add(new PartCategory(bridgeInfoMap.get(res.getBridge_id()).getName(), parts)); + } + else { + Categories.add(new PartCategory("Bridge ID="+res.getBridge_id(), parts)); + } + } + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/toolbox/ProgramakerCustomBlockInstantiator.java b/app/src/main/java/com/codigoparallevar/minicards/toolbox/ProgramakerCustomBlockInstantiator.java new file mode 100644 index 0000000..e08bb39 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/toolbox/ProgramakerCustomBlockInstantiator.java @@ -0,0 +1,22 @@ +package com.codigoparallevar.minicards.toolbox; + +import com.codigoparallevar.minicards.PartInstantiator; +import com.codigoparallevar.minicards.parts.ProgramakerCustomBlockPart; +import com.codigoparallevar.minicards.parts.buttons.RoundButton; +import com.codigoparallevar.minicards.types.Part; +import com.codigoparallevar.minicards.types.PartGrid; +import com.codigoparallevar.minicards.types.functional.Tuple2; +import com.programaker.api.data.ProgramakerCustomBlock; + +class ProgramakerCustomBlockInstantiator extends PartInstantiator { + private final ProgramakerCustomBlock _block; + + public ProgramakerCustomBlockInstantiator(ProgramakerCustomBlock block) { + this._block = block; + } + + @Override + protected Part instantiate(PartGrid grid, Tuple2 center) { + return new ProgramakerCustomBlockPart(grid, center, this._block); + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/Dropper.java b/app/src/main/java/com/codigoparallevar/minicards/types/Dropper.java index 7aea9ee..78cec50 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/types/Dropper.java +++ b/app/src/main/java/com/codigoparallevar/minicards/types/Dropper.java @@ -4,5 +4,5 @@ import com.codigoparallevar.minicards.types.connectors.Wiring.Wire; public interface Dropper { void drop(T wire); - void unlinkWire(T wire); + void wireUnlinked(T wire); } diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/Part.java b/app/src/main/java/com/codigoparallevar/minicards/types/Part.java index dde9f01..dc1bf25 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/types/Part.java +++ b/app/src/main/java/com/codigoparallevar/minicards/types/Part.java @@ -4,6 +4,7 @@ import com.codigoparallevar.minicards.types.connectors.input.InputConnector; import com.codigoparallevar.minicards.types.connectors.output.OutputConnector; import com.codigoparallevar.minicards.types.wireData.WireDataType; +import org.jetbrains.annotations.NotNull; import org.json.JSONException; import org.json.JSONObject; @@ -20,6 +21,7 @@ public interface Part extends Selectable, Moveable, Drawable { List getInputConnectors(); List getOutputConnectors(); + @NotNull JSONObject serialize() throws JSONException; void send(InputConnector inputConnector, WireDataType signal); @@ -32,4 +34,6 @@ public interface Part extends Selectable, Moveable, Drawable { void resume(); void pause(); + + Object query(Object lastValue); } diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/PartConnection.java b/app/src/main/java/com/codigoparallevar/minicards/types/PartConnection.java index a5bdbd4..8c2dd16 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/types/PartConnection.java +++ b/app/src/main/java/com/codigoparallevar/minicards/types/PartConnection.java @@ -38,4 +38,17 @@ public class PartConnection { return serialized; } + + public static JSONObject serializeToJson(final String inputConnectorId, final String inputPartId) { + + JSONObject serialized = new JSONObject(); + try { + serialized.put(PartConnection.INPUT_CONNECTOR_ID_KEY, inputConnectorId); + serialized.put(PartConnection.INPUT_PART_ID_KEY, inputPartId); + } catch (JSONException e) { + e.printStackTrace(); + } + + return serialized; + } } diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/PartGrid.java b/app/src/main/java/com/codigoparallevar/minicards/types/PartGrid.java index b0a0161..7c08c2e 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/types/PartGrid.java +++ b/app/src/main/java/com/codigoparallevar/minicards/types/PartGrid.java @@ -1,18 +1,26 @@ package com.codigoparallevar.minicards.types; +import com.codigoparallevar.minicards.SignalListenerManager; +import com.codigoparallevar.minicards.types.connectors.input.AnyInputConnector; import com.codigoparallevar.minicards.types.connectors.input.BooleanInputConnector; +import com.codigoparallevar.minicards.types.connectors.input.ImageInputConnector; import com.codigoparallevar.minicards.types.connectors.input.SignalInputConnector; import com.codigoparallevar.minicards.types.connectors.input.StringInputConnector; +import com.codigoparallevar.minicards.types.functional.Tuple2; +import com.programaker.api.ProgramakerApi; public interface PartGrid { Selectable getPartOn(int x, int y); + ProgramakerApi getApi(); + SignalListenerManager getListenerManager(); SignalInputConnector getSignalInputConnectorOn(int x, int y); BooleanInputConnector getBooleanInputConnectorOn(int x, int y); + AnyInputConnector getAnyInputConnectorOn(int x, int y); + StringInputConnector getStringInputConnectorOn(int xEnd, int yEnd); + ImageInputConnector getImageInputConnectorOn(int xEnd, int yEnd); Tuple2 getCenteredOn(); void update(); - - StringInputConnector getStringInputConnectorOn(int xEnd, int yEnd); } diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/Tuple2.java b/app/src/main/java/com/codigoparallevar/minicards/types/Tuple2.java deleted file mode 100644 index 081350a..0000000 --- a/app/src/main/java/com/codigoparallevar/minicards/types/Tuple2.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.codigoparallevar.minicards.types; - -public class Tuple2 { - public final T1 item1; - public final T2 item2; - - public Tuple2(T1 item1, T2 item2) { - this.item1 = item1; - this.item2 = item2; - } -} diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/connectors/RoundConnector.java b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/RoundConnector.java new file mode 100644 index 0000000..042bac9 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/RoundConnector.java @@ -0,0 +1,7 @@ +package com.codigoparallevar.minicards.types.connectors; + +public interface RoundConnector { + int getX(); + int getY(); + float getRadius(); +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/connectors/Wiring/AnyWire.java b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/Wiring/AnyWire.java new file mode 100644 index 0000000..cc2e30a --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/Wiring/AnyWire.java @@ -0,0 +1,19 @@ +package com.codigoparallevar.minicards.types.connectors.Wiring; + +import com.codigoparallevar.minicards.types.Drawable; +import com.codigoparallevar.minicards.types.Moveable; +import com.codigoparallevar.minicards.types.connectors.input.AnyInputConnector; +import com.codigoparallevar.minicards.types.connectors.output.OutputConnector; +import com.codigoparallevar.minicards.types.wireData.AnySignal; + +public class AnyWire extends Wire implements Moveable, Drawable { + public AnyWire(OutputConnector dropper, int xInit, int yInit) { + super(dropper, xInit, yInit); + } + + public void send(AnySignal signal) { + if (_attachedTo != null) { + _attachedTo.send(signal); + } + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/connectors/Wiring/BooleanWire.java b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/Wiring/BooleanWire.java index 9275c73..ff1b1f8 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/types/connectors/Wiring/BooleanWire.java +++ b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/Wiring/BooleanWire.java @@ -1,13 +1,13 @@ package com.codigoparallevar.minicards.types.connectors.Wiring; import com.codigoparallevar.minicards.types.Drawable; -import com.codigoparallevar.minicards.types.Dropper; import com.codigoparallevar.minicards.types.Moveable; import com.codigoparallevar.minicards.types.connectors.input.BooleanInputConnector; +import com.codigoparallevar.minicards.types.connectors.output.OutputConnector; import com.codigoparallevar.minicards.types.wireData.BooleanSignal; public class BooleanWire extends Wire implements Moveable, Drawable { - public BooleanWire(Dropper dropper, int xInit, int yInit) { + public BooleanWire(OutputConnector dropper, int xInit, int yInit) { super(dropper, xInit, yInit); } diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/connectors/Wiring/ImageWire.java b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/Wiring/ImageWire.java new file mode 100644 index 0000000..2874546 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/Wiring/ImageWire.java @@ -0,0 +1,19 @@ +package com.codigoparallevar.minicards.types.connectors.Wiring; + +import com.codigoparallevar.minicards.types.Drawable; +import com.codigoparallevar.minicards.types.Moveable; +import com.codigoparallevar.minicards.types.connectors.input.ImageInputConnector; +import com.codigoparallevar.minicards.types.connectors.output.OutputConnector; +import com.codigoparallevar.minicards.types.wireData.ImageSignal; + +public class ImageWire extends Wire implements Moveable, Drawable { + public ImageWire(OutputConnector dropper, int xInit, int yInit) { + super(dropper, xInit, yInit); + } + + public void send(ImageSignal signal) { + if (_attachedTo != null) { + _attachedTo.send(signal); + } + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/connectors/Wiring/SignalWire.java b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/Wiring/SignalWire.java index 61e767c..d256b18 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/types/connectors/Wiring/SignalWire.java +++ b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/Wiring/SignalWire.java @@ -1,13 +1,13 @@ package com.codigoparallevar.minicards.types.connectors.Wiring; import com.codigoparallevar.minicards.types.Drawable; -import com.codigoparallevar.minicards.types.Dropper; import com.codigoparallevar.minicards.types.Moveable; import com.codigoparallevar.minicards.types.connectors.input.SignalInputConnector; +import com.codigoparallevar.minicards.types.connectors.output.OutputConnector; import com.codigoparallevar.minicards.types.wireData.Signal; public class SignalWire extends Wire implements Moveable, Drawable { - public SignalWire(Dropper dropper, int xInit, int yInit) { + public SignalWire(OutputConnector dropper, int xInit, int yInit) { super(dropper, xInit, yInit); } diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/connectors/Wiring/StringWire.java b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/Wiring/StringWire.java index 9ccd9d5..95c1434 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/types/connectors/Wiring/StringWire.java +++ b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/Wiring/StringWire.java @@ -1,13 +1,13 @@ package com.codigoparallevar.minicards.types.connectors.Wiring; import com.codigoparallevar.minicards.types.Drawable; -import com.codigoparallevar.minicards.types.Dropper; import com.codigoparallevar.minicards.types.Moveable; import com.codigoparallevar.minicards.types.connectors.input.StringInputConnector; +import com.codigoparallevar.minicards.types.connectors.output.OutputConnector; import com.codigoparallevar.minicards.types.wireData.StringSignal; public class StringWire extends Wire implements Moveable, Drawable { - public StringWire(Dropper dropper, int xInit, int yInit) { + public StringWire(OutputConnector dropper, int xInit, int yInit) { super(dropper, xInit, yInit); } diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/connectors/Wiring/Wire.java b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/Wiring/Wire.java index 10cf453..f58c8f3 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/types/connectors/Wiring/Wire.java +++ b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/Wiring/Wire.java @@ -3,24 +3,31 @@ package com.codigoparallevar.minicards.types.connectors.Wiring; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; +import android.graphics.RectF; import com.codigoparallevar.minicards.ScrolledCanvas; import com.codigoparallevar.minicards.types.Drawable; import com.codigoparallevar.minicards.types.Dropper; import com.codigoparallevar.minicards.types.Moveable; +import com.codigoparallevar.minicards.types.Selectable; import com.codigoparallevar.minicards.types.connectors.input.InputConnector; +import com.codigoparallevar.minicards.types.connectors.output.OutputConnector; import com.codigoparallevar.minicards.types.wireData.WireDataType; public class Wire> - implements Moveable, Drawable { + implements Moveable, Drawable, Selectable { + + private final double MAX_DISTANCE_TO_CONTAINS = 15; private final Dropper _dropper; private int _xinit; private int _yinit; private int _xend; private int _yend; + private Path path = null; private final static int _pathRunWay = 100; protected InputConnectorType _attachedTo = null; + protected OutputConnector _attachedFrom = null; public Wire(Dropper dropper, int xInit, int yInit) { _dropper = dropper; @@ -34,12 +41,14 @@ public class Wire { +public abstract class AnyInputConnector implements InputConnector { public SignalInputConnector ToSignalInputConnector() { return new WrapAsSignal(this); @@ -51,7 +51,7 @@ public abstract class AnyInputConnector implements InputConnector { +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/connectors/input/InputConnector.java b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/input/InputConnector.java index 125a994..35c4ab1 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/types/connectors/input/InputConnector.java +++ b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/input/InputConnector.java @@ -12,13 +12,16 @@ public interface InputConnector wire); + void addAttachment(Wire wire); Part getPart(); String getId(); void send(T data); + + void wireUnlinked(Wire wire); } diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/connectors/input/SignalInputConnector.java b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/input/SignalInputConnector.java index 39b1378..bcd4c05 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/types/connectors/input/SignalInputConnector.java +++ b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/input/SignalInputConnector.java @@ -1,5 +1,6 @@ package com.codigoparallevar.minicards.types.connectors.input; +import com.codigoparallevar.minicards.types.connectors.Wiring.SignalWire; import com.codigoparallevar.minicards.types.wireData.Signal; public interface SignalInputConnector extends InputConnector { diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/connectors/output/AnyOutputConnector.java b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/output/AnyOutputConnector.java new file mode 100644 index 0000000..e6d9b02 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/output/AnyOutputConnector.java @@ -0,0 +1,8 @@ +package com.codigoparallevar.minicards.types.connectors.output; + +import com.codigoparallevar.minicards.types.connectors.Wiring.AnyWire; +import com.codigoparallevar.minicards.types.connectors.input.AnyInputConnector; +import com.codigoparallevar.minicards.types.wireData.AnySignal; + +public interface AnyOutputConnector extends OutputConnector { +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/connectors/output/ImageOutputConnector.java b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/output/ImageOutputConnector.java new file mode 100644 index 0000000..d60ce77 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/output/ImageOutputConnector.java @@ -0,0 +1,11 @@ +package com.codigoparallevar.minicards.types.connectors.output; + +import com.codigoparallevar.minicards.types.connectors.Wiring.ImageWire; +import com.codigoparallevar.minicards.types.connectors.input.ImageInputConnector; +import com.codigoparallevar.minicards.types.wireData.ImageSignal; + +public interface ImageOutputConnector extends OutputConnector< + ImageSignal, + ImageInputConnector, + ImageWire> { +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/connectors/output/OutputConnector.java b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/output/OutputConnector.java index 4970793..344c357 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/types/connectors/output/OutputConnector.java +++ b/app/src/main/java/com/codigoparallevar/minicards/types/connectors/output/OutputConnector.java @@ -3,9 +3,9 @@ package com.codigoparallevar.minicards.types.connectors.output; import com.codigoparallevar.minicards.ScrolledCanvas; import com.codigoparallevar.minicards.types.Dropper; import com.codigoparallevar.minicards.types.Selectable; -import com.codigoparallevar.minicards.types.Tuple2; import com.codigoparallevar.minicards.types.connectors.Wiring.Wire; import com.codigoparallevar.minicards.types.connectors.input.InputConnector; +import com.codigoparallevar.minicards.types.functional.Tuple2; import com.codigoparallevar.minicards.types.wireData.WireDataType; import java.util.List; @@ -23,4 +23,10 @@ public interface OutputConnector getWires(); + + void wireUnlinked(T2 wire); } diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/functional/Action.java b/app/src/main/java/com/codigoparallevar/minicards/types/functional/Action.java new file mode 100644 index 0000000..2250774 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/types/functional/Action.java @@ -0,0 +1,5 @@ +package com.codigoparallevar.minicards.types.functional; + +public interface Action { + void run() throws InterruptedException; +} 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 new file mode 100644 index 0000000..5c57f5d --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/types/functional/Consumer.java @@ -0,0 +1,5 @@ +package com.codigoparallevar.minicards.types.functional; + +public interface Consumer { + void apply(T param) throws Exception; +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/functional/Function.java b/app/src/main/java/com/codigoparallevar/minicards/types/functional/Function.java new file mode 100644 index 0000000..bebbead --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/types/functional/Function.java @@ -0,0 +1,5 @@ +package com.codigoparallevar.minicards.types.functional; + +public interface Function { + U apply(T param) throws Exception; +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/functional/Producer.java b/app/src/main/java/com/codigoparallevar/minicards/types/functional/Producer.java new file mode 100644 index 0000000..b82d3d7 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/types/functional/Producer.java @@ -0,0 +1,5 @@ +package com.codigoparallevar.minicards.types.functional; + +public interface Producer { + T get(); +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/functional/Tuple2.java b/app/src/main/java/com/codigoparallevar/minicards/types/functional/Tuple2.java new file mode 100644 index 0000000..6832492 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/types/functional/Tuple2.java @@ -0,0 +1,50 @@ +package com.codigoparallevar.minicards.types.functional; + +public class Tuple2 { + public final T1 item1; + public final T2 item2; + + public Tuple2(T1 item1, T2 item2) { + this.item1 = item1; + this.item2 = item2; + } + + @Override + public int hashCode() { + int hash1 = this.item1 == null ? 1 : this.item1.hashCode(); + int hash2 = this.item2 == null ? 2 : this.item2.hashCode(); + + return hash1 ^ hash2; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Tuple2)) { + return false; + } + + Tuple2 other = (Tuple2) obj; + if (other.item1 == null) { + if (this.item1 != null) { + return false; + } + } + else { + if (!other.item1.equals(this.item1)) { + return false; + } + } + + if (other.item2 == null) { + if (this.item2 != null) { + return false; + } + } + else { + if (!other.item2.equals(this.item2)) { + return false; + } + } + return true; + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/functional/Tuple3.java b/app/src/main/java/com/codigoparallevar/minicards/types/functional/Tuple3.java new file mode 100644 index 0000000..6a5c416 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/types/functional/Tuple3.java @@ -0,0 +1,19 @@ +package com.codigoparallevar.minicards.types.functional; + +public class Tuple3 { + + public final T _x; + public final T1 _y; + public final T2 _z; + + public Tuple3(T x, T1 y, T2 z) { + _x = x; + _y = y; + _z = z; + } + + @Override + public String toString(){ + return "(" + _x + " - " + _y + ", " + _z + ")"; + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/Tuple4.java b/app/src/main/java/com/codigoparallevar/minicards/types/functional/Tuple4.java similarity index 87% rename from app/src/main/java/com/codigoparallevar/minicards/types/Tuple4.java rename to app/src/main/java/com/codigoparallevar/minicards/types/functional/Tuple4.java index 3f10b97..43eb6d1 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/types/Tuple4.java +++ b/app/src/main/java/com/codigoparallevar/minicards/types/functional/Tuple4.java @@ -1,4 +1,4 @@ -package com.codigoparallevar.minicards.types; +package com.codigoparallevar.minicards.types.functional; public class Tuple4 { diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/wireData/AnySignal.java b/app/src/main/java/com/codigoparallevar/minicards/types/wireData/AnySignal.java new file mode 100644 index 0000000..67de038 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/types/wireData/AnySignal.java @@ -0,0 +1,14 @@ +package com.codigoparallevar.minicards.types.wireData; + +public class AnySignal implements WireDataType { + public final Object value; + + public AnySignal(Object value) { + this.value = value; + } + + @Override + public Object get() { + return value; + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/types/wireData/ImageSignal.java b/app/src/main/java/com/codigoparallevar/minicards/types/wireData/ImageSignal.java new file mode 100644 index 0000000..6890511 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/types/wireData/ImageSignal.java @@ -0,0 +1,16 @@ +package com.codigoparallevar.minicards.types.wireData; + +import android.media.Image; + +public class ImageSignal implements WireDataType { + public final Image value; + + public ImageSignal(Image value) { + this.value = value; + } + + @Override + public Image get() { + return value; + } +} 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 new file mode 100644 index 0000000..44536b5 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/ui_helpers/DoAsync.java @@ -0,0 +1,27 @@ +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; +import com.codigoparallevar.minicards.types.functional.Tuple2; + +public class DoAsync extends AsyncTask>, + Void, Void> { + + @Override + protected Void doInBackground(Tuple2>... data) { + try { + data[0].item1.run(); + } + catch (Throwable 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 new file mode 100644 index 0000000..02394b7 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/ui_helpers/GetAsync.java @@ -0,0 +1,46 @@ +package com.codigoparallevar.minicards.ui_helpers; + +import android.os.AsyncTask; +import android.util.Log; + +import com.codigoparallevar.minicards.types.functional.Consumer; +import com.codigoparallevar.minicards.types.functional.Producer; +import com.codigoparallevar.minicards.types.functional.Tuple2; +import com.codigoparallevar.minicards.types.functional.Tuple3; + +public class GetAsync extends AsyncTask, Consumer, Consumer>, + Void, + Tuple2>> { + + @Override + protected Tuple2> doInBackground(Tuple3, Consumer, Consumer>... data) { + try { + T result = data[0]._x.get(); + return new Tuple2<>(result, data[0]._y); + } + catch (Throwable ex) { + try { + data[0]._z.apply(ex); + } + catch (Throwable subEx) { + Log.d("GetAsync", "Error handling exception", subEx); + } + return null; + } + } + + @Override + protected void onPostExecute(Tuple2> result) { + if (result == null) { + return; + } + else { + 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/codigoparallevar/minicards/utils/Box.java b/app/src/main/java/com/codigoparallevar/minicards/utils/Box.java new file mode 100644 index 0000000..e827369 --- /dev/null +++ b/app/src/main/java/com/codigoparallevar/minicards/utils/Box.java @@ -0,0 +1,20 @@ +package com.codigoparallevar.minicards.utils; + +public class Box { + private T val = null; + + public Box() { + } + + public Box(T val) { + this.val = val; + } + + public T get() { + return val; + } + + public void set(T val) { + this.val = val; + } +} diff --git a/app/src/main/java/com/codigoparallevar/minicards/utils/Serializations.java b/app/src/main/java/com/codigoparallevar/minicards/utils/Serializations.java index 1119a08..a348c23 100644 --- a/app/src/main/java/com/codigoparallevar/minicards/utils/Serializations.java +++ b/app/src/main/java/com/codigoparallevar/minicards/utils/Serializations.java @@ -1,6 +1,10 @@ package com.codigoparallevar.minicards.utils; +import android.util.Log; + +import org.jetbrains.annotations.NotNull; import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; import java.util.List; @@ -8,6 +12,8 @@ import java.util.Map; public class Serializations { + private static final String LogTag = "Serialization"; + public static JSONArray serialize(List> data) { JSONArray array = new JSONArray(); for (Map dict : data) { @@ -16,4 +22,68 @@ public class Serializations { return array; } + + @NotNull + public static Object blindSerialize(Object obj) { + if (obj == null) { + return JSONObject.NULL; + } + // No serialization needed + else if (obj.getClass().isPrimitive() || + obj instanceof String || + obj instanceof JSONObject || + obj instanceof JSONArray) { + + return obj; + } + + try { + return _blindSerialize(obj); + } + catch (Exception ex) { + Log.w(LogTag, "Error serialization: " + obj, ex); + return obj; + } + } + + private static Object _blindSerialize(Object obj) throws JSONException { + if (obj instanceof Map) { + Map objMap = (Map) obj; + + JSONObject map = new JSONObject(); + + for (Map.Entry entry : objMap.entrySet()) { + map.put(entry.getKey().toString(), blindSerialize(entry.getValue())); + } + + return map; + } + else if (obj instanceof Iterable) { + Iterable objIt = (Iterable) obj; + + JSONArray arr = new JSONArray(); + for (Object val : objIt) { + arr.put(blindSerialize(val)); + } + return arr; + } + else { + // Unknown case + 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; + } + } } diff --git a/app/src/main/java/com/programaker/api/ProgramakerApi.kt b/app/src/main/java/com/programaker/api/ProgramakerApi.kt new file mode 100644 index 0000000..b8703fa --- /dev/null +++ b/app/src/main/java/com/programaker/api/ProgramakerApi.kt @@ -0,0 +1,421 @@ +package com.programaker.api + +import android.os.Build +import android.util.Log +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.programaker.api.data.* +import com.programaker.api.data.api_results.* +import com.programaker.api.exceptions.ProgramakerLoginRequiredException +import com.programaker.api.exceptions.ProgramakerProtocolException +import com.programaker.api.exceptions.TokenNotFoundException +import org.json.JSONArray +import org.json.JSONObject +import java.io.DataOutputStream +import java.io.FileNotFoundException +import java.io.InputStream +import java.io.InputStreamReader +import java.lang.reflect.Type +import java.net.HttpURLConnection +import java.net.URL +import java.net.URLConnection + + +class ProgramakerApi(private val ApiRoot: String="https://programaker.com/api") { + private val enumCallbackCache: MutableMap> = HashMap() + private val LogTag: String = "ProgramakerApi" + + private var userId: String? = null + private var userName: String? = null + var token: String? = null + + // API + fun check(): Boolean { + val conn = URL(getCheckUrl()).openConnection() as HttpURLConnection + if (conn == null){ + Log.e(LogTag, "URL Connection not established, set to NULL") + return false + } + try { + addAuthHeader(conn) + } catch(ex: TokenNotFoundException) { + return false + } + + val result: ProgramakerCheckResult + try { + result = parseJson(conn.inputStream, ProgramakerCheckResult::class.java) + } catch(ex: JsonParseException) { + ex.logError(LogTag) + return false + } catch (ex: FileNotFoundException) { + ex.logError(LogTag) + return false + } + + if (result.success) { + this.userId = result.user_id; + this.userName = result.username; + } + + return result.success + } + + + fun login(user: String, password: String): String { + val conn = URL(getLoginUrl()).openConnection() as HttpURLConnection + conn.setRequestProperty("Content-Type", "application/json") + + conn.requestMethod = "POST"; + conn.doOutput = true; + + val postData = JSONObject() + postData.put("username", user) + postData.put("password", password) + + val wr = DataOutputStream(conn.outputStream) + wr.writeBytes(postData.toString()); + wr.flush(); + wr.close(); + + val result: ProgramakerLoginResult + result = parseJson(conn.inputStream, ProgramakerLoginResult::class.java) + return result.token + } + + fun fetchCustomBlocks(): List { + val conn = URL(getCustomBlocksUrl()).openConnection() as HttpURLConnection + + addAuthHeader(conn) + + val result: ProgramakerGetCustomBlocksResult + + try { + val builder = GsonBuilder() + builder.registerTypeAdapter(ProgramakerGetCustomBlocksResult::class.java, ProgramakerGetCustomBlocksResultTypeAdapter()) + + val gson = builder.create() + val reader = InputStreamReader(conn.inputStream) + + result = gson.fromJson(reader, ProgramakerGetCustomBlocksResult::class.java) + } catch(ex: JsonParseException) { + ex.logError(LogTag) + throw ProgramakerProtocolException() + } catch (ex: FileNotFoundException) { + ex.logError(LogTag) + throw ProgramakerProtocolException() + } + catch (ex: Exception) { + Log.e(LogTag, "Unexpected exception: " + ex, ex) + throw ProgramakerProtocolException() + } + return result.result + } + + fun fetchConnectedBridges(): List { + val conn = URL(getConnectedBridgesUrl()).openConnection() as HttpURLConnection + + addAuthHeader(conn) + + val result: ProgramakerGetBridgeInfoResult + + try { + val builder = GsonBuilder() + builder.registerTypeAdapter(ProgramakerGetBridgeInfoResult::class.java, ProgramakerGetBridgeInfoTypeAdapter()) + + val gson = builder.create() + val reader = InputStreamReader(conn.inputStream) + + result = gson.fromJson(reader, ProgramakerGetBridgeInfoResult::class.java) + } catch(ex: JsonParseException) { + ex.logError(LogTag) + throw ProgramakerProtocolException() + } catch (ex: FileNotFoundException) { + ex.logError(LogTag) + throw ProgramakerProtocolException() + } + return result.result + } + + fun fetchAllowedValues(blockArg: ProgramakerCustomBlockArgument): List? { + val url = getArgumentValuesUrl(blockArg) + val cachedValues = cachedAllowedValues(url) + if (cachedValues != null) { + return cachedValues + } + + val conn = URL(url).openConnection() as HttpURLConnection + + addAuthHeader(conn) + + var results: List? = null + try { + val serialized: HashMap = 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 + } + cacheValues(url, results) + return results + } + + fun callBlock(block: ProgramakerCustomBlock, arguments: List): ProgramakerFunctionCallResult { + val conn = URL(getBlockUrl(block)).openConnection() as HttpURLConnection + conn.setRequestProperty("Content-Type", "application/json") + addAuthHeader(conn) + + conn.requestMethod = "POST"; + conn.doOutput = true; + + val postData = JSONObject(hashMapOf( + "arguments" to JSONArray(arguments) + ) as Map<*, *>) + + val wr = DataOutputStream(conn.outputStream) + wr.writeBytes(postData.toString()); + wr.flush(); + wr.close(); + + val result: ProgramakerFunctionCallResult + result = parseJson(conn.inputStream, ProgramakerFunctionCallResult::class.java) + return result + } + + fun createBridge(name: String): String { + val conn = URL(getCreateBridgeUrl()).openConnection() as HttpURLConnection + conn.setRequestProperty("Content-Type", "application/json") + addAuthHeader(conn) + + conn.requestMethod = "POST"; + conn.doOutput = true; + + val postData = JSONObject(hashMapOf( + "name" to name + ) as Map<*, *>) + + val wr = DataOutputStream(conn.outputStream) + wr.writeBytes(postData.toString()); + wr.flush(); + wr.close(); + + val result: ProgramakerCreateBridgeResult + result = parseJson(conn.inputStream, ProgramakerCreateBridgeResult::class.java) + return result.getBridgeId() + } + + + fun createBridgeAuthenticationToken(bridgeId: String, tokenName: String): String { + val conn = URL(getCreateBridgeAuthenticationTokenUrl(bridgeId)).openConnection() as HttpURLConnection + conn.setRequestProperty("Content-Type", "application/json") + addAuthHeader(conn) + + conn.requestMethod = "POST"; + conn.doOutput = true; + + val postData = JSONObject(hashMapOf( + "name" to tokenName + ) as Map<*, *>) + + val wr = DataOutputStream(conn.outputStream) + wr.writeBytes(postData.toString()); + wr.flush(); + wr.close(); + + val result: ProgramakerFullBridgeInfo + result = parseJson(conn.inputStream, ProgramakerFullBridgeInfo::class.java) + return result.key + } + + fun establishConnection(bridgeId: String): Boolean { + // NOTE: This establishes a connection to a bridge as long as it can be done without interaction + + // Prepare connection + val prepareConn = URL(getPrepareConnectionUrl(bridgeId)).openConnection() as HttpURLConnection + addAuthHeader(prepareConn) + + val prepareResult: ProgramakerPrepareConnectionResult + prepareResult = parseJson(prepareConn.inputStream, ProgramakerPrepareConnectionResult::class.java) + if (prepareResult.type != "direct") { + throw Exception("Expected 'direct' connection type, found '${prepareResult.type}'") + } + + // Establish connection + val establishConn = URL(getEstablishConnectionUrl(bridgeId)).openConnection() as HttpURLConnection + establishConn.setRequestProperty("Content-Type", "application/json") + addAuthHeader(establishConn) + + establishConn.requestMethod = "POST"; + establishConn.doOutput = true; + + val establishWrite = DataOutputStream(establishConn.outputStream) + establishWrite.writeBytes(JSONObject().toString()); + establishWrite.flush(); + establishWrite.close(); + + val establishResult: ProgramakerEstablishDirectConnectionResult + establishResult = parseJson(establishConn.inputStream, ProgramakerEstablishDirectConnectionResult::class.java) + return establishResult.success + } + + // Initialization + init { + // Disable connection reuse if necessary + // HTTP connection reuse which was buggy pre-froyo + if (Build.VERSION.SDK.toInt() < Build.VERSION_CODES.FROYO) { + System.setProperty("http.keepAlive", "false") + } + } + + fun openChannelTo(bridgeId: String, key: String, + listener: ProgramakerSignalListener, + onDisconnectCallback: Runnable): ProgramakerListeningChannel { + val channel = ProgramakerListeningChannel( + bridgeId, key, + this.token!!, + getListenSignalUrl(bridgeId, key), + listener, onDisconnectCallback) + channel.start() + return channel + } + + // Private functions + private fun getCheckUrl(): String { + return "$ApiRoot/v0/sessions/check" + } + + private fun getLoginUrl(): String { + return "$ApiRoot/v0/sessions/login" + } + + private fun getCustomBlocksUrl(): String { + this.withUserName() + return "$ApiRoot/v0/users/$userName/custom-blocks/" + } + + private fun getConnectedBridgesUrl(): String { + this.withUserName() + return "$ApiRoot/v0/users/$userName/bridges/" + } + + private fun getBlockUrl(block: ProgramakerCustomBlock): String { + this.withUserId() + return "$ApiRoot/v0/users/id/$userId/bridges/id/${block.bridge_id}/functions/${block.function_name}" + } + + private fun getCreateBridgeUrl(): String { + this.withUserName() + return "$ApiRoot/v0/users/$userName/bridges" + } + + private fun getCreateBridgeAuthenticationTokenUrl(bridgeId: String): String { + this.withUserId() + return "$ApiRoot/v0/bridges/by-id/$bridgeId/tokens" + } + + private fun getPrepareConnectionUrl(bridgeId: String): String { + this.withUserId() + return "$ApiRoot/v0/services/by-id/$bridgeId/how-to-enable" + } + + private fun getEstablishConnectionUrl(bridgeId: String): String { + this.withUserName() + return "$ApiRoot/v0/users/$userName/services/id/$bridgeId/register" + } + + private fun getListenSignalUrl(bridgeId: String, key: String): String { + this.withUserId() + 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; + } + + private fun withUserName() { + if (userName == null) { + if (token == null) { + throw ProgramakerLoginRequiredException() + } + + this.check(); + if (userName == null) { + throw ProgramakerProtocolException() + } + } + } + + private fun withUserId() { + if (userId == null) { + if (token == null) { + throw ProgramakerLoginRequiredException() + } + + this.check(); + if (userId == null) { + throw ProgramakerProtocolException() + } + } + } + + private fun addAuthHeader(conn: URLConnection) { + if (token != null) { + conn.setRequestProperty("Authorization", token) + } + else { + throw TokenNotFoundException() + } + } + + // Simple result caching + private fun cacheValues(url: String, results: List) { + enumCallbackCache[url] = results + } + + private fun cachedAllowedValues(url: String): List? { + if (enumCallbackCache.containsKey(url)) { + return enumCallbackCache[url] + } + return null + } + + fun hasCachedAllowedValues(blockArg: ProgramakerCustomBlockArgument): Boolean { + if (userId == null) { + // This will be required to fully form the URL. + // Also, without it, it's not possible to have cached values. We can safely discard it + return false + } + val url = getArgumentValuesUrl(blockArg) + return enumCallbackCache.containsKey(url) + } + + // Utils + private fun parseJson(content: InputStream, resultClass: Type): T { + val reader = InputStreamReader(content) + val gson = Gson() + return gson.fromJson(reader, resultClass) + } +} + +private fun FileNotFoundException.logError(tag: String) { + Log.e(tag, "Cannot open: ${this.message}") +} + +class JsonParseException(private val content: String, private val cls: Type) : Exception() { + fun logError(tag: String) { + Log.e(tag, "Cannot JSON parse: ${this.content} as ${this.cls}", this) + } +} diff --git a/app/src/main/java/com/programaker/api/ProgramakerListeningChannel.kt b/app/src/main/java/com/programaker/api/ProgramakerListeningChannel.kt new file mode 100644 index 0000000..90890be --- /dev/null +++ b/app/src/main/java/com/programaker/api/ProgramakerListeningChannel.kt @@ -0,0 +1,78 @@ +package com.programaker.api + +import android.util.Log +import com.google.gson.Gson +import okhttp3.* +import okio.ByteString +import org.json.JSONException +import java.nio.charset.Charset +import java.util.concurrent.TimeUnit + +class ProgramakerListeningChannel( + private val bridgeId: String, + private val key: String, + private val token: String, + private val url: String, + private val listener: ProgramakerSignalListener, + private var onDisconnect: Runnable +): WebSocketListener() { + private var webSocket: WebSocket? = null + private val utf8: Charset = Charset.forName("UTF-8") + private val gson = Gson() + + private val LogTag: String = "PM-ListeningChannel" + private val PING_PERIOD_MILLIS: Long = 15000 // 15seconds + + fun start() { + val client: OkHttpClient = OkHttpClient.Builder() + .pingInterval(PING_PERIOD_MILLIS, TimeUnit.MILLISECONDS) + .readTimeout(0, TimeUnit.MILLISECONDS) + .build() + + val request: Request = Request.Builder() + .url(url) + .addHeader("Authorization", token) + .build() + client.newWebSocket(request, this) + + // Trigger shutdown of the dispatcher's executor so this process can exit cleanly. + client.dispatcher.executorService.shutdown() + } + + // Websocket management + override fun onOpen(webSocket: WebSocket, response: Response) { + this.webSocket = webSocket + } + + override fun onMessage(webSocket: WebSocket, bytes: ByteString) { + onMessage(webSocket, bytes.string(utf8)) + } + + override fun onMessage(webSocket: WebSocket, text: String) { + Log.d(LogTag, "Message: $text") + val json = gson.fromJson(text, HashMap::class.java) + if (json == null) { + this.onFailure(webSocket, JSONException("Error decoding: $text"), null) + } + else { + this.listener.onNewSignal(bridgeId, key, json) + } + } + + override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { + webSocket.close(1000, null) + Log.i(LogTag, "Closing bridge socket {code=$code, reason=$reason}") + onDisconnect.run() + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + Log.e(LogTag, "Error: $t", t) + onDisconnect.run() + } + + fun stop() { + onDisconnect = Runnable { } // Skip disconnection procedure + webSocket?.close(1000, null) + webSocket = null + } +} diff --git a/app/src/main/java/com/programaker/api/ProgramakerSignalListener.kt b/app/src/main/java/com/programaker/api/ProgramakerSignalListener.kt new file mode 100644 index 0000000..a5c8577 --- /dev/null +++ b/app/src/main/java/com/programaker/api/ProgramakerSignalListener.kt @@ -0,0 +1,7 @@ +package com.programaker.api + +import java.util.HashMap + +interface ProgramakerSignalListener { + fun onNewSignal(bridgeId: String, key: String, signal: HashMap<*, *>) +} diff --git a/app/src/main/java/com/programaker/api/data/ProgramakerBridgeInfo.kt b/app/src/main/java/com/programaker/api/data/ProgramakerBridgeInfo.kt new file mode 100644 index 0000000..422fde0 --- /dev/null +++ b/app/src/main/java/com/programaker/api/data/ProgramakerBridgeInfo.kt @@ -0,0 +1,9 @@ +package com.programaker.api.data + +class ProgramakerBridgeInfo ( + val id: String, + val name: String + // val is_connected: boolean + // val icon: Map +) { +} diff --git a/app/src/main/java/com/programaker/api/data/ProgramakerCustomBlock.kt b/app/src/main/java/com/programaker/api/data/ProgramakerCustomBlock.kt new file mode 100644 index 0000000..0510af5 --- /dev/null +++ b/app/src/main/java/com/programaker/api/data/ProgramakerCustomBlock.kt @@ -0,0 +1,73 @@ +package com.programaker.api.data + +import org.json.JSONArray +import org.json.JSONObject + +class ProgramakerCustomBlock( + val block_id: String, + val function_name: String, + val message: String, + val arguments: List?, + val block_type: String?, + val block_result_type: String?, + var bridge_id: String?, + val save_to: ProgramakerCustomBlockSaveTo?, + var key: String?, + val subkey: ProgramakerCustomBlockSubkeyDefinition? +) { + fun serialize(): JSONObject { + + val serializedArguments = JSONArray() + if (arguments != null) { + for (value in arguments) { + serializedArguments.put(value.serialize()) + } + } + + var saveToSerialized: JSONObject? = null + if (save_to != null) { + saveToSerialized = save_to.serialize() + } + + val serialized = hashMapOf( + "block_id" to block_id, + "function_name" to function_name, + "message" to message, + "arguments" to serializedArguments, + "block_type" to block_type, + "bridge_id" to bridge_id, + "save_to" to saveToSerialized + ) + + if (key != null) { + serialized.put("key", key) + serialized.put("subkey", subkey?.serialize()) + } + if (block_result_type != null || key == null) { + serialized.put("block_result_type", block_result_type) + } + + return JSONObject(serialized as Map<*, *>) + } + companion object { + @JvmStatic + fun deserialize(obj: JSONObject): ProgramakerCustomBlock { + var block = ProgramakerCustomBlock( + obj.getString("block_id"), + obj.getString("function_name"), + obj.getString("message"), + ProgramakerCustomBlockArgument.deserialize(obj.optJSONArray("arguments")), + obj.getString("block_type"), + obj.optString("block_result_type"), + obj.optString("bridge_id"), + ProgramakerCustomBlockSaveTo.deserialize(obj.optJSONObject("save_to")), + obj.optString("key"), + ProgramakerCustomBlockSubkeyDefinition.deserialize(obj.optJSONObject("subkey")) + ) + + return block + } + } + +} + diff --git a/app/src/main/java/com/programaker/api/data/ProgramakerCustomBlockArgument.kt b/app/src/main/java/com/programaker/api/data/ProgramakerCustomBlockArgument.kt new file mode 100644 index 0000000..0767e02 --- /dev/null +++ b/app/src/main/java/com/programaker/api/data/ProgramakerCustomBlockArgument.kt @@ -0,0 +1,73 @@ +package com.programaker.api.data + +import android.util.Log +import org.json.JSONArray +import org.json.JSONObject +import java.util.* + +class ProgramakerCustomBlockArgument( + val type: String?, + val default_value: String?, + // val class: String, + val callback: String? +) { + var bridge_id: String? = null + + fun serialize(): JSONObject { + val serialized = hashMapOf( + "type" to type, + "default_value" to default_value, + "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 { + if (arguments == null) { + return listOf(); + } + val results: MutableList = LinkedList() + + var i = 0; + while (i < arguments.length()) { + val obj = arguments.getJSONObject(i) + if (obj == null) { + Log.e("PMCustomBlockArgument", "Looped into a null value!?") + } + else { + results.add(deserialize(obj)) + } + + i += 1; + } + + return results + } + + @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") + + val arg = ProgramakerCustomBlockArgument(type, default_value, callback) + arg.bridge_id = bridge_id + return arg + } + } + +} diff --git a/app/src/main/java/com/programaker/api/data/ProgramakerCustomBlockArgumentValue.kt b/app/src/main/java/com/programaker/api/data/ProgramakerCustomBlockArgumentValue.kt new file mode 100644 index 0000000..902243c --- /dev/null +++ b/app/src/main/java/com/programaker/api/data/ProgramakerCustomBlockArgumentValue.kt @@ -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 { + 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 { + val results = LinkedList() + for (entry in value.asIterable()) { + var value = entry.value.toString() + if (entry.value is Map<*, *>) { + if ((entry.value as Map).containsKey("name")) { + value = (entry.value as Map<*, *>)["name"].toString() + } + } + + results.add(ProgramakerCustomBlockArgumentValue(entry.key.toString(), value)) + } + + return results + } + + @JvmStatic + fun deserialize(value: List<*>): List { + val results = LinkedList() + for (entry in value) { + if (entry is Map<*,*>) { + var name = (entry as Map).optGet("name") + var id = (entry as Map).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 + } + } +} diff --git a/app/src/main/java/com/programaker/api/data/ProgramakerCustomBlockArgumentValuesReturnInfo.kt b/app/src/main/java/com/programaker/api/data/ProgramakerCustomBlockArgumentValuesReturnInfo.kt new file mode 100644 index 0000000..f2334ad --- /dev/null +++ b/app/src/main/java/com/programaker/api/data/ProgramakerCustomBlockArgumentValuesReturnInfo.kt @@ -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 +) { + companion object { + @JvmStatic + fun deserialize(value: HashMap): 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 = Collections.emptyList() + if (_result != null) { + result = ProgramakerCustomBlockArgumentValue.deserializeList(_result) + } + + return ProgramakerCustomBlockArgumentValuesReturnInfo(success, result) + } + } +} diff --git a/app/src/main/java/com/programaker/api/data/ProgramakerCustomBlockSaveTo.kt b/app/src/main/java/com/programaker/api/data/ProgramakerCustomBlockSaveTo.kt new file mode 100644 index 0000000..cee0a04 --- /dev/null +++ b/app/src/main/java/com/programaker/api/data/ProgramakerCustomBlockSaveTo.kt @@ -0,0 +1,27 @@ +package com.programaker.api.data + +import org.json.JSONObject + +class ProgramakerCustomBlockSaveTo ( + val type: String, + val index: Int +) { + fun serialize(): JSONObject { + return JSONObject(hashMapOf( + "type" to type, + "index" to index + ) as Map<*, *>) + } + + companion object { + @JvmStatic + fun deserialize(save_to: JSONObject?): ProgramakerCustomBlockSaveTo? { + if (save_to == null) { + return null; + } + + return ProgramakerCustomBlockSaveTo(save_to.getString("type"), + save_to.getInt("index")) + } + } +} diff --git a/app/src/main/java/com/programaker/api/data/ProgramakerCustomBlockSubkeyDefinition.kt b/app/src/main/java/com/programaker/api/data/ProgramakerCustomBlockSubkeyDefinition.kt new file mode 100644 index 0000000..db2cabe --- /dev/null +++ b/app/src/main/java/com/programaker/api/data/ProgramakerCustomBlockSubkeyDefinition.kt @@ -0,0 +1,29 @@ +package com.programaker.api.data + +import org.json.JSONObject + +class ProgramakerCustomBlockSubkeyDefinition( + val type: String, + val value: String +) { + fun serialize(): JSONObject { + return JSONObject(hashMapOf( + "type" to type, + "value" to value + ) as Map<*, *>) + } + + companion object { + @JvmStatic + fun deserialize(subkey: JSONObject?): ProgramakerCustomBlockSubkeyDefinition? { + if (subkey == null) { + return null; + } + + return ProgramakerCustomBlockSubkeyDefinition( + subkey.getString("type"), + subkey.getString("value")) + } + } + +} diff --git a/app/src/main/java/com/programaker/api/data/ProgramakerFunctionCallResult.kt b/app/src/main/java/com/programaker/api/data/ProgramakerFunctionCallResult.kt new file mode 100644 index 0000000..8a6b870 --- /dev/null +++ b/app/src/main/java/com/programaker/api/data/ProgramakerFunctionCallResult.kt @@ -0,0 +1,10 @@ +package com.programaker.api.data + +import java.util.* + +class ProgramakerFunctionCallResult ( + var success: Boolean, + var result: Object +) { + +} diff --git a/app/src/main/java/com/programaker/api/data/api_results/ProgramakerBridgeCustomBlockResult.kt b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerBridgeCustomBlockResult.kt new file mode 100644 index 0000000..75ba9fb --- /dev/null +++ b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerBridgeCustomBlockResult.kt @@ -0,0 +1,6 @@ +package com.programaker.api.data.api_results + +import com.programaker.api.data.ProgramakerCustomBlock + +class ProgramakerBridgeCustomBlockResult(val bridge_id: String, val blocks: List) { +} diff --git a/app/src/main/java/com/programaker/api/data/api_results/ProgramakerCheckResult.kt b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerCheckResult.kt new file mode 100644 index 0000000..039c983 --- /dev/null +++ b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerCheckResult.kt @@ -0,0 +1,4 @@ +package com.programaker.api.data.api_results + +class ProgramakerCheckResult(val success: Boolean, val user_id: String, val username: String) { +} diff --git a/app/src/main/java/com/programaker/api/data/api_results/ProgramakerCreateBridgeResult.kt b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerCreateBridgeResult.kt new file mode 100644 index 0000000..4d96e44 --- /dev/null +++ b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerCreateBridgeResult.kt @@ -0,0 +1,17 @@ +package com.programaker.api.data.api_results + +class ProgramakerCreateBridgeResult ( + val control_url: String +) { + fun getBridgeId(): String { + val url = control_url.trim('/'); + if (url.endsWith("communication")) { + val no_communication = url.substring(0, url.length - "communication".length); + val chunks = no_communication.trim('/').split("/"); + return chunks[chunks.size - 1] + } + else { + throw IllegalArgumentException("Unknown control_url format") + } + } +} diff --git a/app/src/main/java/com/programaker/api/data/api_results/ProgramakerEstablishDirectConnectionResult.kt b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerEstablishDirectConnectionResult.kt new file mode 100644 index 0000000..4d0f729 --- /dev/null +++ b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerEstablishDirectConnectionResult.kt @@ -0,0 +1,5 @@ +package com.programaker.api.data.api_results + +class ProgramakerEstablishDirectConnectionResult(val success: Boolean) { + +} diff --git a/app/src/main/java/com/programaker/api/data/api_results/ProgramakerFullBridgeInfo.kt b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerFullBridgeInfo.kt new file mode 100644 index 0000000..5478344 --- /dev/null +++ b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerFullBridgeInfo.kt @@ -0,0 +1,6 @@ +package com.programaker.api.data.api_results + +class ProgramakerFullBridgeInfo ( + val key: String +) { +} diff --git a/app/src/main/java/com/programaker/api/data/api_results/ProgramakerGetBridgeInfoResult.kt b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerGetBridgeInfoResult.kt new file mode 100644 index 0000000..6cb00bb --- /dev/null +++ b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerGetBridgeInfoResult.kt @@ -0,0 +1,6 @@ +package com.programaker.api.data.api_results + +import com.programaker.api.data.ProgramakerBridgeInfo + +internal class ProgramakerGetBridgeInfoResult(val result: List) { +} diff --git a/app/src/main/java/com/programaker/api/data/api_results/ProgramakerGetBridgeInfoTypeAdapter.kt b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerGetBridgeInfoTypeAdapter.kt new file mode 100644 index 0000000..fc77ec8 --- /dev/null +++ b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerGetBridgeInfoTypeAdapter.kt @@ -0,0 +1,28 @@ +package com.programaker.api.data.api_results + +import com.google.gson.* +import com.programaker.api.data.ProgramakerBridgeInfo +import com.programaker.api.data.ProgramakerCustomBlock +import java.lang.reflect.Type +import java.util.* + +internal class ProgramakerGetBridgeInfoTypeAdapter : JsonSerializer, JsonDeserializer { + var gson = Gson() + override fun serialize(customBlock: ProgramakerGetBridgeInfoResult?, typeOfT: Type, context: JsonSerializationContext): JsonElement { + throw NotImplementedError() + } + + @Throws(JsonParseException::class) + override fun deserialize(element: JsonElement, typeOfT: Type, context: JsonDeserializationContext): ProgramakerGetBridgeInfoResult { + val array = element.asJsonArray + val bridges: MutableList = LinkedList() + + for (value in array) { + + val bridge = gson.fromJson(value, ProgramakerBridgeInfo::class.java) + bridges.add(bridge) + } + + return ProgramakerGetBridgeInfoResult(bridges) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/programaker/api/data/api_results/ProgramakerGetCustomBlocksResult.kt b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerGetCustomBlocksResult.kt new file mode 100644 index 0000000..07801a9 --- /dev/null +++ b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerGetCustomBlocksResult.kt @@ -0,0 +1,4 @@ +package com.programaker.api.data.api_results + +internal class ProgramakerGetCustomBlocksResult(val result: List) { +} diff --git a/app/src/main/java/com/programaker/api/data/api_results/ProgramakerGetCustomBlocksResultTypeAdapter.kt b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerGetCustomBlocksResultTypeAdapter.kt new file mode 100644 index 0000000..1c73fcc --- /dev/null +++ b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerGetCustomBlocksResultTypeAdapter.kt @@ -0,0 +1,41 @@ +package com.programaker.api.data.api_results + +import com.google.gson.* +import com.programaker.api.data.ProgramakerCustomBlock +import java.lang.reflect.Type +import java.util.* + +internal class ProgramakerGetCustomBlocksResultTypeAdapter : JsonSerializer, JsonDeserializer { + var gson = Gson() + override fun serialize(customBlock: ProgramakerGetCustomBlocksResult?, typeOfT: Type, context: JsonSerializationContext): JsonElement { + throw NotImplementedError() + } + + @Throws(JsonParseException::class) + override fun deserialize(element: JsonElement, typeOfT: Type, context: JsonDeserializationContext): ProgramakerGetCustomBlocksResult { + val json = element.asJsonObject + val bridges: MutableList = LinkedList() + + for ((bridgeId, value1) in json.entrySet()) { + + val blocks: MutableList = LinkedList() + + 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) + } + + val bridge = ProgramakerBridgeCustomBlockResult(bridgeId, blocks) + + bridges.add(bridge) + } + + return ProgramakerGetCustomBlocksResult(bridges) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/programaker/api/data/api_results/ProgramakerLoginResult.kt b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerLoginResult.kt new file mode 100644 index 0000000..69c1eb6 --- /dev/null +++ b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerLoginResult.kt @@ -0,0 +1,5 @@ +package com.programaker.api.data.api_results + +class ProgramakerLoginResult(val user_id: String, val token: String, val success: Boolean) { + +} diff --git a/app/src/main/java/com/programaker/api/data/api_results/ProgramakerPrepareConnectionResult.kt b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerPrepareConnectionResult.kt new file mode 100644 index 0000000..7dd7be9 --- /dev/null +++ b/app/src/main/java/com/programaker/api/data/api_results/ProgramakerPrepareConnectionResult.kt @@ -0,0 +1,5 @@ +package com.programaker.api.data.api_results + +class ProgramakerPrepareConnectionResult(val type: String) { + +} diff --git a/app/src/main/java/com/programaker/api/exceptions/ProgramakerConfigurationException.kt b/app/src/main/java/com/programaker/api/exceptions/ProgramakerConfigurationException.kt new file mode 100644 index 0000000..de7dd73 --- /dev/null +++ b/app/src/main/java/com/programaker/api/exceptions/ProgramakerConfigurationException.kt @@ -0,0 +1,5 @@ +package com.programaker.api.exceptions + +open class ProgramakerConfigurationException : Exception() { + +} diff --git a/app/src/main/java/com/programaker/api/exceptions/ProgramakerLoginRequiredException.kt b/app/src/main/java/com/programaker/api/exceptions/ProgramakerLoginRequiredException.kt new file mode 100644 index 0000000..d7005db --- /dev/null +++ b/app/src/main/java/com/programaker/api/exceptions/ProgramakerLoginRequiredException.kt @@ -0,0 +1,5 @@ +package com.programaker.api.exceptions + +class ProgramakerLoginRequiredException : Throwable() { + +} diff --git a/app/src/main/java/com/programaker/api/exceptions/ProgramakerNetworkException.kt b/app/src/main/java/com/programaker/api/exceptions/ProgramakerNetworkException.kt new file mode 100644 index 0000000..119fc04 --- /dev/null +++ b/app/src/main/java/com/programaker/api/exceptions/ProgramakerNetworkException.kt @@ -0,0 +1,5 @@ +package com.programaker.api.exceptions + +class ProgramakerNetworkException : Throwable() { + +} diff --git a/app/src/main/java/com/programaker/api/exceptions/ProgramakerProtocolException.kt b/app/src/main/java/com/programaker/api/exceptions/ProgramakerProtocolException.kt new file mode 100644 index 0000000..6e4f78b --- /dev/null +++ b/app/src/main/java/com/programaker/api/exceptions/ProgramakerProtocolException.kt @@ -0,0 +1,6 @@ +package com.programaker.api.exceptions + +import java.lang.Exception + +class ProgramakerProtocolException() : Exception() { +} diff --git a/app/src/main/java/com/programaker/api/exceptions/TokenNotFoundException.kt b/app/src/main/java/com/programaker/api/exceptions/TokenNotFoundException.kt new file mode 100644 index 0000000..9d85a30 --- /dev/null +++ b/app/src/main/java/com/programaker/api/exceptions/TokenNotFoundException.kt @@ -0,0 +1,6 @@ +package com.programaker.api.exceptions + +import com.programaker.api.exceptions.ProgramakerConfigurationException + +class TokenNotFoundException : ProgramakerConfigurationException() { +} diff --git a/app/src/main/java/com/programaker/api/utils.kt b/app/src/main/java/com/programaker/api/utils.kt new file mode 100644 index 0000000..1151ce8 --- /dev/null +++ b/app/src/main/java/com/programaker/api/utils.kt @@ -0,0 +1,19 @@ +package com.programaker.api + +internal fun java.util.HashMap.optGet(key: K): V? { + if (this.containsKey(key)) { + return this.get(key) + } + else { + return null + } +} + +internal fun Map.optGet(key: K): V? { + if (this.containsKey(key)) { + return this.get(key) + } + else { + return null + } +} diff --git a/app/src/main/java/com/programaker/bridge/ProgramakerBridge.kt b/app/src/main/java/com/programaker/bridge/ProgramakerBridge.kt new file mode 100644 index 0000000..e4e6f0f --- /dev/null +++ b/app/src/main/java/com/programaker/bridge/ProgramakerBridge.kt @@ -0,0 +1,205 @@ +package com.programaker.bridge + +import android.util.Log +import com.codigoparallevar.minicards.utils.Serializations +import com.google.gson.Gson +import okhttp3.* +import okio.ByteString +import org.json.JSONException +import org.json.JSONObject +import java.nio.charset.Charset +import java.util.concurrent.TimeUnit + + +class ProgramakerBridge( + private val bridge_id: String, + private val user_id: String, + private val bridge_token: String, + private val config: ProgramakerBridgeConfiguration, + private val onReady: Runnable, + private val onComplete: Runnable +) : WebSocketListener() { + private var webSocket: WebSocket? = null + private val utf8: Charset = Charset.forName("UTF-8") + private val gson = Gson() + + private val PING_PERIOD_MILLIS: Long = 15000 + private val apiRoot: String = "wss://programaker.com/api" + private val LogTag = "ProgramakerBridge" + + // Connection establishment + fun run() { + val client: OkHttpClient = OkHttpClient.Builder() + .pingInterval(PING_PERIOD_MILLIS, TimeUnit.MILLISECONDS) + .readTimeout(0, TimeUnit.MILLISECONDS) + .build() + + val request: Request = Request.Builder() + .url(getBridgeControlUrl()) + .build() + client.newWebSocket(request, this) + + // Trigger shutdown of the dispatcher's executor so this process can exit cleanly. + client.dispatcher.executorService.shutdown() + } + + private fun getBridgeControlUrl(): String { + return "$apiRoot/v0/users/id/$user_id/bridges/id/$bridge_id/communication" + } + + // Websocket management + override fun onOpen(webSocket: WebSocket, response: Response) { + this.webSocket = webSocket + + val auth = JSONObject(hashMapOf( + "type" to "AUTHENTICATION", + "value" to hashMapOf ( + "token" to this.bridge_token + ) + ) as Map<*, *>).toString() + + webSocket.send(auth) + + webSocket.send(config.serialize()) + onReady.run() + } + + override fun onMessage(webSocket: WebSocket, bytes: ByteString) { + onMessage(webSocket, bytes.string(utf8)) + } + + override fun onMessage(webSocket: WebSocket, text: String) { + Log.d(LogTag, "Message: $text") + val json = gson.fromJson(text, HashMap::class.java) + if (json == null) { + this.onFailure(webSocket, JSONException("Error decoding: $text"), null) + } + else { + try { + this.onCommand(webSocket, json) + } + catch (ex: Throwable ) { + Log.e(LogTag, "Error on command handling: $ex", ex) + } + } + } + + override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { + webSocket.close(1000, null) + Log.i(LogTag, "Closing bridge socket {code=$code, reason=$reason}") + onComplete.run() + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + Log.e(LogTag, "Error: $t", t) + onComplete.run() + } + + // Protocol handling + private fun onCommand(webSocket: WebSocket, json: Map<*, *>) { + val type = json.get("type") as String + val messageId = json.get("message_id") as String + val value = json.get("value") as Map<*, *> + var userId: String? = null + var extraData: Map<*, *>? = null + if (json.containsKey("user_id")) { + userId = json.get("user_id") as String + } + if (json.containsKey("extra_data")) { + extraData = json.get("extra_data") as Map<*, *> + } + + when (type) { + "GET_HOW_TO_SERVICE_REGISTRATION" -> handleGetServiceRegistrationInfo(webSocket, value, messageId, userId, extraData) + "REGISTRATION" -> handlePerformRegistration(webSocket, value, messageId, userId, extraData) + "FUNCTION_CALL" -> handleFunctionCall(webSocket, value, messageId, userId, extraData) + else -> + { + Log.w(LogTag, "Unknown command type: $type") + } + } + } + + 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 { + var result = config.callFunction(functionName, arguments) + result = Serializations.blindSerialize(result) + + 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( + JSONObject( + hashMapOf( + "message_id" to messageId, + "success" to true, + "result" to null + ) as Map<*, *> + ).toString() + ) + } + + private fun handlePerformRegistration(webSocket: WebSocket, value: Map<*, *>, messageId: String, userId: String?, extraData: Map<*, *>?) { + // Registration is automatic + if (userId == null) { + throw Exception("No connection ID received") + } + + webSocket.send( + JSONObject( + hashMapOf( + "message_id" to messageId, + "success" to true + ) as Map<*, *> + ).toString() + ) + this.config.configManager.bridgeConnectionId = userId + } + + fun stop() { + webSocket?.close(1000, null) + } + + fun sendSignal(key: String, content: JSONObject) { + if (webSocket == null) { + Log.w(LogTag, "Cannot send signal (key=$key) on closed channel") + return + } + + webSocket!!.send(JSONObject(hashMapOf( + "type" to "NOTIFICATION", + "key" to key, + "subkey" to JSONObject.NULL, + "to_user" to JSONObject.NULL, + "value" to content, + "content" to content + ) + ).toString()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/programaker/bridge/ProgramakerBridgeConfiguration.kt b/app/src/main/java/com/programaker/bridge/ProgramakerBridgeConfiguration.kt new file mode 100644 index 0000000..164518c --- /dev/null +++ b/app/src/main/java/com/programaker/bridge/ProgramakerBridgeConfiguration.kt @@ -0,0 +1,49 @@ +package com.programaker.bridge + +import com.codigoparallevar.minicards.ConfigManager +import org.json.JSONObject + +class ProgramakerBridgeConfiguration( + val serviceName: String, + val configManager: ConfigManager, + val blocks: List + // val is_public: boolean, // No reason for this use case + // val registration: ???, // No reason for this use case + // val allow_multiple_connections: boolean // No reason for this use case + // val icon: ???, // Not supported yet // TODO: Add support +) { + + fun serialize(): String { + var serializedBlocks = listOf() + if (blocks != null) { + serializedBlocks = blocks.map { it.serialize() } + } + + val config = JSONObject(hashMapOf( + "service_name" to serviceName, + "is_public" to false, + "registration" to null, + "allow_multiple_connections" to null, + "icon" to null, + "blocks" to serializedBlocks + ) as Map<*, *>) + + val wrapper = JSONObject(hashMapOf( + "type" to "CONFIGURATION", + "value" to config + ) as Map<*, *>) + + return wrapper.toString() + } + + fun callFunction(functionName: String, arguments: List<*>): Any { + for (block in blocks) { + val blockFunction = block.getFunctionName() + if (blockFunction != null && blockFunction == 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 new file mode 100644 index 0000000..294c270 --- /dev/null +++ b/app/src/main/java/com/programaker/bridge/ProgramakerBridgeConfigurationBlock.kt @@ -0,0 +1,11 @@ +package com.programaker.bridge + +import org.json.JSONObject + +interface ProgramakerBridgeConfigurationBlock { + fun serialize() : JSONObject + fun getFunctionName(): String + + @Throws(Exception::class) + fun call(arguments: List<*>): Any +} diff --git a/app/src/main/res/drawable/ic_cancel.xml b/app/src/main/res/drawable/ic_cancel.xml new file mode 100644 index 0000000..39b6244 --- /dev/null +++ b/app/src/main/res/drawable/ic_cancel.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_exit_to_app.xml b/app/src/main/res/drawable/ic_exit_to_app.xml new file mode 100644 index 0000000..30c2bce --- /dev/null +++ b/app/src/main/res/drawable/ic_exit_to_app.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_start.xml b/app/src/main/res/drawable/ic_start.xml new file mode 100644 index 0000000..dfae628 --- /dev/null +++ b/app/src/main/res/drawable/ic_start.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_vector_icon.xml b/app/src/main/res/drawable/ic_vector_icon.xml new file mode 100644 index 0000000..2b0607a --- /dev/null +++ b/app/src/main/res/drawable/ic_vector_icon.xml @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_vector_icon_loading.xml b/app/src/main/res/drawable/ic_vector_icon_loading.xml new file mode 100644 index 0000000..b1665fb --- /dev/null +++ b/app/src/main/res/drawable/ic_vector_icon_loading.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_vector_icon_user_triggered.xml b/app/src/main/res/drawable/ic_vector_icon_user_triggered.xml new file mode 100644 index 0000000..2ab495a --- /dev/null +++ b/app/src/main/res/drawable/ic_vector_icon_user_triggered.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/drawable/ic_vector_stopped_icon.xml b/app/src/main/res/drawable/ic_vector_stopped_icon.xml new file mode 100644 index 0000000..64131b0 --- /dev/null +++ b/app/src/main/res/drawable/ic_vector_stopped_icon.xml @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_deck_preview.xml b/app/src/main/res/layout/activity_deck_preview.xml index 454b5e7..c5c966d 100644 --- a/app/src/main/res/layout/activity_deck_preview.xml +++ b/app/src/main/res/layout/activity_deck_preview.xml @@ -1,28 +1,28 @@ - - - - + - - + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index f718591..bdbfa99 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,5 +1,5 @@ - - + fab:fab_colorPressed="@color/white_pressed" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/card_preview.xml b/app/src/main/res/layout/card_preview.xml index fe9c125..5cca6e4 100644 --- a/app/src/main/res/layout/card_preview.xml +++ b/app/src/main/res/layout/card_preview.xml @@ -1,58 +1,76 @@ - - + card_view:layout_constraintBottom_toBottomOf="parent" + card_view:layout_constraintEnd_toEndOf="parent" + card_view:layout_constraintStart_toStartOf="parent" + card_view:layout_constraintTop_toTopOf="parent"> - + android:layout_height="100dp" + android:height="100dp"> + android:layout_marginEnd="20dp" + android:layout_marginRight="20dp" + android:layout_marginBottom="30dp" + android:text="@string/placeholder_text" + android:textColor="?android:attr/colorForeground" + android:textSize="25sp" + card_view:layout_constraintBottom_toBottomOf="parent" + card_view:layout_constraintEnd_toEndOf="parent" + card_view:layout_constraintStart_toStartOf="parent" + card_view:layout_constraintTop_toTopOf="parent" /> - + - + - + \ No newline at end of file + android:layout_marginBottom="4dp" + android:clickable="true" + android:focusable="true" + card_view:elevation="4dp" + card_view:hoveredFocusedTranslationZ="4dp" + card_view:layout_constraintBottom_toBottomOf="parent" + card_view:layout_constraintEnd_toEndOf="parent" + card_view:srcCompat="@drawable/ic_settings_black" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/content_deck_preview.xml b/app/src/main/res/layout/content_deck_preview.xml index 9efa657..b87a878 100644 --- a/app/src/main/res/layout/content_deck_preview.xml +++ b/app/src/main/res/layout/content_deck_preview.xml @@ -1,5 +1,5 @@ - + + + app:layout_constraintTop_toBottomOf="@id/login_in_programaker_button" /> - + diff --git a/app/src/main/res/layout/login_dialog_view.xml b/app/src/main/res/layout/login_dialog_view.xml new file mode 100644 index 0000000..07cf6c7 --- /dev/null +++ b/app/src/main/res/layout/login_dialog_view.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + +