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