diff --git a/.gitignore b/.gitignore
index b724f29..d7d42e5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,71 +2,16 @@
# Android
################################################################################
-# Built application files
-*.apk
-*.ap_
-
-# Files for the ART/Dalvik VM
-*.dex
-
-# Java class files
-*.class
-
-# Generated files
bin/
-gen/
-out/
-
-# Gradle files
-.gradle/
build/
-# Local configuration file (sdk path, etc)
-local.properties
-
-# Proguard folder generated by Eclipse
-proguard/
-
-# Log Files
-*.log
-
-# Android Studio Navigation editor temp files
-.navigation/
-
-# Android Studio captures folder
-captures/
-
-# IntelliJ
+# gradle & android specific files
+.gradle/
*.iml
-.idea/workspace.xml
-.idea/tasks.xml
-.idea/gradle.xml
-.idea/assetWizardSettings.xml
-.idea/dictionaries
-.idea/libraries
-.idea/caches
-
-# Keystore files
-# Uncomment the following line if you do not want to check your keystore files in.
-#*.jks
-
-# External native build folder generated in Android Studio 2.2 and later
-.externalNativeBuild
-
-# Google Services (e.g. APIs or Firebase)
-google-services.json
-
-# Freeline
-freeline.py
-freeline/
-freeline_project_description.json
+.idea
+local.properties
-# fastlane
-fastlane/report.xml
-fastlane/Preview.html
-fastlane/screenshots
-fastlane/test_output
-fastlane/readme.md
+.DS_Store
################################################################################
# MyScript
diff --git a/LICENSES/androidSupportLib.txt b/LICENSES/androidSupportLib.txt
new file mode 100644
index 0000000..d955a86
--- /dev/null
+++ b/LICENSES/androidSupportLib.txt
@@ -0,0 +1,11 @@
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
\ No newline at end of file
diff --git a/LICENSES/inter.txt b/LICENSES/inter.txt
new file mode 100644
index 0000000..65ec0f9
--- /dev/null
+++ b/LICENSES/inter.txt
@@ -0,0 +1,94 @@
+Copyright (c) 2016-2020 The Inter Project Authors.
+"Inter" is trademark of Rasmus Andersson.
+https://github.com/rsms/inter
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION AND CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
\ No newline at end of file
diff --git a/LICENSES/kotlin.txt b/LICENSES/kotlin.txt
new file mode 100644
index 0000000..51fca54
--- /dev/null
+++ b/LICENSES/kotlin.txt
@@ -0,0 +1,11 @@
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/LICENSES/materialDesign.txt b/LICENSES/materialDesign.txt
new file mode 100644
index 0000000..d955a86
--- /dev/null
+++ b/LICENSES/materialDesign.txt
@@ -0,0 +1,11 @@
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
\ No newline at end of file
diff --git a/LICENSES/stix.pdf b/LICENSES/stix.pdf
new file mode 100644
index 0000000..45f6d31
Binary files /dev/null and b/LICENSES/stix.pdf differ
diff --git a/LICENSES/surface-duo-sdk.txt b/LICENSES/surface-duo-sdk.txt
new file mode 100644
index 0000000..7965606
--- /dev/null
+++ b/LICENSES/surface-duo-sdk.txt
@@ -0,0 +1,21 @@
+ MIT License
+
+ Copyright (c) Microsoft Corporation.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE
\ No newline at end of file
diff --git a/README.md b/README.md
index 5648ba6..95140cb 100644
--- a/README.md
+++ b/README.md
@@ -8,37 +8,26 @@ This repository comes in addition with further advanced Android examples that de
## Installation
-1. Clone the examples repository `git clone https://github.com/MyScript/iink_sdk-additional-examples-android.git`.
+1. Clone the examples repository `git clone https://github.com/MyScript/iink_sdk-additional-examples-android.git`.
2. If you already have a certificate go to next step, else claim to receive the free license to start develop your application by following the first steps of [Getting Started](https://developer.myscript.com/getting-started).
-3. Copy this certificate to `certificate/src/main/java/com/myscript/certificate/MyCertificate.java`
+3. Copy this certificate to `samples/certificate/src/main/java/com/myscript/certificate/MyCertificate.java`
-4. Open `java` folder in Android Studio.
+4. Open `samples` folder in Android Studio.
## Various examples
This repository provides you with an additional set of ready-to-use examples based on Android:
-1. The batch mode sample is an example of how to integrate iink SDK off-screen, without any user interface. It consists in batch processing content, i.e. processing a series of pointer events corresponding to already collected ink strokes and exporting the recognition result. It comes with four pointer events samples that correspond to four different content types "Text", "Math", "Diagram", "Raw Content". When starting the app a dialog will be displayed to choose which type of part you want to proceed. By default those content types are exported in respectively .txt, LaTeX, svg and JIIX formats, but you can choose to export in png by modifying the following line in the MainActivity class:
-
-~~~#!java
- // this is the function where we process exteranl output and export it
- // add true if you want to export in png
- offScreenProcess(typeOfPart[it])
-~~~
-
-
-
-
-NB: you will retrieve data converted in your device internal storage : Android\data\com.myscript.iink.samples.batchmode\files
+1. The batch mode sample is an example of how to integrate iink SDK without any user interface. It consists in batch processing content, i.e. processing a series of pointer events corresponding to already collected ink strokes and exporting the recognition result. It comes with four pointer events samples that correspond to four different content types "Text", "Math", "Diagram", "Raw Content". When starting the app a dialog will be displayed to choose which type of part you want to proceed. By default those content types are exported in respectively .txt, LaTeX, svg and JIIX formats, but you can choose to export in png depending on your choice in the configuration window.
2. The exercise assessment illustrates the case when you want to use several writing areas each one for a specific purpose in your application. It is thus using multiple editors, one per writing area, as each one has a different purpose:
- First one is dedicated to "Math" content types
-- Second one is dedicated to "Math" content types but with user defined gramar which is dynamically loaded at start.
+- Second one is dedicated to "Math" content types but with a user defined grammar which is dynamically loaded at start.
- Third one is dedicated to "Text" content types
-- Fourth one is dedicated to "Diagram" content types
-- Fifth one is dedicated to "Draw" content types
+- Fourth one is dedicated to "Diagram" content types
+- Fifth one is dedicated to "Draw" content types
@@ -52,9 +41,43 @@ NB: you will retrieve data converted in your device internal storage : Android\d
val partType = "Raw Content" // change to "Text Document" if you want to test
~~~
-
+
+
+
+4. The write to type example gives you a hint how to implement Scribble like feature relying on the Recognizer API of iink SDK. It is based on a contextless gesture recognition combined with a text recognition.
+To run both recognitions simultaneously, two instances of Recognizer are created - one for Gesture recognition and the other one for Text recognition:
+~~~#!java
+ static final float INCH_IN_MILLIMETER = 25.4f;
+
+ float scaleX = INCH_IN_MILLIMETER / displayMetrics.xdpi;
+ float scaleY = INCH_IN_MILLIMETER / displayMetrics.ydpi;
+
+ Recognizer textRecognizer = engine.createRecognizer(scaleX, scaleY, "Text");
+ Recognizer gestureRecognizer = engine.createRecognizer(scaleX, scaleY, "Gesture");
+~~~
+
+
+
+NB: the Recognizer API is available from iink SDK 2.1.
+
+For more details on the sample, read more [here](samples/write-to-type/ReadMe.pdf).
+
+5. The offscreen-interactivity samples shows how to integrate MyScript iink SDK interactivity with your own rendering.
+It drives the content model by sending the captured strokes to iink SDK and keeps the incremental recognition principle and gesture notifications.
+This sample uses a third-party rendering library to manage the captured strokes and display its model, and get real-time recognition results and gesture notifications.
+
+
+
+
+
+6. The keyboard input sample shows a specific usage for the `Placeholder` feature. In this sample, you'll be able to input text by keyboard within an iink document, with minumum code.
+It shows how to manage switching from an Android view to an image managed by iink.
+
+7. The handwriting generation sample demonstrates the use of MyScript's handwriting generation APIs. It shows how to generate ink from text input, in different styles, and how to create a style from your own handwriting.
+To use the handwriting generation, you must [contact our sales team](https://developer.myscript.com/contact-us/on-device-recognition). You will then have access to the "myscript-iink-handwriting-generation" resource package, from which `handwriting_generation` folder must extracted and copied to `samples/hwgeneration/src/main/assets/resources/`
+
## Documentation
A complete guide is available on [MyScript Developer Portal](https://developer.myscript.com/docs/interactive-ink/latest/android/).
diff --git a/UIReferenceImplementation/src/main/AndroidManifest.xml b/UIReferenceImplementation/src/main/AndroidManifest.xml
deleted file mode 100644
index 40dfebb..0000000
--- a/UIReferenceImplementation/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/IRenderView.java b/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/IRenderView.java
deleted file mode 100644
index cc1b182..0000000
--- a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/IRenderView.java
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright @ MyScript. All rights reserved.
-
-package com.myscript.iink.uireferenceimplementation;
-
-import android.graphics.Typeface;
-
-import com.myscript.iink.Editor;
-import com.myscript.iink.IRenderTarget;
-import com.myscript.iink.Renderer;
-
-import java.util.EnumSet;
-import java.util.Map;
-
-/**
- * Implemented by views that render Interactive Ink content.
- */
-public interface IRenderView
-{
- /**
- * Tells whether this view renders a single layer or all the layers
- * @return
true
if this view renders a single layer.
- */
- boolean isSingleLayerView();
-
- /**
- * If the view is a single layer view return the type of the layer it renders.
- * @return the type of the rendered layer.
- */
- IRenderTarget.LayerType getType();
-
- /**
- * Sets the render target that owns this render view.
- * @param renderTarget the render target.
- */
- void setRenderTarget(IRenderTarget renderTarget);
-
- /**
- * Sets the editor that holds the content to render.
- * @param editor the editor.
- */
- void setEditor(Editor editor);
-
- /**
- * Sets the image loader used to render images.
- * @param imageLoader the image loader.
- */
- void setImageLoader(ImageLoader imageLoader);
-
- /**
- * Sets the map of custom typefaces to use for text rendering.
- * @param typefaceMap the map of custom typefaces.
- */
- void setCustomTypefaces(Map
typefaceMap);
-
- /**
- * Requests an update of the specified area of the view.
- * @param renderer the renderer to be used to render the area.
- * @param x the area top x position.
- * @param y the area top y position.
- * @param width the area width.
- * @param height the area height.
- * @param layers the layers to update. To be ignored if this is a single layer view.
- */
- void update(Renderer renderer, int x, int y, int width, int height, EnumSet layers);
-}
diff --git a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/InputController.java b/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/InputController.java
deleted file mode 100644
index 00406bc..0000000
--- a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/InputController.java
+++ /dev/null
@@ -1,273 +0,0 @@
-// Copyright @ MyScript. All rights reserved.
-
-package com.myscript.iink.uireferenceimplementation;
-
-import android.content.Context;
-import android.os.SystemClock;
-import android.view.GestureDetector;
-import android.view.MotionEvent;
-import android.view.View;
-
-import com.myscript.iink.ContentBlock;
-import com.myscript.iink.Editor;
-import com.myscript.iink.IRenderTarget;
-import com.myscript.iink.PointerEvent;
-import com.myscript.iink.PointerEventType;
-import com.myscript.iink.PointerType;
-import com.myscript.iink.graphics.Point;
-
-import java.util.EnumSet;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import androidx.core.view.GestureDetectorCompat;
-
-public class InputController implements View.OnTouchListener, GestureDetector.OnGestureListener
-{
-
- public interface ViewListener
- {
- void showScrollbars();
- }
-
- public static final int INPUT_MODE_NONE = -1;
- public static final int INPUT_MODE_FORCE_PEN = 0;
- public static final int INPUT_MODE_FORCE_TOUCH = 1;
- public static final int INPUT_MODE_AUTO = 2;
- public static final int INPUT_MODE_ERASER = 3;
-
- private final IRenderTarget renderTarget;
- private final Editor editor;
- private int _inputMode;
- private final GestureDetectorCompat gestureDetector;
- private IInputControllerListener _listener;
- private final long eventTimeOffset;
- @VisibleForTesting
- public PointerType iinkPointerType;
- private ViewListener _viewListener;
-
- public InputController(Context context, IRenderTarget renderTarget, Editor editor)
- {
- this.renderTarget = renderTarget;
- this.editor = editor;
- _listener = null;
- _inputMode = INPUT_MODE_AUTO;
- gestureDetector = new GestureDetectorCompat(context, this);
-
- long rel_t = SystemClock.uptimeMillis();
- long abs_t = System.currentTimeMillis();
- eventTimeOffset = abs_t - rel_t;
- }
-
- public final synchronized void setInputMode(int inputMode)
- {
- this._inputMode = inputMode;
- }
-
- public final synchronized int getInputMode()
- {
- return _inputMode;
- }
-
- public final synchronized void setViewListener(ViewListener listener)
- {
- this._viewListener = listener;
- }
-
- public final synchronized void setListener(IInputControllerListener listener)
- {
- this._listener = listener;
- }
-
- public final synchronized IInputControllerListener getListener()
- {
- return _listener;
- }
-
- private boolean handleOnTouchForPointer(MotionEvent event, int actionMask, int pointerIndex)
- {
- final int pointerId = event.getPointerId(pointerIndex);
- final int pointerType = event.getToolType(pointerIndex);
-
- int inputMode = getInputMode();
-
- if (inputMode == INPUT_MODE_FORCE_PEN)
- {
- iinkPointerType = PointerType.PEN;
- }
- else if (inputMode == INPUT_MODE_FORCE_TOUCH)
- {
- iinkPointerType = PointerType.TOUCH;
- }
- else
- {
- switch (pointerType)
- {
- case MotionEvent.TOOL_TYPE_STYLUS:
- if (inputMode == INPUT_MODE_ERASER)
- {
- iinkPointerType = PointerType.ERASER;
- }
- else
- {
- iinkPointerType = PointerType.PEN;
- }
- break;
- case MotionEvent.TOOL_TYPE_FINGER:
- case MotionEvent.TOOL_TYPE_MOUSE:
- iinkPointerType = PointerType.TOUCH;
- break;
- default:
- // unsupported event type
- return false;
- }
- }
-
- if (iinkPointerType == PointerType.TOUCH)
- {
- gestureDetector.onTouchEvent(event);
- }
-
- int historySize = event.getHistorySize();
-
- switch (actionMask)
- {
- case MotionEvent.ACTION_POINTER_DOWN:
- case MotionEvent.ACTION_DOWN:
- editor.pointerDown(event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), event.getPressure(), iinkPointerType, pointerId);
- return true;
-
- case MotionEvent.ACTION_MOVE:
- if (historySize > 0)
- {
- PointerEvent[] pointerEvents = new PointerEvent[historySize + 1];
- for (int i = 0; i < historySize; ++i)
- pointerEvents[i] = new PointerEvent(PointerEventType.MOVE, event.getHistoricalX(pointerIndex, i), event.getHistoricalY(pointerIndex, i), eventTimeOffset + event.getHistoricalEventTime(i), event.getHistoricalPressure(pointerIndex, i), iinkPointerType, pointerId);
- pointerEvents[historySize] = new PointerEvent(PointerEventType.MOVE, event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), event.getPressure(), iinkPointerType, pointerId);
- editor.pointerEvents(pointerEvents, true);
- }
- else
- {
- editor.pointerMove(event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), event.getPressure(), iinkPointerType, pointerId);
- }
- return true;
-
- case MotionEvent.ACTION_POINTER_UP:
- case MotionEvent.ACTION_UP:
- if (historySize > 0)
- {
- PointerEvent[] pointerEvents = new PointerEvent[historySize];
- for (int i = 0; i < historySize; ++i)
- pointerEvents[i] = new PointerEvent(PointerEventType.MOVE, event.getHistoricalX(pointerIndex, i), event.getHistoricalY(pointerIndex, i), eventTimeOffset + event.getHistoricalEventTime(i), event.getHistoricalPressure(pointerIndex, i), iinkPointerType, pointerId);
- editor.pointerEvents(pointerEvents, true);
- }
- editor.pointerUp(event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), event.getPressure(), iinkPointerType, pointerId);
- return true;
-
- case MotionEvent.ACTION_CANCEL:
- editor.pointerCancel(pointerId);
- return true;
-
- default:
- return false;
- }
- }
-
- @Override
- public boolean onTouch(View v, MotionEvent event)
- {
- if (editor == null)
- {
- return false;
- }
-
- final int action = event.getAction();
- final int actionMask = action & MotionEvent.ACTION_MASK;
-
- if (actionMask == MotionEvent.ACTION_POINTER_DOWN || actionMask == MotionEvent.ACTION_POINTER_UP)
- {
- final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
- return handleOnTouchForPointer(event, actionMask, pointerIndex);
- }
- else
- {
- boolean consumed = false;
- final int pointerCount = event.getPointerCount();
- for (int pointerIndex = 0; pointerIndex < pointerCount; pointerIndex++)
- {
- try
- {
- consumed = consumed || handleOnTouchForPointer(event, actionMask, pointerIndex);
- }
- catch(Exception e) {
- // ignore spurious invalid touch events that may occurs when spamming undo/redo button
- }
- }
- return consumed;
- }
- }
-
- @Override
- public boolean onDown(MotionEvent event)
- {
- return false;
- }
-
- @Override
- public void onShowPress(MotionEvent event)
- {
- // no-op
- }
-
- @Override
- public boolean onSingleTapUp(MotionEvent event)
- {
- return false;
- }
-
- @Override
- public void onLongPress(MotionEvent event)
- {
- IInputControllerListener listener = getListener();
- if (listener != null)
- {
- final float x = event.getX();
- final float y = event.getY();
- // Only handle block ID and not `ContentBlock` to simplify native `AutoCloseable` object lifecycle.
- // Otherwise, depending on which object owns such `ContentBlock`, the reasoning about closing it
- // would be more complicated.
- // Providing block ID delegates to listeners the ownership of the retrieved block (if any),
- // typically calling `Editor.getBlockById()`.
- try (@Nullable ContentBlock block = editor.hitBlock(x, y))
- {
- String blockId = block != null ? block.getId() : null;
- listener.onLongPress(x, y, blockId);
- }
- }
- }
-
- @Override
- public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)
- {
- if (editor.isScrollAllowed())
- {
- Point oldOffset = editor.getRenderer().getViewOffset();
- Point newOffset = new Point(oldOffset.x + distanceX, oldOffset.y + distanceY);
- editor.clampViewOffset(newOffset);
- editor.getRenderer().setViewOffset(Math.round(newOffset.x), Math.round(newOffset.y));
- renderTarget.invalidate(editor.getRenderer(), EnumSet.allOf(IRenderTarget.LayerType.class));
- if(_viewListener != null)
- {
- _viewListener.showScrollbars();
- }
- return true;
- }
- return false;
- }
-
- @Override
- public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
- {
- return false;
- }
-}
diff --git a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/LayerView.java b/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/LayerView.java
deleted file mode 100644
index 81063cc..0000000
--- a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/LayerView.java
+++ /dev/null
@@ -1,262 +0,0 @@
-// Copyright @ MyScript. All rights reserved.
-
-package com.myscript.iink.uireferenceimplementation;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.PorterDuff;
-import android.graphics.Rect;
-import android.graphics.Typeface;
-import androidx.annotation.Nullable;
-import android.util.AttributeSet;
-import android.util.DisplayMetrics;
-import android.view.View;
-
-import com.myscript.iink.Editor;
-import com.myscript.iink.IRenderTarget;
-import com.myscript.iink.IRenderTarget.LayerType;
-import com.myscript.iink.Renderer;
-
-import java.util.EnumSet;
-import java.util.Map;
-
-public class LayerView extends View implements IRenderView
-{
- private final LayerType type;
- private IRenderTarget renderTarget;
-
- private ImageLoader imageLoader;
-
- @Nullable
- private Map typefaceMap;
-
- @Nullable
- private Renderer lastRenderer = null;
-
- @Nullable
- private Rect updateArea;
- @Nullable
- private Bitmap bitmap;
- @Nullable
- private android.graphics.Canvas sysCanvas;
- @Nullable
- private Canvas iinkCanvas;
- @Nullable
- private OfflineSurfaceManager offlineSurfaceManager = null;
- @Nullable
- private Renderer renderer = null;
- private int pageHeight = 0;
- private int viewHeight = 0;
- private int viewWidth = 0;
- private int pageWidth = 0;
- private int yMin = 0;
- private int xMin = 0;
-
- public LayerView(Context context)
- {
- this(context, null, 0);
- }
-
- public LayerView(Context context, @Nullable AttributeSet attrs)
- {
- this(context, attrs, 0);
- }
-
- public LayerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)
- {
- super(context, attrs, defStyleAttr);
-
- updateArea = null;
-
- TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LayerView, defStyleAttr, 0);
- try
- {
- int typeOrdinal = typedArray.getInteger(R.styleable.LayerView_layerType, 0);
- type = LayerType.values()[typeOrdinal];
- }
- finally
- {
- typedArray.recycle();
- }
- }
-
- @Override
- public boolean isSingleLayerView()
- {
- return true;
- }
-
- @Override
- public LayerType getType()
- {
- return type;
- }
-
- @Override
- public void setRenderTarget(IRenderTarget renderTarget)
- {
- this.renderTarget = renderTarget;
- }
-
- public void setOfflineSurfaceManager(@Nullable OfflineSurfaceManager offlineSurfaceManager)
- {
- this.offlineSurfaceManager = offlineSurfaceManager;
- }
-
- @Override
- public void setEditor(Editor editor)
- {
- // do not need the editor
- }
-
- @Override
- public void setImageLoader(ImageLoader imageLoader)
- {
- this.imageLoader = imageLoader;
- }
-
- public void setCustomTypefaces(Map typefaceMap)
- {
- this.typefaceMap = typefaceMap;
- }
-
- @Override
- protected final void onDraw(android.graphics.Canvas canvas)
- {
- Rect localUpdateArea;
- Renderer renderer;
-
- synchronized (this)
- {
- localUpdateArea = this.updateArea;
-
- this.updateArea = null;
- renderer = lastRenderer;
- lastRenderer = null;
- }
-
- if (localUpdateArea != null)
- {
-
- prepare(sysCanvas, localUpdateArea);
- try
- {
- switch (type)
- {
- case MODEL:
- renderer.drawModel(localUpdateArea.left, localUpdateArea.top, localUpdateArea.width(), localUpdateArea.height(), iinkCanvas);
- break;
- case CAPTURE:
- renderer.drawCaptureStrokes(localUpdateArea.left, localUpdateArea.top, localUpdateArea.width(), localUpdateArea.height(), iinkCanvas);
- break;
- default:
- break;
- }
- }
- finally
- {
- restore(sysCanvas);
- }
- }
-
- canvas.drawBitmap(bitmap, 0, 0, null);
- }
-
- @Override
- protected void onSizeChanged(int newWidth, int newHeight, int oldWidth, int oldHeight)
- {
- if (bitmap != null)
- {
- bitmap.recycle();
- }
- bitmap = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);
- sysCanvas = new android.graphics.Canvas(bitmap);
- DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
- iinkCanvas = new Canvas(sysCanvas, typefaceMap, imageLoader, offlineSurfaceManager, metrics.xdpi, metrics.ydpi);
-
- super.onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
- }
-
- private void prepare(android.graphics.Canvas canvas, Rect clipRect)
- {
- canvas.save();
- canvas.clipRect(clipRect);
- canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
- }
-
- private void restore(android.graphics.Canvas canvas)
- {
- canvas.restore();
- }
-
- @Override
- public final void update(Renderer renderer, int x, int y, int width, int height, EnumSet layers)
- {
- boolean emptyArea;
- synchronized (this)
- {
- if (updateArea != null)
- updateArea.union(x, y, x + width, y + height);
- else
- updateArea = new Rect(x, y, x + width, y + height);
-
- if (bitmap != null)
- updateArea.intersect(new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()));
- emptyArea = updateArea.isEmpty();
- lastRenderer = renderer;
- }
- if (!emptyArea)
- postInvalidate(x, y, x + width, y + height);
- }
-
- public void setScrollbar(Renderer renderer, int viewWidthPx, int pageWidthtPx, int xMin, int viewHeightPx, int pageHeightPx, int yMin)
- {
- this.viewWidth = viewWidthPx;
- this.pageWidth = pageWidthtPx;
- this.renderer = renderer;
- this.pageHeight = pageHeightPx;
- this.viewHeight = viewHeightPx;
- this.xMin = xMin;
- this.yMin = yMin;
- setVerticalScrollBarEnabled(true);
- awakenScrollBars();
- }
-
- @Override
- protected int computeVerticalScrollRange()
- {
- return pageHeight;
- }
-
- @Override
- protected int computeVerticalScrollExtent()
- {
- return viewHeight;
- }
-
- @Override
- protected int computeVerticalScrollOffset()
- {
- return renderer != null ? (int) renderer.getViewOffset().y - yMin : 0;
- }
-
- @Override
- protected int computeHorizontalScrollRange()
- {
- return pageWidth;
- }
-
- @Override
- protected int computeHorizontalScrollExtent()
- {
- return viewWidth;
- }
-
- @Override
- protected int computeHorizontalScrollOffset()
- {
- return renderer != null ? (int) renderer.getViewOffset().x - xMin : 0;
- }
-}
diff --git a/UIReferenceImplementation/src/main/res/values/attrs.xml b/UIReferenceImplementation/src/main/res/values/attrs.xml
deleted file mode 100644
index 9fecfd5..0000000
--- a/UIReferenceImplementation/src/main/res/values/attrs.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/batch.gif b/batch.gif
deleted file mode 100644
index 6db7339..0000000
Binary files a/batch.gif and /dev/null differ
diff --git a/certificate/.gitignore b/certificate/.gitignore
deleted file mode 100644
index 796b96d..0000000
--- a/certificate/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
diff --git a/certificate/build_android.gradle b/certificate/build_android.gradle
deleted file mode 100644
index a9b8c17..0000000
--- a/certificate/build_android.gradle
+++ /dev/null
@@ -1,13 +0,0 @@
-plugins {
- id 'com.android.library'
-}
-
-android {
- compileSdkVersion = project.ext.compileSdkVersion
- defaultConfig {
- minSdkVersion project.ext.minSdkVersion
- targetSdkVersion project.ext.targetSdkVersion
- versionCode 1
- versionName '1.0'
- }
-}
diff --git a/certificate/src/main/AndroidManifest.xml b/certificate/src/main/AndroidManifest.xml
deleted file mode 100644
index d8a8531..0000000
--- a/certificate/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/java/.gitignore b/java/.gitignore
deleted file mode 100644
index 1add293..0000000
--- a/java/.gitignore
+++ /dev/null
@@ -1,78 +0,0 @@
-################################################################################
-# Android
-################################################################################
-
-# Built application files
-*.apk
-*.ap_
-
-# Files for the ART/Dalvik VM
-*.dex
-
-# Java class files
-*.class
-
-# Generated files
-bin/
-gen/
-out/
-
-# Gradle files
-.gradle/
-build/
-
-# Local configuration file (sdk path, etc)
-local.properties
-
-# Proguard folder generated by Eclipse
-proguard/
-
-# Log Files
-*.log
-
-# Android Studio Navigation editor temp files
-.navigation/
-
-# Android Studio captures folder
-captures/
-
-# IntelliJ
-*.iml
-.idea/workspace.xml
-.idea/tasks.xml
-.idea/gradle.xml
-.idea/assetWizardSettings.xml
-.idea/dictionaries
-.idea/libraries
-.idea/caches
-
-# Keystore files
-# Uncomment the following line if you do not want to check your keystore files in.
-#*.jks
-
-# External native build folder generated in Android Studio 2.2 and later
-.externalNativeBuild
-
-# Google Services (e.g. APIs or Firebase)
-google-services.json
-
-# Freeline
-freeline.py
-freeline/
-freeline_project_description.json
-
-# fastlane
-fastlane/report.xml
-fastlane/Preview.html
-fastlane/screenshots
-fastlane/test_output
-fastlane/readme.md
-
-################################################################################
-# MyScript
-################################################################################
-
-# auto generated
-.idea/.name
-.idea/misc.xml
-.idea/modules.xml
diff --git a/java/.idea/codeStyles/Project.xml b/java/.idea/codeStyles/Project.xml
deleted file mode 100644
index 06f1f7c..0000000
--- a/java/.idea/codeStyles/Project.xml
+++ /dev/null
@@ -1,137 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- xmlns:android
- ^$
-
-
-
-
-
-
-
-
- xmlns:.*
- ^$
-
-
- BY_NAME
-
-
-
-
-
-
- .*:id
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- .*:name
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- name
- ^$
-
-
-
-
-
-
-
-
- style
- ^$
-
-
-
-
-
-
-
-
- .*
- ^$
-
-
- BY_NAME
-
-
-
-
-
-
- .*
- http://schemas.android.com/apk/res/android
-
-
- ANDROID_ATTRIBUTE_ORDER
-
-
-
-
-
-
- .*
- .*
-
-
- BY_NAME
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/java/.idea/codeStyles/codeStyleConfig.xml b/java/.idea/codeStyles/codeStyleConfig.xml
deleted file mode 100644
index 79ee123..0000000
--- a/java/.idea/codeStyles/codeStyleConfig.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/java/.idea/compiler.xml b/java/.idea/compiler.xml
deleted file mode 100644
index fb7f4a8..0000000
--- a/java/.idea/compiler.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/java/.idea/copyright/MyScript.xml b/java/.idea/copyright/MyScript.xml
deleted file mode 100644
index 8d99023..0000000
--- a/java/.idea/copyright/MyScript.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
diff --git a/java/.idea/copyright/profiles_settings.xml b/java/.idea/copyright/profiles_settings.xml
deleted file mode 100644
index 5a9c9a9..0000000
--- a/java/.idea/copyright/profiles_settings.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/java/.idea/jarRepositories.xml b/java/.idea/jarRepositories.xml
deleted file mode 100644
index d2ce72d..0000000
--- a/java/.idea/jarRepositories.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/java/.idea/vcs.xml b/java/.idea/vcs.xml
deleted file mode 100644
index 6c0b863..0000000
--- a/java/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/java/build.gradle b/java/build.gradle
deleted file mode 100644
index d9e4344..0000000
--- a/java/build.gradle
+++ /dev/null
@@ -1,62 +0,0 @@
-
-buildscript {
- repositories {
- google()
- mavenCentral()
- }
- dependencies {
- classpath 'com.android.tools.build:gradle:7.1.3'
- classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21'
- }
-}
-
-subprojects {
- afterEvaluate { proj ->
- if (proj.hasProperty('android')) {
- android {
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
-
- if (proj.hasProperty('kotlin')) {
- kotlinOptions {
- jvmTarget = JavaVersion.VERSION_1_8.toString()
- }
- }
-
- ndkVersion '21.4.7075529'
- }
- }
- }
-}
-allprojects {
- repositories {
- google()
- mavenCentral()
- }
-
- ext {
- // configure versions used by dependencies to harmonize and update easily across all components
-
- // Android SDK
- compileSdkVersion = 31
- minSdkVersion = 21
- targetSdkVersion = 31
-
- // Android libraries
- kotlinCoreVersion='1.7.0'
- appcompatVersion = '1.4.1'
- materialLibraryVersion = '1.6.0'
- gsonVersion = '2.8.9'
-
- // iink version
- iinkVersionCode = 2010
- iinkVersionName = '2.0.1'
- }
-
-}
-
-task clean(type: Delete) {
- delete rootProject.buildDir
-}
diff --git a/java/buildSrc/settings.gradle b/java/buildSrc/settings.gradle
deleted file mode 100644
index 5ddf9ad..0000000
--- a/java/buildSrc/settings.gradle
+++ /dev/null
@@ -1,3 +0,0 @@
-/*
- * Copyright (c) MyScript. All rights reserved.
- */
diff --git a/java/buildSrc/src/main/groovy/com/myscript/gradle/tasks/CopyResourceAssets.groovy b/java/buildSrc/src/main/groovy/com/myscript/gradle/tasks/CopyResourceAssets.groovy
deleted file mode 100644
index a6fbe1e..0000000
--- a/java/buildSrc/src/main/groovy/com/myscript/gradle/tasks/CopyResourceAssets.groovy
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (c) MyScript. All rights reserved.
- */
-
-package com.myscript.gradle.tasks
-
-import org.gradle.api.DefaultTask
-import org.gradle.api.tasks.TaskAction
-
-/**
- * Download resource assets from remote server.
- */
-@SuppressWarnings("GroovyUnusedDeclaration")
-class CopyResourceAssetsTask extends DefaultTask {
-
- @TaskAction
- void copyResourceAssets() {
- def baseUrl = "https://s3-us-west-2.amazonaws.com/iink/assets/$project.ext.iinkVersionName"
- def urls = [
- "$baseUrl/myscript-iink-recognition-diagram.zip",
- "$baseUrl/myscript-iink-recognition-raw-content.zip",
- "$baseUrl/myscript-iink-recognition-math.zip",
- "$baseUrl/myscript-iink-recognition-text-en_US.zip"
- ]
-
- def intoDir = project.file("$project.projectDir/src/main/assets")
- if (!intoDir.isDirectory())
- intoDir.mkdirs()
-
- def diagramConf = project.file("$intoDir/conf/diagram.conf")
- def rawContentConf = project.file("$intoDir/conf/raw-content.conf")
- def mathConf = project.file("$intoDir/conf/math.conf")
- def textConf = project.file("$intoDir/conf/en_US.conf")
-
- if (!diagramConf.exists() || !rawContentConf.exists() || !mathConf.exists() || !textConf
- .exists()) {
- def fromDir = project.file("$intoDir/temp")
- if (!fromDir.isDirectory())
- fromDir.mkdirs()
-
- // download resource zips
- urls.each { url ->
- ant.get(src: url, dest: fromDir.getPath())
- }
-
- // unzip
- fromDir.listFiles({ it.name.endsWith(".zip") } as FileFilter).each {
- def filePath = it.getPath()
- project.copy {
- from project.zipTree(filePath)
- into fromDir
- }
- }
-
- // copy into assets folder
- project.copy {
- from "$fromDir/recognition-assets"
- into intoDir
- }
-
- // delete useless files
- project.delete(fromDir)
- }
- }
-}
diff --git a/java/common-kotlin/.gitignore b/java/common-kotlin/.gitignore
deleted file mode 100644
index 42afabf..0000000
--- a/java/common-kotlin/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
\ No newline at end of file
diff --git a/java/common-kotlin/build.gradle b/java/common-kotlin/build.gradle
deleted file mode 100644
index 66a98e2..0000000
--- a/java/common-kotlin/build.gradle
+++ /dev/null
@@ -1,53 +0,0 @@
-import com.myscript.gradle.tasks.CopyResourceAssetsTask
-
-/*
- * Copyright (c) MyScript. All rights reserved.
- */
-
-plugins {
- id 'com.android.library'
- id 'org.jetbrains.kotlin.android'
-}
-
-android {
- compileSdk project.ext.compileSdkVersion
-
- defaultConfig {
- minSdkVersion project.ext.minSdkVersion
- targetSdkVersion project.ext.targetSdkVersion
- versionCode project.ext.iinkVersionCode
- versionName project.ext.iinkVersionName
- }
-
- buildTypes {
- release {
- minifyEnabled false
- }
- }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
- kotlinOptions {
- jvmTarget = '1.8'
- }
-}
-
-dependencies {
-
- implementation "androidx.core:core-ktx:${project.ext.kotlinCoreVersion}"
- implementation "androidx.appcompat:appcompat:${project.ext.appcompatVersion}"
- implementation "com.google.android.material:material:${project.ext.materialLibraryVersion}"
-
- // iink SDK.
- // once iink wil be published we will use this
- api "com.myscript:iink:${project.ext.iinkVersionName}"
-}
-
-clean.doFirst {
- delete "${projectDir}/src/main/assets/conf"
- delete "${projectDir}/src/main/assets/resources"
-}
-
-task copyResourceAssets(type: CopyResourceAssetsTask)
-preBuild.dependsOn copyResourceAssets
\ No newline at end of file
diff --git a/java/common-kotlin/src/main/AndroidManifest.xml b/java/common-kotlin/src/main/AndroidManifest.xml
deleted file mode 100644
index e07f1c9..0000000
--- a/java/common-kotlin/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/java/common-kotlin/src/main/java/com/myscript/iink/app/common/IInteractiveInkApplication.kt b/java/common-kotlin/src/main/java/com/myscript/iink/app/common/IInteractiveInkApplication.kt
deleted file mode 100644
index 35512e7..0000000
--- a/java/common-kotlin/src/main/java/com/myscript/iink/app/common/IInteractiveInkApplication.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-/*
- * Copyright (c) MyScript. All rights reserved.
- */
-
-package com.myscript.iink.app.common
-
-annotation class IInteractiveInkApplication()
diff --git a/java/common-kotlin/src/main/java/com/myscript/iink/app/common/utils/CustomProgressDialog.kt b/java/common-kotlin/src/main/java/com/myscript/iink/app/common/utils/CustomProgressDialog.kt
deleted file mode 100644
index 0fa03b8..0000000
--- a/java/common-kotlin/src/main/java/com/myscript/iink/app/common/utils/CustomProgressDialog.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.myscript.iink.app.common.utils
-
-import android.app.Dialog
-import android.content.Context
-import android.view.Gravity
-import android.view.LayoutInflater
-import android.view.View
-import android.view.WindowManager
-import com.myscript.iink.app.common.R
-
-class CustomProgressDialog(ctx:Context) : Dialog(ctx){
- init{
- val wlmp: WindowManager.LayoutParams = window!!.attributes
-
- wlmp.gravity = Gravity.CENTER_HORIZONTAL
- window!!.attributes = wlmp
- setTitle(null)
- setCancelable(false)
- setOnCancelListener(null)
- val view: View = LayoutInflater.from(context).inflate(
- R.layout.custom_progress_dlg, null
- )
- setContentView(view)
- }
-}
\ No newline at end of file
diff --git a/java/common-kotlin/src/main/java/com/myscript/iink/app/common/utils/Dialogs.kt b/java/common-kotlin/src/main/java/com/myscript/iink/app/common/utils/Dialogs.kt
deleted file mode 100644
index 123fc02..0000000
--- a/java/common-kotlin/src/main/java/com/myscript/iink/app/common/utils/Dialogs.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright @ MyScript. All rights reserved.
-
-package com.myscript.iink.app.common.utils
-
-import android.app.AlertDialog
-import android.content.Context
-import android.content.DialogInterface
-import android.view.LayoutInflater
-import android.widget.EditText
-import androidx.annotation.StringRes
-import com.myscript.iink.app.common.R
-
-fun Context.launchSingleChoiceDialog(
- @StringRes titleRes: Int,
- items: List,
- selectedIndex: Int = -1,
- onItemSelected: (selected: Int) -> Unit,
- onCancelSelected: DialogInterface.OnClickListener? =null
-){
- var newIndex = selectedIndex
- AlertDialog.Builder(this)
- .setTitle(titleRes)
- .setSingleChoiceItems(
- items.toTypedArray(),
- selectedIndex
- ) { _, which ->
- newIndex = which
- }
- .setPositiveButton(R.string.dialog_ok) { _, _ ->
- if (newIndex in items.indices) {
- onItemSelected(newIndex)
- }
- }
- .setNegativeButton(R.string.dialog_cancel, onCancelSelected)
- .show()
-}
-
-fun Context.launchActionChoiceDialog(
- @StringRes titleRes: Int,
- items: List,
- onItemSelected: (selected: Int) -> Unit
-)
-{
- AlertDialog.Builder(this)
- .setTitle(titleRes)
- .setItems(items.toTypedArray()) { _, which ->
- if (which in items.indices) {
- onItemSelected(which)
- }
- }
- .show()
-}
-fun Context.launchActionChoiceDialog(
- items: List,
- onItemSelected: (selected: Int) -> Unit
-) {
- AlertDialog.Builder(this)
- .setItems(items.toTypedArray()) { _, which ->
- if (which in items.indices) {
- onItemSelected(which)
- }
- }
- .show()
-}
-
-fun Context.launchTextBlockInputDialog(onInputDone: (text: String) -> Unit) {
- val editTextLayout = LayoutInflater.from(this).inflate(R.layout.editor_text_input_layout, null)
- val editText = editTextLayout.findViewById(R.id.editor_text_input)
- val builder = AlertDialog.Builder(this)
- .setView(editTextLayout)
- .setTitle(R.string.editor_dialog_insert_text_title)
- .setPositiveButton(R.string.editor_dialog_insert_text_action) { _, _ ->
- val text = editText.text.toString()
- if (text.isNotBlank()) {
- onInputDone(text)
- }
- }
- .setNegativeButton(R.string.dialog_cancel, null)
- .create()
- editText.requestFocus()
- builder.show()
-}
\ No newline at end of file
diff --git a/java/common-kotlin/src/main/res/layout/custom_progress_dlg.xml b/java/common-kotlin/src/main/res/layout/custom_progress_dlg.xml
deleted file mode 100644
index 45c2d4e..0000000
--- a/java/common-kotlin/src/main/res/layout/custom_progress_dlg.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/java/common-kotlin/src/main/res/layout/editor_text_input_layout.xml b/java/common-kotlin/src/main/res/layout/editor_text_input_layout.xml
deleted file mode 100644
index 7db19d1..0000000
--- a/java/common-kotlin/src/main/res/layout/editor_text_input_layout.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
diff --git a/java/common-kotlin/src/main/res/values/strings.xml b/java/common-kotlin/src/main/res/values/strings.xml
deleted file mode 100644
index 7321b97..0000000
--- a/java/common-kotlin/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- OK
- Cancel
-
- Invalid certificate
- Please check certificate data provided at Engine creation.
- [%1$s] %2$s
-
- Add text block
- Insert
-
\ No newline at end of file
diff --git a/java/gradle/wrapper/gradle-wrapper.jar b/java/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index 5c2d1cf..0000000
Binary files a/java/gradle/wrapper/gradle-wrapper.jar and /dev/null differ
diff --git a/java/gradlew b/java/gradlew
deleted file mode 100755
index dd7dcde..0000000
--- a/java/gradlew
+++ /dev/null
@@ -1,189 +0,0 @@
-#!/usr/bin/env sh
-
-#
-# Copyright 2015 the original author or authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-##############################################################################
-##
-## Gradle start up script for UN*X
-##
-##############################################################################
-
-# Attempt to set APP_HOME
-# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
-done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
-
-# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
-
-warn () {
- echo "$*"
-}
-
-die () {
- echo
- echo "$*"
- echo
- exit 1
-}
-
-# OS specific support (must be 'true' or 'false').
-cygwin=false
-msys=false
-darwin=false
-nonstop=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
- NONSTOP* )
- nonstop=true
- ;;
-esac
-
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
-
-# Determine the Java command to use to start the JVM.
-if [ -n "$JAVA_HOME" ] ; then
- if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
- # IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
- else
- JAVACMD="$JAVA_HOME/bin/java"
- fi
- if [ ! -x "$JAVACMD" ] ; then
- die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
- fi
-else
- JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-fi
-
-# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
-fi
-
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
-
-# For Cygwin or MSYS, switch paths to Windows format before running java
-if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-
- JAVACMD=`cygpath --unix "$JAVACMD"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
- # Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
- fi
- i=`expr $i + 1`
- done
- case $i in
- 0) set -- ;;
- 1) set -- "$args0" ;;
- 2) set -- "$args0" "$args1" ;;
- 3) set -- "$args0" "$args1" "$args2" ;;
- 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
- esac
-fi
-
-# Escape application args
-save () {
- for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
- echo " "
-}
-APP_ARGS=`save "$@"`
-
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
-
-# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
-if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
- cd "$(dirname "$0")"
-fi
-
-exec "$JAVACMD" "$@"
diff --git a/java/samples/batch-mode-kt/build.gradle b/java/samples/batch-mode-kt/build.gradle
deleted file mode 100644
index 7dd82fc..0000000
--- a/java/samples/batch-mode-kt/build.gradle
+++ /dev/null
@@ -1,41 +0,0 @@
-plugins {
- id 'com.android.application'
- id 'kotlin-android'
-}
-
-android {
- compileSdkVersion project.ext.compileSdkVersion
-
- buildFeatures {
- viewBinding true
- }
-
- defaultConfig {
- minSdkVersion project.ext.minSdkVersion
- targetSdkVersion project.ext.targetSdkVersion
-
- applicationId 'com.myscript.iink.samples.batchmode'
- versionCode project.ext.iinkVersionCode
- versionName project.ext.iinkVersionName
-
- vectorDrawables.useSupportLibrary true
- }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
- kotlinOptions {
- jvmTarget = '1.8'
- }
-}
-
-dependencies {
- implementation "androidx.core:core-ktx:${project.ext.kotlinCoreVersion}"
- implementation "androidx.appcompat:appcompat:${project.ext.appcompatVersion}"
- implementation "com.google.android.material:material:${project.ext.materialLibraryVersion}"
- implementation "com.google.code.gson:gson:${project.ext.gsonVersion}"
-
- implementation project(':UIReferenceImplementation')
- implementation project(':myscript-certificate')
- implementation project(':common-kotlin')
-}
\ No newline at end of file
diff --git a/java/samples/batch-mode-kt/src/main/java/com/myscript/iink/samples/batchmode/MainActivity.kt b/java/samples/batch-mode-kt/src/main/java/com/myscript/iink/samples/batchmode/MainActivity.kt
deleted file mode 100644
index cf5328a..0000000
--- a/java/samples/batch-mode-kt/src/main/java/com/myscript/iink/samples/batchmode/MainActivity.kt
+++ /dev/null
@@ -1,285 +0,0 @@
-package com.myscript.iink.samples.batchmode
-
-import android.content.DialogInterface
-import android.graphics.Typeface
-import android.os.Bundle
-import android.util.DisplayMetrics
-import android.util.Log
-import android.widget.Toast
-import androidx.appcompat.app.AlertDialog
-import androidx.appcompat.app.AppCompatActivity
-import com.google.gson.Gson
-import com.myscript.iink.*
-import com.myscript.iink.app.common.utils.autoCloseable
-import com.myscript.iink.app.common.utils.launchSingleChoiceDialog
-import com.myscript.iink.uireferenceimplementation.FontMetricsProvider
-import com.myscript.iink.uireferenceimplementation.FontUtils
-import com.myscript.iink.uireferenceimplementation.ImageLoader
-import com.myscript.iink.uireferenceimplementation.ImagePainter
-import java.io.*
-
-
-class MainActivity : AppCompatActivity() {
- private val TAG = "MainActivity"
-
- /**/
- private var renderer by autoCloseable(null)
- private var editor by autoCloseable(null)
- private var contentPackage : ContentPackage? =null
- private var contentPart : ContentPart? = null
-
- /**/
- //warning use the real myscript name of part as this string will be use for part creation
- private val typeOfPart = listOf("Text", "Math", "Diagram", "Raw Content")
- private val iinkPackageName = "package.iink"
- private val exportFileName = "export"
- private val language = "en_US"
- private val incremental = false
-
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- // Note: could be managed by domain layer and handled through observable error channel
- // but kept simple as is to avoid adding too much complexity for this special (unrecoverable) error case
- if (IInkApplication.getEngine() == null) {
- // the certificate provided in `BatchModule.provideEngine` is most likely incorrect
- AlertDialog.Builder(this)
- .setTitle( getString(R.string.app_error_invalid_certificate_title))
- .setMessage( getString(R.string.app_error_invalid_certificate_message))
- .setPositiveButton(R.string.dialog_ok, null)
- .show()
- finishAndRemoveTask() // be sure to end the application
- return
- }
-
- // at creation we have to pre initialise the editor with dpi and screen size
- intialConfiguration()
-
- //Small dialog to ask user which type of Part he/she wants to proceed
- launchSingleChoiceDialog(R.string.dialog_Part_choice_Title,
- typeOfPart,
- 0,
- {
- // this is the function where we process exteranl output and export it
- // add true if you want to export in png
- offScreenProcess(typeOfPart[it])
-
- //close the application
- Thread(Runnable {
- Thread.sleep(2000)// just ot let time to display the toast (2sec)
- finishAndRemoveTask() // be sure to end the application
- }).start()
- },
- DialogInterface.OnClickListener { _, _ -> finishAndRemoveTask() })
- }
-
- private fun intialConfiguration(){
- val displayMetrics = resources.displayMetrics
-
- // configure recognition
- IInkApplication.getEngine()?.apply {
- configuration.let { conf ->
- val confDir = "zip://${application.packageCodePath}!/assets/conf"
- conf.setStringArray("configuration-manager.search-path", arrayOf(confDir))
- val tempDir = File(application.cacheDir, "tmp")
- conf.setString("content-package.temp-folder", tempDir.absolutePath)
-
- // To enable text recognition for a specific language,
- conf.setString("lang", language);
- // Configure the engine to disable guides (recommended in batch mode)
- conf.setBoolean("text.guides.enable", false);
- }
- }
-
- // Create a renderer with a null render target
- renderer = IInkApplication.getEngine()?.createRenderer(displayMetrics.xdpi, displayMetrics.ydpi, null)
- renderer?.setViewOffset(0.0f, 0.0f)
- renderer?.viewScale = 1.0f
-
- // Create the editor
- editor = IInkApplication.getEngine()?.createEditor(renderer!!, IInkApplication.getEngine()!!.createToolController())
-
- // The editor requires a font metrics provider and a view size *before* calling setPart()
- val typefaceMap: Map = HashMap()
- editor!!.setFontMetricsProvider(FontMetricsProvider(displayMetrics, typefaceMap))
- editor!!.setViewSize(displayMetrics.widthPixels, displayMetrics.heightPixels);
- }
-
- private fun offScreenProcess(partType: String, renderToPNG : Boolean = false){
- try {
- // Create a new package
- contentPackage = IInkApplication.getEngine()?.createPackage(iinkPackageName)!!
-
- // Create a new part
- contentPart = contentPackage!!.createPart(partType)
- } catch (e: IOException) {
- Log.e(TAG, "Failed to open package \"$iinkPackageName\"", e)
- AlertDialog.Builder(this)
- .setTitle( "Package Creation IllegalArgumentException")
- .setMessage( "Failed to open package \"$iinkPackageName\"")
- .setPositiveButton(R.string.dialog_ok, null)
- .show()
- return;
-
- } catch (e: IllegalArgumentException) {
- Log.e(TAG, "Failed to open type of part \"$partType\"", e)
- AlertDialog.Builder(this)
- .setTitle( "Part Creation IllegalArgumentException")
- .setMessage( "Failed to open type of part \"$partType\"")
- .setPositiveButton(R.string.dialog_ok, null)
- .show()
- return;
- }
-
- // Associate editor with the new part
- editor!!.part = contentPart
-
- // now we can process pointer events
- // and we feed the editor with an array of Pointer Events loaded for the right json file
- // incremntal way or not
- loadAndFeedPointerEvents(incremental, editor!!, partType, resources.displayMetrics)
-
-
- // choose the right mimeType to export according to the partType we choose
- var mimeType = MimeType.PNG
- if(!renderToPNG) {
- when (partType) {
- typeOfPart[0] -> mimeType = MimeType.TEXT // Text
- typeOfPart[1] -> mimeType = MimeType.LATEX // Math
- typeOfPart[2] -> mimeType = MimeType.SVG // Diagram
- typeOfPart[3] -> mimeType = MimeType.JIIX //Raw Content
- else -> {}
- }
- }
-
- // Exported file is stored in the Virtual SD Card : "Android/data/com.myscript.iink.samples.batchmode/files"
- val file = File(
- getExternalFilesDir(null),
- File.separator + exportFileName.toString() + mimeType.fileExtensions
- )
- editor!!.waitForIdle()
- try {
- var imagePainter : ImagePainter? = null
- if(mimeType.isImage) {
- //we have to create a image painter to render in png
- imagePainter = ImagePainter().apply {
- setImageLoader(ImageLoader(editor!!))
- // load fonts
-
- // load fonts
- val assetManager = applicationContext.assets
- val typefaceMap = FontUtils.loadFontsFromAssets(assetManager)
- setTypefaceMap(typefaceMap)
- }
- }
- editor!!.export_(null, file.getAbsolutePath(), mimeType, imagePainter)
- } catch (e: Exception) {
- e.printStackTrace()
- }
-
- //quick reminder display of where the data has been exported
- Toast.makeText(applicationContext, "File exported in : ${file.path}", Toast.LENGTH_SHORT).show()
-
- //clean the elements
- editor!!.part = null
- contentPart?.close()
- contentPackage?.close()
- try {
- IInkApplication.getEngine()?.deletePackage(packageName)
- } catch (e: IOException) {
- e.printStackTrace()
- }
- editor!!.close()
- renderer!!.close()
-
- }
-
- private fun loadAndFeedPointerEvents(incremental : Boolean, editor: Editor, partType: String, displayMetrics: DisplayMetrics){
- var pointerEventsPath =
- "conf/pointerEvents/${partType.lowercase()}/pointerEvents.json"
- if (partType.lowercase().equals("text")) {
- pointerEventsPath = "conf/pointerEvents/${partType.lowercase()}/$language/pointerEvents.json"
- }
-
- try {
- // Loading the content of the pointerEvents JSON file
- val inputStream: InputStream = resources.assets.open(pointerEventsPath)
-
- // Mapping the content into a JsonResult class
- val jsonResult: JsonResult =
- Gson().fromJson(InputStreamReader(inputStream), JsonResult::class.java)
-
- // add each element to a list
- var pointerEventsList = mutableListOf()
-
- for (stroke in jsonResult.getStrokes()!!) {
- val strokeX: FloatArray = stroke.x
- val strokeY: FloatArray = stroke.y
- val strokeT: LongArray = stroke.t
- val strokeP: FloatArray = stroke.p
- val length: Int = stroke.x.size
- for (j in 0 until length) {
- if(incremental){
- //in incremental mode we send data direct to the editor
- if (j == 0) {
- editor.pointerDown(strokeX[j] / 25.4f * displayMetrics.xdpi,
- strokeY[j] / 25.4f * displayMetrics.ydpi,
- strokeT[j],
- strokeP[j],
- PointerType.PEN,
- 1)
- } else if (j == length - 1) {
- editor.pointerUp(strokeX[j] / 25.4f * displayMetrics.xdpi,
- strokeY[j] / 25.4f * displayMetrics.ydpi,
- strokeT[j],
- strokeP[j],
- PointerType.PEN,
- 1)
- } else {
- editor.pointerMove(strokeX[j] / 25.4f * displayMetrics.xdpi,
- strokeY[j] / 25.4f * displayMetrics.ydpi,
- strokeT[j],
- strokeP[j],
- PointerType.PEN,
- stroke.pointerId)
- }
- }else {
- //in batch mode we keep data in a array
- val pointerEvent = PointerEvent()
- pointerEvent.pointerType = stroke.pointerType!!
- pointerEvent.pointerId = stroke.pointerId
- if (j == 0) {
- pointerEvent.eventType = PointerEventType.DOWN
- } else if (j == length - 1) {
- pointerEvent.eventType = PointerEventType.UP
- } else {
- pointerEvent.eventType = PointerEventType.MOVE
- }
-
- // Transform the x and y coordinates of the stroke from mm to px
- // This is needed to be adaptive for each device
- pointerEvent.x = strokeX[j] / 25.4f * displayMetrics.xdpi
- pointerEvent.y = strokeY[j] / 25.4f * displayMetrics.ydpi
- pointerEvent.t = strokeT[j]
- pointerEvent.f = strokeP[j]
- //add it to the list
- pointerEventsList += pointerEvent
- }
- }
- }
- if(!incremental){
- editor.pointerEvents(pointerEventsList.toTypedArray(), false)
- }
- } catch (e: FileNotFoundException) {
- AlertDialog.Builder(this)
- .setTitle( "file not found")
- .setMessage( "no file to parse found : $pointerEventsPath")
- .setPositiveButton(R.string.dialog_ok, null)
- .show()
- e.printStackTrace()
- } catch (e: IOException) {
- e.printStackTrace()
- }
- }
-}
\ No newline at end of file
diff --git a/java/samples/batch-mode-kt/src/main/res/values-night/themes.xml b/java/samples/batch-mode-kt/src/main/res/values-night/themes.xml
deleted file mode 100644
index 47fc32a..0000000
--- a/java/samples/batch-mode-kt/src/main/res/values-night/themes.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/java/samples/batch-mode-kt/src/main/res/values/colors.xml b/java/samples/batch-mode-kt/src/main/res/values/colors.xml
deleted file mode 100644
index f8c6127..0000000
--- a/java/samples/batch-mode-kt/src/main/res/values/colors.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
- #FFBB86FC
- #FF6200EE
- #FF3700B3
- #FF03DAC5
- #FF018786
- #FF000000
- #FFFFFFFF
-
\ No newline at end of file
diff --git a/java/samples/batch-mode-kt/src/main/res/values/strings.xml b/java/samples/batch-mode-kt/src/main/res/values/strings.xml
deleted file mode 100644
index ae8c503..0000000
--- a/java/samples/batch-mode-kt/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
- Batch Mode
-
- Invalid certificate
- Please check certificate data provided at Engine creation.
-
- Choose the part type sample you want to proceed. \n This operation may take time!
- Start the conversion
-
-
\ No newline at end of file
diff --git a/java/samples/batch-mode-kt/src/main/res/values/themes.xml b/java/samples/batch-mode-kt/src/main/res/values/themes.xml
deleted file mode 100644
index fbb15da..0000000
--- a/java/samples/batch-mode-kt/src/main/res/values/themes.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/java/samples/exercise-assessment-kt/.gitignore b/java/samples/exercise-assessment-kt/.gitignore
deleted file mode 100644
index 42afabf..0000000
--- a/java/samples/exercise-assessment-kt/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
\ No newline at end of file
diff --git a/java/samples/exercise-assessment-kt/build.gradle b/java/samples/exercise-assessment-kt/build.gradle
deleted file mode 100644
index d133ef5..0000000
--- a/java/samples/exercise-assessment-kt/build.gradle
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (c) MyScript. All rights reserved.
- */
-
-plugins {
- id 'com.android.application'
- id 'org.jetbrains.kotlin.android'
-}
-
-android {
- compileSdk project.ext.compileSdkVersion
-
- defaultConfig {
- applicationId "com.myscript.iink.samples.assessment"
-
- minSdkVersion project.ext.minSdkVersion
- targetSdkVersion project.ext.targetSdkVersion
- versionCode project.ext.iinkVersionCode
- versionName project.ext.iinkVersionName
- }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
- kotlinOptions {
- jvmTarget = '1.8'
- }
-}
-
-dependencies {
-
- implementation "androidx.core:core-ktx:${project.ext.kotlinCoreVersion}"
- implementation "androidx.appcompat:appcompat:${project.ext.appcompatVersion}"
- implementation "com.google.android.material:material:${project.ext.materialLibraryVersion}"
-
- implementation project(':UIReferenceImplementation')
- implementation project(':myscript-certificate')
- implementation project(':common-kotlin')
-}
-
-task copyCustomMathConfigRecognitionAssets(type: org.gradle.api.tasks.Copy) {
- from "${projectDir}/custom_res"
- into "${projectDir}/src/main/assets"
-}
-
-preBuild.dependsOn(copyCustomMathConfigRecognitionAssets)
\ No newline at end of file
diff --git a/java/samples/exercise-assessment-kt/src/main/java/com/myscript/iink/samples/assessment/MathGrammarK8DynamicRes.kt b/java/samples/exercise-assessment-kt/src/main/java/com/myscript/iink/samples/assessment/MathGrammarK8DynamicRes.kt
deleted file mode 100644
index d81f9b8..0000000
--- a/java/samples/exercise-assessment-kt/src/main/java/com/myscript/iink/samples/assessment/MathGrammarK8DynamicRes.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (c) MyScript. All rights reserved.
- */
-
-package com.myscript.iink.samples.assessment
-
-import com.myscript.iink.Engine
-import java.io.File
-import java.io.IOException
-
-
-class MathGrammarK8DynamicRes
-{
- private val fileName = "math-grm-standardK8.res"
- private val k8Grammar = """symbol = 0 1 2 3 4 5 6 7 8 9 + - / ÷ = . , % | ( ) : * x
-leftpar = (
-rightpar = )
-currency_symbol = $ R € ₹ £
-character ::= identity(symbol)
- | identity(currency_symbol)
-fractionless ::= identity(character)
- | fence (fractionless, leftpar, rightpar)
- | hpair(fractionless, fractionless)
-fractionable ::= identity(character)
- | fence (fractionable, leftpar, rightpar)
- | hpair(fractionable, fractionable)
- | fraction(fractionless, fractionless)
-expression ::= identity(character)
- | fence (expression, leftpar, rightpar)
- | hpair(expression, expression)
- | fraction(fractionable, fractionable)
-start(expression)"""
-
- @Throws(IllegalArgumentException::class, RuntimeException::class, IOException::class)
- fun build(eng : Engine, filePath : String) {
- val rab = eng.createRecognitionAssetsBuilder()
- rab.compile("Math Grammar",k8Grammar)
- val file: File = File(filePath, File.separator + fileName)
- rab.store(file.path)
- }
-}
diff --git a/java/samples/search-kt/.gitignore b/java/samples/search-kt/.gitignore
deleted file mode 100644
index 796b96d..0000000
--- a/java/samples/search-kt/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
diff --git a/java/samples/search-kt/build.gradle b/java/samples/search-kt/build.gradle
deleted file mode 100644
index 6689b62..0000000
--- a/java/samples/search-kt/build.gradle
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (c) MyScript. All rights reserved.
- */
-
-plugins {
- id 'com.android.application'
- id 'org.jetbrains.kotlin.android'
-}
-
-android {
- compileSdkVersion project.ext.compileSdkVersion
- defaultConfig {
- applicationId "com.myscript.iink.samples.search"
- minSdkVersion project.ext.minSdkVersion
- targetSdkVersion project.ext.targetSdkVersion
- versionCode project.ext.iinkVersionCode
- versionName project.ext.iinkVersionName
- }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
- kotlinOptions {
- jvmTarget = '1.8'
- }
-}
-
-dependencies {
- implementation project(':UIReferenceImplementation')
- implementation project(':myscript-certificate')
- implementation project(':common-kotlin')
-
- implementation "androidx.core:core-ktx:${project.ext.kotlinCoreVersion}"
- implementation "androidx.appcompat:appcompat:${project.ext.appcompatVersion}"
- implementation "com.google.android.material:material:${project.ext.materialLibraryVersion}"
-
- // Google Gson
- implementation "com.google.code.gson:gson:${project.ext.gsonVersion}"
- implementation 'org.jetbrains:annotations:15.0'
-}
\ No newline at end of file
diff --git a/java/settings.gradle b/java/settings.gradle
deleted file mode 100644
index 450d65c..0000000
--- a/java/settings.gradle
+++ /dev/null
@@ -1,30 +0,0 @@
-
-/*
- * Copyright (c) MyScript. All rights reserved.
- */
-
-rootProject.name = 'myscript.iink.samples.android.java'
-
-def myProjects = [
- // samples
- ':batch-mode-kotlin' : file("$settingsDir/samples/batch-mode-kt"),
- ':exercise-assessment-kotlin' : file("$settingsDir/samples/exercise-assessment-kt"),
- ':search-sample-kotlin' : file("$settingsDir/samples/search-kt"),
- // MyScript certificate -> this should be empty
- ':myscript-certificate' : file("${settingsDir.parent}/certificate"),
- // common libraries
- ':common-kotlin' : file("$settingsDir/common-kotlin"),
- // MyScript iink UI reference implementation.
- // Copy from: https://github.com/MyScript/interactive-ink-examples-android/tree/master/UIReferenceImplementation
- ':UIReferenceImplementation' : file("${settingsDir.parent}/UIReferenceImplementation"),
-
-]
-
-myProjects.each { myProject, dir ->
- logger.info("Including $myProject")
- include myProject
- if (dir != null)
- project(myProject).projectDir = dir
- if(myProject==':myscript-certificate')
- project(myProject).buildFileName = 'build_android.gradle'
-}
diff --git a/offscreen-sample.gif b/offscreen-sample.gif
new file mode 100644
index 0000000..717d516
Binary files /dev/null and b/offscreen-sample.gif differ
diff --git a/UIReferenceImplementation/build.gradle b/samples/UIReferenceImplementation/build.gradle
similarity index 51%
rename from UIReferenceImplementation/build.gradle
rename to samples/UIReferenceImplementation/build.gradle
index 6aec1a7..d7234d6 100644
--- a/UIReferenceImplementation/build.gradle
+++ b/samples/UIReferenceImplementation/build.gradle
@@ -3,13 +3,15 @@ plugins {
}
android {
- compileSdkVersion project.ext.compileSdkVersion
+ namespace 'com.myscript.iink.uireferenceimplementation'
+
+ compileSdk project.ext.compileSdk
defaultConfig {
- minSdkVersion project.ext.minSdkVersion
- targetSdkVersion project.ext.targetSdkVersion
- versionCode 2010
- versionName '2.0.1'
+ minSdk project.ext.minSdk
+ targetSdk project.ext.targetSdk
+ versionCode project.ext.iinkVersionCode
+ versionName project.ext.iinkVersionName
vectorDrawables.useSupportLibrary true
}
@@ -18,5 +20,7 @@ android {
dependencies {
implementation "androidx.appcompat:appcompat:${project.ext.appcompatVersion}"
implementation "com.google.code.gson:gson:${project.ext.gsonVersion}"
+ // iink SDK.
+ // once iink wil be published we will use this
api "com.myscript:iink:${project.ext.iinkVersionName}"
}
diff --git a/samples/UIReferenceImplementation/src/main/AndroidManifest.xml b/samples/UIReferenceImplementation/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..9b65eb0
--- /dev/null
+++ b/samples/UIReferenceImplementation/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/samples/UIReferenceImplementation/src/main/assets/fonts/MyScriptInter-Bold.otf b/samples/UIReferenceImplementation/src/main/assets/fonts/MyScriptInter-Bold.otf
new file mode 100644
index 0000000..93179f8
Binary files /dev/null and b/samples/UIReferenceImplementation/src/main/assets/fonts/MyScriptInter-Bold.otf differ
diff --git a/samples/UIReferenceImplementation/src/main/assets/fonts/MyScriptInter-Regular.otf b/samples/UIReferenceImplementation/src/main/assets/fonts/MyScriptInter-Regular.otf
new file mode 100644
index 0000000..935f484
Binary files /dev/null and b/samples/UIReferenceImplementation/src/main/assets/fonts/MyScriptInter-Regular.otf differ
diff --git a/samples/UIReferenceImplementation/src/main/assets/fonts/STIX-Italic.otf b/samples/UIReferenceImplementation/src/main/assets/fonts/STIX-Italic.otf
new file mode 100644
index 0000000..b79db47
Binary files /dev/null and b/samples/UIReferenceImplementation/src/main/assets/fonts/STIX-Italic.otf differ
diff --git a/samples/UIReferenceImplementation/src/main/assets/fonts/STIXGeneral.ttf b/samples/UIReferenceImplementation/src/main/assets/fonts/STIXGeneral.ttf
new file mode 100644
index 0000000..ac15532
Binary files /dev/null and b/samples/UIReferenceImplementation/src/main/assets/fonts/STIXGeneral.ttf differ
diff --git a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/Canvas.java b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/Canvas.java
similarity index 70%
rename from UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/Canvas.java
rename to samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/Canvas.java
index d84d3d2..a787f79 100644
--- a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/Canvas.java
+++ b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/Canvas.java
@@ -6,41 +6,48 @@
import android.graphics.DashPathEffect;
import android.graphics.Matrix;
import android.graphics.Paint;
+import android.graphics.PointF;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
-
-import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.graphics.ColorUtils;
-
+import android.graphics.Xfermode;
import android.text.TextPaint;
import android.util.Log;
+import com.myscript.iink.GLRenderer;
+import com.myscript.iink.ParameterSet;
import com.myscript.iink.graphics.Color;
+import com.myscript.iink.graphics.ExtraBrushStyle;
import com.myscript.iink.graphics.FillRule;
import com.myscript.iink.graphics.ICanvas;
import com.myscript.iink.graphics.IPath;
+import com.myscript.iink.graphics.InkPoints;
import com.myscript.iink.graphics.LineCap;
import com.myscript.iink.graphics.LineJoin;
-import com.myscript.iink.graphics.Point;
import com.myscript.iink.graphics.Style;
import com.myscript.iink.graphics.Transform;
-import java.util.HashSet;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
import java.util.Map;
-import java.util.Set;
+import java.util.Objects;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.graphics.ColorUtils;
public class Canvas implements ICanvas
{
private static final Style DEFAULT_SVG_STYLE = new Style();
+ private static final PorterDuffXfermode xferModeSrcOver = new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER);
- @NonNull
- private final android.graphics.Canvas canvas;
+ @Nullable
+ private android.graphics.Canvas canvas;
@NonNull
private final Paint strokePaint;
@@ -76,13 +83,18 @@ public class Canvas implements ICanvas
@Nullable
private final ImageLoader imageLoader;
private final OfflineSurfaceManager offlineSurfaceManager;
+ @Nullable
+ private GLRenderer glRenderer;
+ private boolean keepGLRenderer = false;
+
+ private boolean clearOnStartDraw = true;
- private final Set clips;
+ private final List clips;
private final Map typefaceMap;
private float[] dashArray;
- private int dashOffset = 0;
+ private float dashOffset = 0;
private final float xdpi;
private final float ydpi;
@@ -92,7 +104,37 @@ public class Canvas implements ICanvas
@NonNull
private final Matrix pointScaleMatrix;
- public Canvas(@NonNull android.graphics.Canvas canvas, Map typefaceMap, ImageLoader imageLoader, @Nullable OfflineSurfaceManager offlineSurfaceManager, float xdpi, float ydpi)
+ public static class ExtraBrushConfig
+ {
+ @NonNull
+ public final String baseName;
+ @NonNull
+ public final Bitmap stampBitmap;
+ @Nullable
+ public final Bitmap backgroundBitmap;
+ @NonNull
+ public final ParameterSet config;
+
+ public ExtraBrushConfig(@NonNull String baseName, @NonNull Bitmap stampBitmap, @Nullable Bitmap backgroundBitmap, @NonNull ParameterSet config)
+ {
+ this.baseName = baseName;
+ this.stampBitmap = stampBitmap;
+ this.backgroundBitmap = backgroundBitmap;
+ this.config = config;
+ }
+ }
+
+ public Canvas(@Nullable android.graphics.Canvas canvas, Map typefaceMap, ImageLoader imageLoader, float xdpi, float ydpi)
+ {
+ this(canvas, Collections.emptyList(), typefaceMap, imageLoader, null, xdpi, ydpi);
+ }
+
+ public Canvas(@Nullable android.graphics.Canvas canvas, @NonNull List extraBrushConfigs, Map typefaceMap, ImageLoader imageLoader, float xdpi, float ydpi)
+ {
+ this(canvas, extraBrushConfigs, typefaceMap, imageLoader, null, xdpi, ydpi);
+ }
+
+ public Canvas(@Nullable android.graphics.Canvas canvas, @NonNull List extraBrushConfigs, Map typefaceMap, ImageLoader imageLoader, @Nullable OfflineSurfaceManager offlineSurfaceManager, float xdpi, float ydpi)
{
this.canvas = canvas;
this.typefaceMap = typefaceMap;
@@ -101,7 +143,14 @@ public Canvas(@NonNull android.graphics.Canvas canvas, Map typ
this.xdpi = xdpi;
this.ydpi = ydpi;
- clips = new HashSet<>();
+ if (!extraBrushConfigs.isEmpty() && GLRenderer.isDeviceSupported())
+ {
+ glRenderer = new GLRenderer();
+ for (ExtraBrushConfig config : extraBrushConfigs)
+ glRenderer.configureBrush(config.baseName, config.stampBitmap, config.backgroundBitmap, config.config);
+ }
+
+ clips = new ArrayList<>();
strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
strokePaint.setStyle(Paint.Style.STROKE);
@@ -135,9 +184,28 @@ public Canvas(@NonNull android.graphics.Canvas canvas, Map typ
applyStyle(DEFAULT_SVG_STYLE);
}
- public Canvas(@NonNull android.graphics.Canvas canvas, Map typefaceMap, ImageLoader imageLoader, float xdpi, float ydpi)
+ public void destroy()
+ {
+ if (glRenderer != null)
+ {
+ glRenderer.destroy();
+ glRenderer = null;
+ }
+ }
+
+ public void setCanvas(@NonNull android.graphics.Canvas canvas)
+ {
+ this.canvas = canvas;
+ }
+
+ public void setClearOnStartDraw(boolean clearOnStartDraw)
{
- this(canvas, typefaceMap, imageLoader, null, xdpi, ydpi);
+ this.clearOnStartDraw = clearOnStartDraw;
+ }
+
+ public void setKeepGLRenderer(boolean keepGLRenderer)
+ {
+ this.keepGLRenderer = keepGLRenderer;
}
private void applyStyle(@NonNull Style style)
@@ -174,6 +242,7 @@ public void setTransform(@NonNull Transform transform)
transformValues[Matrix.MSCALE_Y] = (float) transform.yy;
transformValues[Matrix.MTRANS_Y] = (float) transform.ty;
+ Objects.requireNonNull(canvas);
transformMatrix.setValues(transformValues);
canvas.setMatrix(transformMatrix);
@@ -265,6 +334,7 @@ public void setStrokeDashArray(float[] strokeDashArray)
@Override
public void setStrokeDashOffset(float strokeDashOffset)
{
+ dashOffset = strokeDashOffset;
if (dashArray != null)
strokePaint.setPathEffect(new DashPathEffect(dashArray, dashOffset));
else
@@ -313,6 +383,7 @@ public final void setFontProperties(@NonNull String fontFamily, float fontLineHe
@Override
public void startDraw(int x, int y, int width, int height)
{
+ Objects.requireNonNull(canvas);
canvas.save();
pointsCache[0] = x;
@@ -322,7 +393,7 @@ public void startDraw(int x, int y, int width, int height)
// When offscreen rendering is supported, clear the destination
// Otherwise, do not clear the destination (e.g. when exporting image, we want a white background)
- if (offlineSurfaceManager != null)
+ if (offlineSurfaceManager != null && clearOnStartDraw)
canvas.drawRect(pointsCache[0], pointsCache[1], pointsCache[2], pointsCache[3], clearPaint);
// Hardware canvas does not support PorterDuffXfermode
@@ -332,6 +403,13 @@ public void startDraw(int x, int y, int width, int height)
@Override
public void endDraw()
{
+ if (!keepGLRenderer && glRenderer != null)
+ {
+ glRenderer.destroy();
+ glRenderer = null;
+ }
+
+ Objects.requireNonNull(canvas);
canvas.restore();
}
@@ -340,6 +418,7 @@ public void startGroup(@NonNull String id, float x, float y, float width, float
{
if (clipContent)
{
+ Objects.requireNonNull(canvas);
clips.add(id);
canvas.save();
@@ -350,9 +429,12 @@ public void startGroup(@NonNull String id, float x, float y, float width, float
@Override
public void endGroup(@NonNull String id)
{
- if (clips.remove(id))
+ int index = clips.lastIndexOf(id);
+ if (index != -1)
{
+ Objects.requireNonNull(canvas);
canvas.restore();
+ clips.remove(index);
}
}
@@ -378,6 +460,7 @@ public final IPath createPath()
@Override
public void drawPath(@NonNull IPath ipath)
{
+ Objects.requireNonNull(canvas);
Path path = (Path) ipath;
if (android.graphics.Color.alpha(fillPaint.getColor()) != 0)
@@ -391,9 +474,65 @@ public void drawPath(@NonNull IPath ipath)
}
}
+ @Override
+ public boolean isExtraBrushSupported(@NonNull String brushName)
+ {
+ return glRenderer != null && glRenderer.isBrushSupported(brushName);
+ }
+
+ @Override
+ public void drawStrokeWithExtraBrush(@NonNull InkPoints[] vInkPoints, int temporaryPoints,
+ @NonNull ExtraBrushStyle style, boolean fullStroke, long id)
+ {
+ Objects.requireNonNull(canvas);
+
+ if (!isExtraBrushSupported(style.brushName))
+ return;
+
+ if (vInkPoints.length == 0 || vInkPoints[0].x.length == 0 || style.strokeWidth <= 0.f || android.graphics.Color.alpha(fillPaint.getColor()) == 0)
+ return;
+
+ if (!glRenderer.isInitialized())
+ {
+ glRenderer.initialize(keepGLRenderer, canvas.getWidth(), canvas.getHeight(), xdpi, ydpi);
+ }
+
+ Xfermode xfm = fillPaint.getXfermode();
+
+ try
+ {
+ canvas.setMatrix(null); // GLRenderer works with pixels
+ fillPaint.setXfermode(xferModeSrcOver);
+
+ PointF strokeOrigin = glRenderer.drawStroke(vInkPoints, temporaryPoints, transformValues, style, fillPaint, fullStroke, id);
+ Bitmap strokeBitmap = glRenderer.saveStroke();
+ if (strokeBitmap != null)
+ canvas.drawBitmap(strokeBitmap, strokeOrigin.x, strokeOrigin.y, fillPaint);
+
+ if (temporaryPoints > 0 && vInkPoints.length == 1)
+ {
+ PointF temporaryOrigin = glRenderer.drawTemporary(vInkPoints, temporaryPoints, transformValues, style, fillPaint);
+ Bitmap temporaryBitmap = glRenderer.saveTemporary();
+ if (temporaryBitmap != null)
+ canvas.drawBitmap(temporaryBitmap, temporaryOrigin.x, temporaryOrigin.y, fillPaint);
+ }
+ }
+ catch (Exception e)
+ {
+ Log.e("Canvas", "Error trying to draw stroke with extra brush: " + e.getMessage(), e);
+ }
+ finally
+ {
+ // restore
+ fillPaint.setXfermode(xfm);
+ canvas.setMatrix(transformMatrix);
+ }
+ }
+
@Override
public void drawRectangle(float x, float y, float width, float height)
{
+ Objects.requireNonNull(canvas);
if (android.graphics.Color.alpha(fillPaint.getColor()) != 0)
{
canvas.drawRect(x, y, x + width, y + height, fillPaint);
@@ -407,6 +546,7 @@ public void drawRectangle(float x, float y, float width, float height)
@Override
public void drawLine(float x1, float y1, float x2, float y2)
{
+ Objects.requireNonNull(canvas);
canvas.drawLine(x1, y1, x2, y2, strokePaint);
}
@@ -416,16 +556,16 @@ public void drawObject(@NonNull String url, @NonNull String mimeType, float x, f
if (imageLoader == null)
return;
- Point screenMin = new Point(x, y);
- transform.apply(screenMin);
- Point screenMax = new Point(x + width, y + height);
- transform.apply(screenMax);
+ Objects.requireNonNull(canvas);
+
+ RectF pixelSize = new RectF(x,y,x + width, y + height);
+ transformMatrix.mapRect(pixelSize);
final Rect targetRect = new Rect(
- (int) Math.floor(screenMin.x),
- (int) Math.floor(screenMin.y),
- (int) (Math.ceil(screenMax.x) - x),
- (int) (Math.ceil(screenMax.y) - y));
+ (int) Math.floor(pixelSize.left),
+ (int) Math.floor(pixelSize.top),
+ (int) (Math.ceil(pixelSize.right)),
+ (int) (Math.ceil(pixelSize.bottom)));
synchronized (imageLoader)
{
@@ -441,22 +581,6 @@ public void drawObject(@NonNull String url, @NonNull String mimeType, float x, f
}
else
{
- // adjust rectangle so that the image gets fit into original rectangle
- float fx = width / image.getWidth();
- float fy = height / image.getHeight();
- if (fx > fy)
- {
- float w = image.getWidth() * fy;
- x += (width - w) / 2;
- width = w;
- }
- else
- {
- float h = image.getHeight() * fx;
- y += (height - h) / 2;
- height = h;
- }
-
// draw the image
Rect srcRect = new Rect(0, 0, image.getWidth(), image.getHeight());
RectF dstRect = new RectF(x, y, x + width, y + height);
@@ -471,6 +595,7 @@ public void drawObject(@NonNull String url, @NonNull String mimeType, float x, f
@Override
public void drawText(@NonNull String label, float x, float y, float xmin, float ymin, float xmax, float ymax)
{
+ Objects.requireNonNull(canvas);
// transform the insertion point so that it is not impacted by text scale
pointsCache[0] = x;
pointsCache[1] = y;
@@ -496,6 +621,7 @@ public void blendOffscreen(int id, float srcX, float srcY, float srcWidth, float
if (bitmap != null)
{
+ Objects.requireNonNull(canvas);
floatRectCache.set(destX, destY, destX + destWidth, destY + destHeight);
simpleRectCache.set(Math.round(srcX), Math.round(srcY),
Math.round(srcX + srcWidth), Math.round(srcY + srcHeight));
diff --git a/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ContextualActions.java b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ContextualActions.java
new file mode 100644
index 0000000..ce2f7d4
--- /dev/null
+++ b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ContextualActions.java
@@ -0,0 +1,53 @@
+package com.myscript.iink.uireferenceimplementation;
+
+import com.myscript.iink.ContentSelection;
+import com.myscript.iink.Editor;
+
+/**
+ * Describes the actions available for a given selection or block.
+ *
+ * @since 2.0
+ */
+public enum ContextualActions {
+ /**
+ * Add block
+ */
+ ADD_BLOCK,
+ /**
+ * Remove block or selection
+ */
+ REMOVE,
+ /**
+ * Convert.
+ * @see Editor#getSupportedTargetConversionStates(ContentSelection)
+ */
+ CONVERT,
+ /**
+ * Copy block or selection
+ */
+ COPY,
+ /**
+ * Paste current copy
+ */
+ PASTE,
+ /**
+ * Export.
+ * @see Editor#getSupportedExportMimeTypes(ContentSelection)
+ */
+ EXPORT,
+ /**
+ * Change Text blocks format.
+ * @see Editor#getSupportedTextFormats(ContentSelection)
+ */
+ FORMAT_TEXT,
+ /**
+ * Change selection mode.
+ * @see Editor#getAvailableSelectionModes()
+ */
+ SELECTION_MODE,
+ /**
+ * Change selection type.
+ * @see Editor#getAvailableSelectionTypes(ContentSelection)
+ */
+ SELECTION_TYPE
+}
diff --git a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ContextualActionsHelper.java b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ContextualActionsHelper.java
similarity index 78%
rename from UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ContextualActionsHelper.java
rename to samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ContextualActionsHelper.java
index 4ba7399..972d845 100644
--- a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ContextualActionsHelper.java
+++ b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ContextualActionsHelper.java
@@ -5,7 +5,7 @@
import com.myscript.iink.ContentBlock;
import com.myscript.iink.ContentPart;
import com.myscript.iink.ContentSelection;
-import com.myscript.iink.ContextualActions;
+import com.myscript.iink.ContentSelectionMode;
import com.myscript.iink.Editor;
import java.util.EnumSet;
@@ -32,12 +32,14 @@ public static EnumSet getAvailableActionsForBlock(@NonNull Ed
boolean onTextDocument = "Text Document".equals(part != null ? part.getType() : null);
boolean blockIsEmpty = editor.isEmpty(block);
- boolean displayAddBlock = editor.getSupportedAddBlockTypes().length > 0 && isRootBlock;
+ boolean displayAddBlock = editor.getSupportedAddBlockTypes().length > 0 && (!onTextDocument || isRootBlock);
boolean displayRemove = !isRootBlock;
boolean displayCopy = !isRootBlock || !onTextDocument;
boolean displayConvert = !blockIsEmpty && editor.getSupportedTargetConversionStates(block).length > 0;
boolean displayExport = editor.getSupportedExportMimeTypes(block).length > 0;
boolean displayFormatText = editor.getSupportedTextFormats(block).size() > 0;
+ boolean displaySelectionMode = editor.getAvailableSelectionModes().size() > 0;
+ boolean displaySelectionType = editor.getAvailableSelectionTypes(block).length > 0;
if (displayAddBlock) actions.add(ContextualActions.ADD_BLOCK);
if (displayRemove) actions.add(ContextualActions.REMOVE);
@@ -46,6 +48,8 @@ public static EnumSet getAvailableActionsForBlock(@NonNull Ed
if (isRootBlock) actions.add(ContextualActions.PASTE);
if (displayExport) actions.add(ContextualActions.EXPORT);
if (displayFormatText) actions.add(ContextualActions.FORMAT_TEXT);
+ if (displaySelectionMode) actions.add(ContextualActions.SELECTION_MODE);
+ if (displaySelectionType) actions.add(ContextualActions.SELECTION_TYPE);
}
return actions;
}
@@ -58,12 +62,16 @@ public static EnumSet getAvailableActionsForSelection(@NonNul
boolean displayConvert = editor.getSupportedTargetConversionStates(selection).length > 0;
boolean displayExport = editor.getSupportedExportMimeTypes(selection).length > 0;
boolean displayFormatText = selection != null && !editor.getSupportedTextFormats(selection).isEmpty();
+ boolean displaySelectionMode = editor.getAvailableSelectionModes().size() > 0;
+ boolean displaySelectionType = editor.getAvailableSelectionTypes(selection).length > 0;
actions.add(ContextualActions.REMOVE);
if (displayConvert) actions.add(ContextualActions.CONVERT);
actions.add(ContextualActions.COPY);
if (displayExport) actions.add(ContextualActions.EXPORT);
if (displayFormatText) actions.add(ContextualActions.FORMAT_TEXT);
+ if (displaySelectionMode) actions.add(ContextualActions.SELECTION_MODE);
+ if (displaySelectionType) actions.add(ContextualActions.SELECTION_TYPE);
return actions;
}
diff --git a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/CustomTextSpan.java b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/CustomTextSpan.java
similarity index 100%
rename from UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/CustomTextSpan.java
rename to samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/CustomTextSpan.java
diff --git a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorBinding.java b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorBinding.java
similarity index 96%
rename from UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorBinding.java
rename to samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorBinding.java
index 9be2092..90c6e8b 100644
--- a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorBinding.java
+++ b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorBinding.java
@@ -48,7 +48,7 @@ private void bindEditor(@NonNull EditorView editorView, @Nullable Editor editor)
}
@NonNull
- public final EditorData openEditor(@Nullable EditorView editorView)
+ public EditorData openEditor(@Nullable EditorView editorView)
{
Editor editor = null;
Renderer renderer = null;
diff --git a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorData.java b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorData.java
similarity index 86%
rename from UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorData.java
rename to samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorData.java
index 283ce0f..ec781bf 100644
--- a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorData.java
+++ b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorData.java
@@ -2,11 +2,11 @@
package com.myscript.iink.uireferenceimplementation;
-import androidx.annotation.Nullable;
-
import com.myscript.iink.Editor;
import com.myscript.iink.Renderer;
+import androidx.annotation.Nullable;
+
public final class EditorData
{
@Nullable
@@ -17,19 +17,19 @@ public final class EditorData
private final InputController inputController;
@Nullable
- public final Editor getEditor()
+ public Editor getEditor()
{
return this.editor;
}
@Nullable
- public final Renderer getRenderer()
+ public Renderer getRenderer()
{
return this.renderer;
}
@Nullable
- public final InputController getInputController()
+ public InputController getInputController()
{
return this.inputController;
}
diff --git a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorView.java b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorView.java
similarity index 69%
rename from UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorView.java
rename to samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorView.java
index c8987bb..b557b04 100644
--- a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorView.java
+++ b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorView.java
@@ -10,19 +10,21 @@
import android.view.View;
import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
import com.myscript.iink.Editor;
import com.myscript.iink.IRenderTarget;
import com.myscript.iink.Renderer;
import com.myscript.iink.graphics.ICanvas;
import com.myscript.iink.graphics.Point;
+import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
public class EditorView extends FrameLayout implements IRenderTarget, InputController.ViewListener
{
private int viewWidth;
@@ -37,22 +39,20 @@ public class EditorView extends FrameLayout implements IRenderTarget, InputContr
@NonNull
private final OfflineSurfaceManager offlineSurfaceManager;
@Nullable
- private IRenderView renderView;
- @Nullable
- private IRenderView[] layerViews;
+ private LayerView layerView;
private Map typefaceMap = new HashMap<>();
+ @NonNull
+ private List extraBrushConfigs = Collections.emptyList();
public EditorView(Context context)
{
- super(context);
- offlineSurfaceManager = new OfflineSurfaceManager();
+ this(context, null, 0);
}
public EditorView(Context context, @Nullable AttributeSet attrs)
{
- super(context, attrs);
- offlineSurfaceManager = new OfflineSurfaceManager();
+ this(context, attrs, 0);
}
public EditorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)
@@ -70,46 +70,22 @@ protected void onFinishInflate()
for (int i = 0, count = getChildCount(); i < count; ++i)
{
View view = getChildAt(i);
- if (view instanceof IRenderView)
+ if (view instanceof LayerView)
{
- IRenderView renderView = (IRenderView) view;
- if (renderView.isSingleLayerView())
- {
- if (layerViews == null)
- layerViews = new IRenderView[2];
- if (renderView.getType() == LayerType.MODEL)
- layerViews[0] = renderView;
- else if (renderView.getType() == LayerType.CAPTURE)
- {
- layerViews[1] = renderView;
- }
- else
- {
- throw new RuntimeException("Unknown layer view type");
- }
- }
- else
- {
- this.renderView = renderView;
- }
+ layerView = (LayerView) view;
- renderView.setRenderTarget(this);
+ layerView.setRenderTarget(this);
if (editor != null)
{
- renderView.setEditor(editor);
+ layerView.setEditor(editor);
}
if (imageLoader != null)
{
- renderView.setImageLoader(imageLoader);
+ layerView.setImageLoader(imageLoader);
}
- renderView.setCustomTypefaces(typefaceMap);
-
- if (view instanceof LayerView)
- {
- LayerView layerView = (LayerView) view;
- layerView.setOfflineSurfaceManager(offlineSurfaceManager);
- }
+ layerView.setCustomTypefaces(typefaceMap);
+ layerView.setOfflineSurfaceManager(offlineSurfaceManager);
}
}
}
@@ -125,19 +101,9 @@ public void setEditor(@Nullable Editor editor)
if (editor != null)
{
renderer = editor.getRenderer();
- if (renderView != null)
- {
- renderView.setEditor(editor);
- }
- else if (layerViews != null)
+ if (layerView != null)
{
- for (IRenderView layerView : layerViews)
- {
- if (layerView != null)
- {
- layerView.setEditor(editor);
- }
- }
+ layerView.setEditor(editor);
}
if (viewWidth > 0 && viewHeight > 0)
{
@@ -163,25 +129,48 @@ public Renderer getRenderer()
return renderer;
}
- public void setImageLoader(ImageLoader imageLoader)
+ public void setExtraBrushConfigs(@NonNull List extraBrushConfigs)
{
- this.imageLoader = imageLoader;
-
- // transfer image loader to render views
- if (renderView != null)
+ if (editor != null)
{
- renderView.setImageLoader(imageLoader);
+ throw new IllegalStateException("Please set the extra brush configs of the EditorView before binding the editor (through EditorView.setEngine() or EditorView.setEditor())");
}
- else if (layerViews != null)
+
+ this.extraBrushConfigs = extraBrushConfigs;
+ for (int i = 0, count = getChildCount(); i < count; ++i)
{
- for (IRenderView layerView : layerViews)
+ View view = getChildAt(i);
+ if (view instanceof LayerView)
{
- if (layerView != null)
- layerView.setImageLoader(imageLoader);
+ LayerView layerView = (LayerView) view;
+ layerView.setExtraBrushConfigs(extraBrushConfigs);
}
}
}
+ @NonNull
+ public List getExtraBrushConfigs()
+ {
+ return extraBrushConfigs;
+ }
+
+ public void setImageLoader(ImageLoader imageLoader)
+ {
+ this.imageLoader = imageLoader;
+
+ // transfer image loader to render views
+ if (layerView != null)
+ {
+ layerView.setImageLoader(imageLoader);
+ }
+ }
+
+ @Nullable
+ public ImageLoader getImageLoader()
+ {
+ return imageLoader;
+ }
+
public void setTypefaces(@NonNull Map typefaceMap)
{
if (editor != null)
@@ -193,10 +182,10 @@ public void setTypefaces(@NonNull Map typefaceMap)
for (int i = 0, count = getChildCount(); i < count; ++i)
{
View view = getChildAt(i);
- if (view instanceof IRenderView)
+ if (view instanceof LayerView)
{
- IRenderView renderView = (IRenderView) view;
- renderView.setCustomTypefaces(typefaceMap);
+ LayerView layerView = (LayerView) view;
+ layerView.setCustomTypefaces(typefaceMap);
}
}
}
@@ -206,12 +195,6 @@ public Map getTypefaces()
return typefaceMap;
}
- @Nullable
- public ImageLoader getImageLoader()
- {
- return imageLoader;
- }
-
@Override
protected void onSizeChanged(int newWidth, int newHeight, int oldWidth, int oldHeight)
{
@@ -239,19 +222,9 @@ public final void invalidate(@NonNull Renderer renderer, int x, int y, int width
if (width <= 0 || height <= 0)
return;
- if (renderView != null)
+ if (layerView != null)
{
- renderView.update(renderer, x, y, width, height, layers);
- }
- else if (layerViews != null)
- {
- for (LayerType type : layers)
- {
- int layerID = (type == LayerType.MODEL) ? 0 : 1;
- IRenderView layerView = layerViews[layerID];
- if (layerView != null)
- layerView.update(renderer, x, y, width, height, layers);
- }
+ layerView.update(renderer, x, y, width, height);
}
}
@@ -317,7 +290,7 @@ public ICanvas createOffscreenRenderCanvas(int offscreenID)
if (offlineBitmap == null)
return null;
android.graphics.Canvas canvas = new android.graphics.Canvas(offlineBitmap);
- return new Canvas(canvas, typefaceMap, imageLoader, offlineSurfaceManager, renderer.getDpiX(), renderer.getDpiY());
+ return new Canvas(canvas, extraBrushConfigs, typefaceMap, imageLoader, offlineSurfaceManager, renderer.getDpiX(), renderer.getDpiY());
}
@Override
@@ -331,10 +304,6 @@ public void showScrollbars()
editor.clampViewOffset(bottomRightPx);
float pageHeightPx = bottomRightPx.y - topLeftPx.y + viewHeightPx;
float pageWidthPx = bottomRightPx.x - topLeftPx.x + viewWidthPx;
- for (IRenderView layerView : layerViews)
- {
- if (layerView instanceof LayerView && layerView.getType() == LayerType.MODEL)
- ((LayerView) layerView).setScrollbar(renderer, viewWidthPx, (int) pageWidthPx, (int) topLeftPx.x, viewHeightPx, (int) pageHeightPx, (int) topLeftPx.y);
- }
+ layerView.setScrollbar(renderer, viewWidthPx, (int) pageWidthPx, (int) topLeftPx.x, viewHeightPx, (int) pageHeightPx, (int) topLeftPx.y);
}
}
diff --git a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/FontMetricsProvider.java b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/FontMetricsProvider.java
similarity index 96%
rename from UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/FontMetricsProvider.java
rename to samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/FontMetricsProvider.java
index 1ce918e..1b13888 100644
--- a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/FontMetricsProvider.java
+++ b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/FontMetricsProvider.java
@@ -19,6 +19,7 @@
import android.text.style.MetricAffectingSpan;
import android.text.style.TextAppearanceSpan;
import android.util.DisplayMetrics;
+import android.util.TypedValue;
import androidx.annotation.NonNull;
import androidx.collection.LruCache;
@@ -88,11 +89,6 @@ public FontMetricsProvider(DisplayMetrics displayMetrics, Map
this.typefaceMap = typefaceMap;
}
- private float x_mm2px(float mm)
- {
- return (mm / 25.4f) * displayMetrics.xdpi;
- }
-
private float y_mm2px(float mm)
{
return (mm / 25.4f) * displayMetrics.ydpi;
@@ -127,7 +123,7 @@ public Rectangle[] getCharacterBoundingBoxes(@NonNull Text text, TextSpan[] span
@Override
public float getFontSizePx(Style style)
{
- return style.getFontSize() * displayMetrics.scaledDensity;
+ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, style.getFontSize(), displayMetrics);
}
@Override
@@ -167,7 +163,7 @@ private StaticLayout getLayout(@NonNull SpannableString string)
}
@Override
- public GlyphMetrics[] getGlyphMetrics(Text text, TextSpan[] spans)
+ public synchronized GlyphMetrics[] getGlyphMetrics(Text text, TextSpan[] spans)
{
final String label = text.getLabel();
@@ -188,6 +184,7 @@ public GlyphMetrics[] getGlyphMetrics(Text text, TextSpan[] spans)
int typefaceStyle = FontUtils.getTypefaceStyle(style);
int fontSize = Math.round(y_mm2px(style.getFontSize()));
+ fontSize = Math.max(fontSize, 1);
int start = text.getGlyphBeginAt(spans[i].beginPosition);
int end = text.getGlyphEndAt(spans[i].endPosition - 1);
diff --git a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/FontUtils.java b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/FontUtils.java
similarity index 100%
rename from UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/FontUtils.java
rename to samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/FontUtils.java
diff --git a/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/FrameTimeEstimator.java b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/FrameTimeEstimator.java
new file mode 100644
index 0000000..8c61275
--- /dev/null
+++ b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/FrameTimeEstimator.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Adapted from:
+// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:input/input-motionprediction/
+
+package com.myscript.iink.uireferenceimplementation;
+
+import android.content.Context;
+import android.os.Build;
+import android.view.Display;
+import android.view.WindowManager;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+/**
+ * Get screen fastest refresh rate (in ms)
+ */
+@SuppressWarnings("deprecation")
+public class FrameTimeEstimator {
+ private static final float LEGACY_FRAME_TIME_MS = 16f;
+ private static final float MS_IN_A_SECOND = 1000f;
+
+ static public float getFrameTime(@NonNull Context context)
+ {
+ return getFastestFrameTimeMs(context);
+ }
+
+ private static Display getDisplayForContext(Context context)
+ {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
+ {
+ return Api30Impl.getDisplayForContext(context);
+ }
+ return ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
+ }
+
+ private static float getFastestFrameTimeMs(Context context)
+ {
+ Display defaultDisplay = getDisplayForContext(context);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
+ {
+ return Api23Impl.getFastestFrameTimeMs(defaultDisplay);
+ }
+ else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
+ {
+ return Api21Impl.getFastestFrameTimeMs(defaultDisplay);
+ }
+ else
+ {
+ return LEGACY_FRAME_TIME_MS;
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ static class Api21Impl
+ {
+ private Api21Impl()
+ {
+ // Not instantiable
+ }
+
+ @DoNotInline
+ static float getFastestFrameTimeMs(Display display)
+ {
+ float[] refreshRates = display.getSupportedRefreshRates();
+ float largestRefreshRate = refreshRates[0];
+
+ for (int c = 1; c < refreshRates.length; c++)
+ {
+ if (refreshRates[c] > largestRefreshRate)
+ largestRefreshRate = refreshRates[c];
+ }
+
+ return MS_IN_A_SECOND / largestRefreshRate;
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ static class Api23Impl
+ {
+ private Api23Impl()
+ {
+ // Not instantiable
+ }
+
+ @DoNotInline
+ static float getFastestFrameTimeMs(Display display)
+ {
+ Display.Mode[] displayModes = display.getSupportedModes();
+ float largestRefreshRate = displayModes[0].getRefreshRate();
+
+ for (int c = 1; c < displayModes.length; c++)
+ {
+ float currentRefreshRate = displayModes[c].getRefreshRate();
+ if (currentRefreshRate > largestRefreshRate)
+ largestRefreshRate = currentRefreshRate;
+ }
+
+ return MS_IN_A_SECOND / largestRefreshRate;
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.R)
+ static class Api30Impl
+ {
+ private Api30Impl()
+ {
+ // Not instantiable
+ }
+
+ @DoNotInline
+ static Display getDisplayForContext(Context context)
+ {
+ return context.getDisplay();
+ }
+ }
+}
diff --git a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/IInputControllerListener.java b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/IInputControllerListener.java
similarity index 100%
rename from UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/IInputControllerListener.java
rename to samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/IInputControllerListener.java
diff --git a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ISizeListener.java b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ISizeListener.java
similarity index 100%
rename from UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ISizeListener.java
rename to samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ISizeListener.java
diff --git a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ImageLoader.java b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ImageLoader.java
similarity index 77%
rename from UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ImageLoader.java
rename to samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ImageLoader.java
index d82e044..0c15530 100644
--- a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ImageLoader.java
+++ b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ImageLoader.java
@@ -21,13 +21,14 @@ public class ImageLoader
@NonNull
private final Editor editor;
LruCache cache;
+ static final float CACHE_MAX_MEMORY_RATIO = 1.f / 8; // in ]0, 1[
public ImageLoader(@NonNull Editor editor)
{
this.editor = editor;
- // Use a part of the maximum available memory to define the cache's size
- int cacheSize = (int) (Runtime.getRuntime().maxMemory() / 8);
+ // Use a part of the maximum available memory to define the cache's size (in Bytes)
+ int cacheSize = (int) (Runtime.getRuntime().maxMemory() * CACHE_MAX_MEMORY_RATIO);
this.cache = new LruCache(cacheSize)
{
@@ -59,12 +60,22 @@ public synchronized Bitmap getImage(final String url, final String mimeType, fin
{
Bitmap image = cache.get(url);
if (image != null)
- return image;
+ return image; // found
Pair newImage = renderObject(url, mimeType, dstWidth, dstHeight);
if (newImage.second) // Not dummy
+ {
+ int imageSize = newImage.first.getByteCount();
+ if (imageSize > cache.maxSize())
+ {
+ Log.w("ImageLoader", "Image too big for cache: resizing cache ("
+ + imageSize / (1024.f * 1024.f) + "MB > " + cache.maxSize() / (1024.f * 1024.f) + "MB)");
+ cache.resize(imageSize);
+ }
+
cache.put(url, newImage.first);
+ }
return newImage.first;
}
@@ -87,12 +98,18 @@ private Pair renderObject(String url, String mimeType, int dstW
if (scaledImage != null)
return Pair.create(scaledImage, true);
+ else
+ Log.e("ImageLoader", "Unable to scale image: using placeholder image");
}
else
{
return Pair.create(image, true);
}
}
+ else
+ {
+ Log.e("ImageLoader", "Unable to decode file: using placeholder image");
+ }
}
catch (Exception e)
{
@@ -102,7 +119,7 @@ private Pair renderObject(String url, String mimeType, int dstW
catch (OutOfMemoryError e)
{
// Error: use fallback bitmap
- Log.w("ImageLoader", "Out of memory: unable to load image, using placeholder instead", e);
+ Log.w("ImageLoader", "Out of memory: unable to load image: using placeholder instead", e);
}
}
@@ -110,6 +127,9 @@ private Pair renderObject(String url, String mimeType, int dstW
Bitmap image = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565);
if (image != null)
image.eraseColor(Color.WHITE);
+ else
+ Log.e("ImageLoader", "Unable to render image nor placeholder");
+
return Pair.create(image, false);
}
}
diff --git a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ImagePainter.java b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ImagePainter.java
similarity index 82%
rename from UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ImagePainter.java
rename to samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ImagePainter.java
index 70b8b08..54be6df 100644
--- a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ImagePainter.java
+++ b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ImagePainter.java
@@ -5,8 +5,6 @@
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Typeface;
-import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
import com.myscript.iink.IImagePainter;
import com.myscript.iink.graphics.ICanvas;
@@ -14,19 +12,35 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
+import java.util.Collections;
+import java.util.List;
import java.util.Map;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+
public class ImagePainter implements IImagePainter
{
private ImageLoader imageLoader = null;
private Map typefaceMap = null;
+ protected android.graphics.Canvas canvas = null;
private Bitmap bitmap = null;
- private android.graphics.Canvas canvas = null;
- private float dpi = 96;
-
+ @NonNull
+ private final List extraBrushConfigs;
+ private float dpi = 96.f;
@ColorInt
private int backgroundColor = Color.WHITE;
+ public ImagePainter()
+ {
+ this(Collections.emptyList());
+ }
+
+ public ImagePainter(@NonNull List extraBrushConfigs)
+ {
+ this.extraBrushConfigs = extraBrushConfigs;
+ }
+
public void setImageLoader(ImageLoader imageLoader)
{
this.imageLoader = imageLoader;
@@ -45,7 +59,7 @@ public void setBackgroundColor(@ColorInt int backgroundColor)
@Override
public ICanvas createCanvas()
{
- return new Canvas(canvas, typefaceMap, imageLoader, null, dpi, dpi);
+ return new Canvas(canvas, extraBrushConfigs, typefaceMap, imageLoader, dpi, dpi);
}
@Override
diff --git a/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/InputController.java b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/InputController.java
new file mode 100644
index 0000000..d17673c
--- /dev/null
+++ b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/InputController.java
@@ -0,0 +1,456 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.uireferenceimplementation;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.View;
+
+import com.myscript.iink.ContentBlock;
+import com.myscript.iink.Editor;
+import com.myscript.iink.IRenderTarget;
+import com.myscript.iink.PointerEvent;
+import com.myscript.iink.PointerEventType;
+import com.myscript.iink.PointerTool;
+import com.myscript.iink.PointerType;
+import com.myscript.iink.Renderer;
+import com.myscript.iink.ToolController;
+import com.myscript.iink.graphics.Point;
+
+import java.util.EnumSet;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+public class InputController implements View.OnTouchListener, GestureDetector.OnGestureListener, ScaleGestureDetector.OnScaleGestureListener
+{
+
+ public interface ViewListener
+ {
+ void showScrollbars();
+ }
+
+ public static final int INPUT_MODE_NONE = -1;
+ public static final int INPUT_MODE_FORCE_PEN = 0;
+ public static final int INPUT_MODE_FORCE_TOUCH = 1;
+ public static final int INPUT_MODE_AUTO = 2;
+ public static final int INPUT_MODE_ERASER = 3;
+
+ private static final float SCALING_SENSIBILITY = 1.5f;
+ private static final float SCALING_THRESHOLD = 0.02f;
+
+ private final EditorView editorView;
+ private final Editor editor;
+ private int _inputMode;
+ private final GestureDetector gestureDetector;
+ private final ScaleGestureDetector scaleGestureDetector;
+ private IInputControllerListener _listener;
+ private final long eventTimeOffset;
+ @VisibleForTesting
+ public PointerType iinkPointerType;
+ private ViewListener _viewListener;
+
+ private boolean isScalingEnabled = false;
+ private float getPreviousScalingSpan;
+ private float previousScalingFocusX;
+ private float previousScalingFocusY;
+ private boolean isMultiFingerTouch = false;
+ private int previousPointerId;
+
+ private boolean isScrollingEnabled = true;
+
+ public InputController(Context context, EditorView editorView, Editor editor)
+ {
+ this.editorView = editorView;
+ this.editor = editor;
+ _listener = null;
+ _inputMode = INPUT_MODE_AUTO;
+ scaleGestureDetector = new ScaleGestureDetector(context, this);
+ gestureDetector = new GestureDetector(context, this);
+
+ long rel_t = SystemClock.uptimeMillis();
+ long abs_t = System.currentTimeMillis();
+ eventTimeOffset = abs_t - rel_t;
+ }
+
+ public final synchronized void setInputMode(int inputMode)
+ {
+ this._inputMode = inputMode;
+ }
+
+ public final synchronized int getInputMode()
+ {
+ return _inputMode;
+ }
+
+ public final synchronized void setViewListener(ViewListener listener)
+ {
+ this._viewListener = listener;
+ }
+
+ public final synchronized void setListener(IInputControllerListener listener)
+ {
+ this._listener = listener;
+ }
+
+ public final synchronized void setScalingEnabled(boolean enabled)
+ {
+ isScalingEnabled = enabled;
+ }
+
+ public final synchronized void setScrollingEnabled(boolean enabled) {
+ isScrollingEnabled = enabled;
+ }
+
+ public final synchronized IInputControllerListener getListener()
+ {
+ return _listener;
+ }
+
+ public final synchronized int getPreviousPointerId()
+ {
+ return previousPointerId;
+ }
+
+ private boolean handleOnTouchForPointer(MotionEvent event, int actionMask, int pointerIndex)
+ {
+ final int pointerId = event.getPointerId(pointerIndex);
+ final int pointerType = event.getToolType(pointerIndex);
+ final int historySize = event.getHistorySize();
+ final boolean useTiltInfo = pointerType == MotionEvent.TOOL_TYPE_STYLUS;
+
+ int inputMode = getInputMode();
+ if (inputMode == INPUT_MODE_FORCE_PEN)
+ {
+ iinkPointerType = PointerType.PEN;
+ }
+ else if (inputMode == INPUT_MODE_FORCE_TOUCH)
+ {
+ iinkPointerType = PointerType.TOUCH;
+ }
+ else
+ {
+ switch (pointerType)
+ {
+ case MotionEvent.TOOL_TYPE_STYLUS:
+ if (inputMode == INPUT_MODE_ERASER)
+ iinkPointerType = PointerType.ERASER;
+ else
+ iinkPointerType = PointerType.PEN;
+ break;
+ case MotionEvent.TOOL_TYPE_FINGER:
+ case MotionEvent.TOOL_TYPE_MOUSE:
+ iinkPointerType = PointerType.TOUCH;
+ break;
+ default:
+ // unsupported event type
+ return false;
+ }
+ }
+
+ if (isScalingEnabled)
+ scaleGestureDetector.onTouchEvent(event);
+
+ if (iinkPointerType == PointerType.TOUCH)
+ gestureDetector.onTouchEvent(event);
+
+ switch (actionMask)
+ {
+ // ACTION_POINTER_DOWN is "A non-primary pointer has gone down", only called when a pointer is already on the touchscreen.
+ case MotionEvent.ACTION_POINTER_DOWN:
+ {
+ isMultiFingerTouch = true;
+ if (previousPointerId != -1)
+ {
+ editor.pointerCancel(previousPointerId);
+ previousPointerId = -1;
+ }
+ return true;
+ }
+ case MotionEvent.ACTION_DOWN:
+ {
+ previousPointerId = pointerId;
+ isMultiFingerTouch = false;
+ // Request unbuffered events for tools that require low capture latency
+ ToolController toolController = editor.getToolController();
+ PointerTool tool = toolController.getToolForType(iinkPointerType);
+ if (tool == PointerTool.PEN || tool == PointerTool.HIGHLIGHTER)
+ editorView.requestUnbufferedDispatch(event);
+
+ try
+ {
+ if (useTiltInfo)
+ editor.pointerDown(event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(),
+ event.getPressure(), event.getAxisValue(MotionEvent.AXIS_TILT, pointerIndex), event.getAxisValue(MotionEvent.AXIS_ORIENTATION, pointerIndex), iinkPointerType, pointerId);
+ else
+ editor.pointerDown(event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), event.getPressure(), iinkPointerType, pointerId);
+ }
+ catch (UnsupportedOperationException e) {
+ // Special case: pointerDown already called, discard previous and retry
+ editor.pointerCancel(pointerId);
+ if (useTiltInfo)
+ editor.pointerDown(event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(),
+ event.getPressure(), event.getAxisValue(MotionEvent.AXIS_TILT, pointerIndex), event.getAxisValue(MotionEvent.AXIS_ORIENTATION, pointerIndex), iinkPointerType, pointerId);
+ else
+ editor.pointerDown(event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), event.getPressure(), iinkPointerType, pointerId);
+ }
+ return true;
+ }
+ case MotionEvent.ACTION_MOVE:
+ {
+ if (isMultiFingerTouch)
+ return true;
+
+ if (historySize > 0)
+ {
+ PointerEvent[] pointerEvents = new PointerEvent[historySize + 1];
+ if (useTiltInfo)
+ {
+ for (int i = 0; i < historySize; ++i)
+ {
+ pointerEvents[i] = new PointerEvent(PointerEventType.MOVE, event.getHistoricalX(pointerIndex, i), event.getHistoricalY(pointerIndex, i), eventTimeOffset + event.getHistoricalEventTime(i),
+ event.getHistoricalPressure(pointerIndex, i), event.getHistoricalAxisValue(MotionEvent.AXIS_TILT, pointerIndex, i), event.getHistoricalAxisValue(MotionEvent.AXIS_ORIENTATION, pointerIndex, i), iinkPointerType, pointerId);
+ }
+ pointerEvents[historySize] = new PointerEvent(PointerEventType.MOVE, event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(),
+ event.getPressure(), event.getAxisValue(MotionEvent.AXIS_TILT, pointerIndex), event.getAxisValue(MotionEvent.AXIS_ORIENTATION, pointerIndex), iinkPointerType, pointerId);
+ }
+ else
+ {
+ for (int i = 0; i < historySize; ++i)
+ {
+ pointerEvents[i] = new PointerEvent(PointerEventType.MOVE, event.getHistoricalX(pointerIndex, i), event.getHistoricalY(pointerIndex, i), eventTimeOffset + event.getHistoricalEventTime(i),
+ event.getHistoricalPressure(pointerIndex, i), iinkPointerType, pointerId);
+ }
+ pointerEvents[historySize] = new PointerEvent(PointerEventType.MOVE, event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), event.getPressure(), iinkPointerType, pointerId);
+ }
+ editor.pointerEvents(pointerEvents, true);
+ }
+ else // no history
+ {
+ if (useTiltInfo)
+ editor.pointerMove(event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(),
+ event.getPressure(), event.getAxisValue(MotionEvent.AXIS_TILT, pointerIndex), event.getAxisValue(MotionEvent.AXIS_ORIENTATION, pointerIndex), iinkPointerType, pointerId);
+ else
+ editor.pointerMove(event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), event.getPressure(), iinkPointerType, pointerId);
+ }
+ return true;
+ }
+ // ACTION_POINTER_UP is "A non-primary pointer has gone up", at least one finger is still on the touchscreen.
+ case MotionEvent.ACTION_POINTER_UP:
+ {
+ return true;
+ }
+ case MotionEvent.ACTION_UP:
+ {
+ if (isMultiFingerTouch)
+ {
+ isMultiFingerTouch = false;
+ return true;
+ }
+ if (historySize > 0)
+ {
+ PointerEvent[] pointerEvents = new PointerEvent[historySize];
+ if (useTiltInfo)
+ {
+ for (int i = 0; i < historySize; ++i)
+ {
+ pointerEvents[i] = new PointerEvent(PointerEventType.MOVE, event.getHistoricalX(pointerIndex, i), event.getHistoricalY(pointerIndex, i), eventTimeOffset + event.getHistoricalEventTime(i),
+ event.getHistoricalPressure(pointerIndex, i), event.getHistoricalAxisValue(MotionEvent.AXIS_TILT, pointerIndex, i), event.getHistoricalAxisValue(MotionEvent.AXIS_ORIENTATION, pointerIndex, i), iinkPointerType, pointerId);
+ }
+ }
+ else
+ {
+ for (int i = 0; i < historySize; ++i)
+ {
+ pointerEvents[i] = new PointerEvent(PointerEventType.MOVE, event.getHistoricalX(pointerIndex, i), event.getHistoricalY(pointerIndex, i), eventTimeOffset + event.getHistoricalEventTime(i),
+ event.getHistoricalPressure(pointerIndex, i), iinkPointerType, pointerId);
+ }
+ }
+ editor.pointerEvents(pointerEvents, true);
+ }
+ if (useTiltInfo)
+ editor.pointerUp(event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(),
+ event.getPressure(), event.getAxisValue(MotionEvent.AXIS_TILT, pointerIndex), event.getAxisValue(MotionEvent.AXIS_ORIENTATION, pointerIndex), iinkPointerType, pointerId);
+ else
+ editor.pointerUp(event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), event.getPressure(), iinkPointerType, pointerId);
+
+ return true;
+ }
+ case MotionEvent.ACTION_CANCEL:
+ {
+ editor.pointerCancel(pointerId);
+ return true;
+ }
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event)
+ {
+ if (editor == null)
+ {
+ return false;
+ }
+
+ final int action = event.getAction();
+ final int actionMask = action & MotionEvent.ACTION_MASK;
+
+ try
+ {
+ if (actionMask == MotionEvent.ACTION_POINTER_DOWN || actionMask == MotionEvent.ACTION_POINTER_UP)
+ {
+ final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+ return handleOnTouchForPointer(event, actionMask, pointerIndex);
+ }
+ else
+ {
+ boolean consumed = false;
+ final int pointerCount = event.getPointerCount();
+ for (int pointerIndex = 0; pointerIndex < pointerCount; pointerIndex++)
+ {
+ try
+ {
+ consumed = consumed || handleOnTouchForPointer(event, actionMask, pointerIndex);
+ }
+ catch(Exception e) {
+ // ignore spurious invalid touch events that may occurs when spamming undo/redo button
+ }
+ }
+ return consumed;
+ }
+ }
+ catch(UnsupportedOperationException e) {
+ // such an error may be generated by monkey tests
+ Log.e("InputController", "bad touch sequence", e);
+ return false;
+ }
+ }
+
+ @Override
+ public boolean onDown(MotionEvent event)
+ {
+ return false;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent event)
+ {
+ // no-op
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent event)
+ {
+ return false;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent event)
+ {
+ IInputControllerListener listener = getListener();
+ if (listener != null)
+ {
+ final float x = event.getX();
+ final float y = event.getY();
+ // Only handle block ID and not `ContentBlock` to simplify native `AutoCloseable` object lifecycle.
+ // Otherwise, depending on which object owns such `ContentBlock`, the reasoning about closing it
+ // would be more complicated.
+ // Providing block ID delegates to listeners the ownership of the retrieved block (if any),
+ // typically calling `Editor.getBlockById()`.
+ try (@Nullable ContentBlock block = editor.hitBlock(x, y))
+ {
+ String blockId = block != null ? block.getId() : null;
+ listener.onLongPress(x, y, blockId);
+ }
+ }
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)
+ {
+ if (editor.isScrollAllowed() && isScrollingEnabled)
+ {
+ Point oldOffset = editor.getRenderer().getViewOffset();
+ Point newOffset = new Point(oldOffset.x + distanceX, oldOffset.y + distanceY);
+ editor.getRenderer().setViewOffset(Math.round(newOffset.x), Math.round(newOffset.y));
+ editorView.invalidate(editor.getRenderer(), EnumSet.allOf(IRenderTarget.LayerType.class));
+ if(_viewListener != null)
+ {
+ _viewListener.showScrollbars();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
+ {
+ return false;
+ }
+
+ @Override
+ public boolean onScale(ScaleGestureDetector scaleGestureDetector)
+ {
+ Renderer renderer = editorView.getRenderer();
+
+ // Store the current focus of the scaleGestureDetector
+ float currentScalingFocusX = scaleGestureDetector.getFocusX();
+ float currentScalingFocusY = scaleGestureDetector.getFocusY();
+ float currentSpan = scaleGestureDetector.getCurrentSpan();
+
+ // Measure the delta of the currentFocus to the previous
+ float distanceX = previousScalingFocusX - currentScalingFocusX;
+ float distanceY = previousScalingFocusY - currentScalingFocusY;
+
+ previousScalingFocusX = currentScalingFocusX;
+ previousScalingFocusY = currentScalingFocusY;
+
+ Point oldOffset = renderer.getViewOffset();
+ Point newOffset = new Point(oldOffset.x + distanceX, oldOffset.y + distanceY);
+
+ // Apply the translation of the scaling focus to the render
+ renderer.setViewOffset(Math.round(newOffset.x), Math.round(newOffset.y));
+
+ float deltaSpan = getPreviousScalingSpan / currentSpan;
+ // Apply a ratio in order to avoid the scaling to move too fast
+ deltaSpan = 1.0f + ((1.0f - deltaSpan) / SCALING_SENSIBILITY);
+
+ // Do not move if the scaling is too small
+ if (deltaSpan > (1 + SCALING_THRESHOLD) || deltaSpan < (1 - SCALING_THRESHOLD))
+ {
+ renderer.zoomAt(new Point(currentScalingFocusX, currentScalingFocusY), deltaSpan);
+ }
+
+ // Store the span for next time
+ getPreviousScalingSpan = currentSpan;
+ editorView.invalidate(renderer, EnumSet.allOf(IRenderTarget.LayerType.class));
+
+ if(_viewListener != null)
+ {
+ _viewListener.showScrollbars();
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector)
+ {
+ getPreviousScalingSpan = scaleGestureDetector.getCurrentSpan();
+ previousScalingFocusX = scaleGestureDetector.getFocusX();
+ previousScalingFocusY = scaleGestureDetector.getFocusY();
+ return true;
+ }
+
+ @Override
+ public void onScaleEnd(ScaleGestureDetector scaleGestureDetector)
+ {
+ // no-op
+ }
+}
diff --git a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/JiixDefinitions.java b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/JiixDefinitions.java
similarity index 100%
rename from UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/JiixDefinitions.java
rename to samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/JiixDefinitions.java
diff --git a/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/LayerView.java b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/LayerView.java
new file mode 100644
index 0000000..de30b2e
--- /dev/null
+++ b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/LayerView.java
@@ -0,0 +1,303 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.uireferenceimplementation;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.View;
+
+import com.myscript.iink.Editor;
+import com.myscript.iink.IRenderTarget;
+import com.myscript.iink.IRenderTarget.LayerType;
+import com.myscript.iink.Renderer;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class LayerView extends View
+{
+ private final static int MODEL = 0;
+ private final static int CAPTURE = 1;
+ private ImageLoader imageLoader;
+
+ @Nullable
+ private Map typefaceMap;
+
+ @Nullable
+ private Renderer lastRenderer = null;
+
+ @Nullable
+ private OfflineSurfaceManager offlineSurfaceManager = null;
+ @Nullable
+ private Renderer renderer = null;
+ @NonNull
+ private Rect updateArea = new Rect(0, 0, 0, 0);
+ @NonNull
+ private Rect localUpdateArea = new Rect(0, 0, 0, 0);
+ @Nullable
+ private Bitmap bitmap = null; // for API < 28
+ @Nullable
+ private android.graphics.Canvas sysCanvas = null; // for API < 28
+ @Nullable
+ private Canvas iinkCanvas = null;
+ @NonNull
+ private List extraBrushConfigs = Collections.emptyList();
+ private int pageWidth = 0;
+ private int pageHeight = 0;
+ private int viewWidth = 0;
+ private int viewHeight = 0;
+ private int canvasWidth = 0;
+ private int canvasHeight = 0;
+ private int xMin = 0;
+ private int yMin = 0;
+
+ public LayerView(Context context)
+ {
+ this(context, null, 0);
+ }
+
+ public LayerView(Context context, @Nullable AttributeSet attrs)
+ {
+ this(context, attrs, 0);
+ }
+
+ public LayerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)
+ {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public void setRenderTarget(IRenderTarget renderTarget)
+ {
+ // do not need the renderTarget
+ }
+
+ public void setExtraBrushConfigs(@NonNull List extraBrushConfigs)
+ {
+ this.extraBrushConfigs = extraBrushConfigs;
+ }
+
+ public void setOfflineSurfaceManager(@Nullable OfflineSurfaceManager offlineSurfaceManager)
+ {
+ this.offlineSurfaceManager = offlineSurfaceManager;
+ }
+
+ public void setEditor(Editor editor)
+ {
+ // do not need the editor
+ }
+
+ public void setImageLoader(ImageLoader imageLoader)
+ {
+ this.imageLoader = imageLoader;
+ }
+
+ public void setCustomTypefaces(Map typefaceMap)
+ {
+ this.typefaceMap = typefaceMap;
+ }
+
+ @Override
+ protected final void onDraw(android.graphics.Canvas canvas)
+ {
+ super.onDraw(canvas);
+
+ // Draw directly in hardware-accelerated Canvas if scaling is supported (since API 28)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
+ {
+ Renderer renderer;
+ synchronized (this)
+ {
+ localUpdateArea.set(0, 0, canvasWidth, canvasHeight);
+ renderer = lastRenderer;
+ }
+
+ iinkCanvas.setCanvas(canvas);
+ prepare(canvas, localUpdateArea);
+
+ try
+ {
+ renderer.drawModel(localUpdateArea.left, localUpdateArea.top, localUpdateArea.width(), localUpdateArea.height(), iinkCanvas);
+ renderer.drawCaptureStrokes(localUpdateArea.left, localUpdateArea.top, localUpdateArea.width(), localUpdateArea.height(), iinkCanvas);
+ }
+ finally
+ {
+ restore(canvas);
+ }
+ }
+ else // Draw in intermediate bitmap
+ {
+ Renderer renderer;
+ synchronized (this)
+ {
+ localUpdateArea.set(this.updateArea);
+ this.updateArea.setEmpty();
+
+ renderer = lastRenderer;
+ lastRenderer = null;
+ }
+
+ if (!localUpdateArea.isEmpty())
+ {
+ prepare(sysCanvas, localUpdateArea);
+ try
+ {
+ renderer.drawModel(localUpdateArea.left, localUpdateArea.top, localUpdateArea.width(), localUpdateArea.height(), iinkCanvas);
+ renderer.drawCaptureStrokes(localUpdateArea.left, localUpdateArea.top, localUpdateArea.width(), localUpdateArea.height(), iinkCanvas);
+ }
+ finally
+ {
+ restore(sysCanvas);
+ }
+ }
+
+ canvas.drawBitmap(bitmap, 0, 0, null);
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int newWidth, int newHeight, int oldWidth, int oldHeight)
+ {
+ DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
+
+ synchronized (this)
+ {
+ // Direct draw
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
+ {
+ if (iinkCanvas != null)
+ iinkCanvas.destroy();
+
+ iinkCanvas = new Canvas(null, extraBrushConfigs, typefaceMap, imageLoader, offlineSurfaceManager, metrics.xdpi, metrics.ydpi);
+ }
+ else // Bitmap draw
+ {
+ if (bitmap != null)
+ bitmap.recycle();
+ if (iinkCanvas != null)
+ iinkCanvas.destroy();
+
+ bitmap = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);
+ sysCanvas = new android.graphics.Canvas(bitmap);
+ iinkCanvas = new Canvas(sysCanvas, extraBrushConfigs, typefaceMap, imageLoader, offlineSurfaceManager, metrics.xdpi, metrics.ydpi);
+ }
+
+ iinkCanvas.setClearOnStartDraw(false);
+ iinkCanvas.setKeepGLRenderer(true);
+ canvasWidth = newWidth;
+ canvasHeight = newHeight;
+ }
+
+ super.onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
+ }
+
+ private void prepare(android.graphics.Canvas canvas, Rect clipRect)
+ {
+ canvas.save();
+ canvas.clipRect(clipRect);
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
+ canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
+ }
+
+ private void restore(android.graphics.Canvas canvas)
+ {
+ canvas.restore();
+ }
+
+ public final void update(Renderer renderer, int x, int y, int width, int height)
+ {
+ boolean emptyArea;
+
+ // Direct draw
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
+ {
+ Rect updatedArea = new Rect(x, y, x + width, y + height);
+ synchronized (this)
+ {
+ if (canvasWidth > 0 && canvasHeight > 0)
+ updatedArea.intersect(new Rect(0, 0, canvasWidth, canvasHeight));
+
+ emptyArea = updatedArea.isEmpty();
+ lastRenderer = renderer;
+ }
+ }
+ else // Bitmap draw
+ {
+ synchronized (this)
+ {
+ updateArea.union(x, y, x + width, y + height);
+ if (canvasWidth > 0 && canvasHeight > 0)
+ updateArea.intersect(new Rect(0, 0, canvasWidth, canvasHeight));
+
+ emptyArea = updateArea.isEmpty();
+ lastRenderer = renderer;
+ }
+ }
+
+ if (!emptyArea)
+ {
+ postInvalidate(x, y, x + width, y + height);
+ }
+ }
+
+ public void setScrollbar(Renderer renderer, int viewWidthPx, int pageWidthPx, int xMin, int viewHeightPx, int pageHeightPx, int yMin)
+ {
+ this.viewWidth = viewWidthPx;
+ this.pageWidth = pageWidthPx;
+ this.renderer = renderer;
+ this.pageHeight = pageHeightPx;
+ this.viewHeight = viewHeightPx;
+ this.xMin = xMin;
+ this.yMin = yMin;
+ setVerticalScrollBarEnabled(true);
+ awakenScrollBars();
+ }
+
+ @Override
+ protected int computeVerticalScrollRange()
+ {
+ return pageHeight;
+ }
+
+ @Override
+ protected int computeVerticalScrollExtent()
+ {
+ return viewHeight;
+ }
+
+ @Override
+ protected int computeVerticalScrollOffset()
+ {
+ return renderer != null ? (int) renderer.getViewOffset().y - yMin : 0;
+ }
+
+ @Override
+ protected int computeHorizontalScrollRange()
+ {
+ return pageWidth;
+ }
+
+ @Override
+ protected int computeHorizontalScrollExtent()
+ {
+ return viewWidth;
+ }
+
+ @Override
+ protected int computeHorizontalScrollOffset()
+ {
+ return renderer != null ? (int) renderer.getViewOffset().x - xMin : 0;
+ }
+}
diff --git a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/OfflineSurfaceManager.java b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/OfflineSurfaceManager.java
similarity index 100%
rename from UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/OfflineSurfaceManager.java
rename to samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/OfflineSurfaceManager.java
diff --git a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/Path.java b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/Path.java
similarity index 100%
rename from UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/Path.java
rename to samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/Path.java
diff --git a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/SmartGuideView.java b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/SmartGuideView.java
similarity index 96%
rename from UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/SmartGuideView.java
rename to samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/SmartGuideView.java
index 868b011..9389293 100644
--- a/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/SmartGuideView.java
+++ b/samples/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/SmartGuideView.java
@@ -311,12 +311,15 @@ public void setEditor(@Nullable Editor editor)
Engine engine = editor.getEngine();
exportParams = engine.createParameterSet();
- exportParams.setBoolean("export.jiix.text.words", true);
- exportParams.setBoolean("export.jiix.strokes", false);
exportParams.setBoolean("export.jiix.bounding-box", false);
exportParams.setBoolean("export.jiix.glyphs", false);
exportParams.setBoolean("export.jiix.primitives", false);
- exportParams.setBoolean("export.jiix.chars", false);
+ exportParams.setBoolean("export.jiix.strokes", false);
+ exportParams.setBoolean("export.jiix.text.chars", false);
+ exportParams.setBoolean("export.jiix.text.lines", false);
+ exportParams.setBoolean("export.jiix.text.spans", false);
+ exportParams.setBoolean("export.jiix.text.structure", false);
+ exportParams.setBoolean("export.jiix.text.words", true);
importParams = engine.createParameterSet();
importParams.setString("diagram.import.jiix.action", "update");
@@ -330,7 +333,6 @@ public void setEditor(@Nullable Editor editor)
fadeOutOtherDelay = configuration.getNumber("smart-guide.fade-out-delay.other", SMART_GUIDE_FADE_OUT_DELAY_OTHER_DEFAULT).intValue();
removeHighlightDelay = configuration.getNumber("smart-guide.highlight-removal-delay", SMART_GUIDE_HIGHLIGHT_REMOVAL_DELAY_DEFAULT).intValue();
}
-
}
public void setMenuListener(@Nullable MenuListener moreMenuListener)
@@ -399,12 +401,16 @@ public void contentChanged(@NonNull Editor editor, String[] blockIds)
// the old instance is invalid but can be restored by remapping the identifier
if (activeBlock != null && !activeBlock.isValid())
{
- activeBlock.close();
- activeBlock = editor.getBlockById(activeBlock.getId());
- if (activeBlock == null)
+ String activeBlockId = activeBlock.getId();
+ ContentBlock newActiveBlock = editor.getBlockById(activeBlockId);
+ if (newActiveBlock != null)
+ {
+ activeBlock.close();
+ activeBlock = newActiveBlock;
+ }
+ else
{
update(null, UpdateCause.EDIT);
- return;
}
}
@@ -423,12 +429,7 @@ public void onError(@NonNull Editor editor, @NonNull String blockId, @NonNull Ed
@Override
public void selectionChanged(@NonNull Editor editor)
{
- if (selectedBlock != null)
- {
- selectedBlock.close();
- }
- selectedBlock = null;
-
+ ContentBlock newSelectionBlock = null;
ContentSelectionMode mode = editor.getSelectionMode();
if (mode != ContentSelectionMode.NONE && mode != ContentSelectionMode.LASSO)
{
@@ -446,7 +447,7 @@ public void selectionChanged(@NonNull Editor editor)
ContentBlock block = editor.getBlockById(blockId);
if (block != null && block.getType().equals("Text"))
{
- selectedBlock = block;
+ newSelectionBlock = block;
break;
}
else if (block != null)
@@ -456,7 +457,13 @@ else if (block != null)
}
}
- update(selectedBlock, UpdateCause.SELECTION);
+ update(newSelectionBlock, UpdateCause.SELECTION);
+
+ if (selectedBlock != null)
+ {
+ selectedBlock.close();
+ }
+ selectedBlock = newSelectionBlock;
}
@Override
@@ -468,13 +475,15 @@ public void activeBlockChanged(@NonNull Editor editor, @NonNull String blockId)
// selectionChanged already changed the active block
return;
}
+
+ ContentBlock newActiveBlock = editor.getBlockById(blockId);
+ update(newActiveBlock, UpdateCause.EDIT);
+
if (activeBlock != null)
{
activeBlock.close();
}
- activeBlock = editor.getBlockById(blockId);
-
- update(activeBlock, UpdateCause.EDIT);
+ activeBlock = newActiveBlock;
}
@Override
diff --git a/UIReferenceImplementation/src/main/res/drawable/ic_smart_guide_more.xml b/samples/UIReferenceImplementation/src/main/res/drawable/ic_smart_guide_more.xml
similarity index 100%
rename from UIReferenceImplementation/src/main/res/drawable/ic_smart_guide_more.xml
rename to samples/UIReferenceImplementation/src/main/res/drawable/ic_smart_guide_more.xml
diff --git a/UIReferenceImplementation/src/main/res/drawable/smart_guide_bottom_border.xml b/samples/UIReferenceImplementation/src/main/res/drawable/smart_guide_bottom_border.xml
similarity index 100%
rename from UIReferenceImplementation/src/main/res/drawable/smart_guide_bottom_border.xml
rename to samples/UIReferenceImplementation/src/main/res/drawable/smart_guide_bottom_border.xml
diff --git a/UIReferenceImplementation/src/main/res/layout/editor_view.xml b/samples/UIReferenceImplementation/src/main/res/layout/editor_view.xml
similarity index 63%
rename from UIReferenceImplementation/src/main/res/layout/editor_view.xml
rename to samples/UIReferenceImplementation/src/main/res/layout/editor_view.xml
index 0a0991d..443f223 100644
--- a/UIReferenceImplementation/src/main/res/layout/editor_view.xml
+++ b/samples/UIReferenceImplementation/src/main/res/layout/editor_view.xml
@@ -6,29 +6,19 @@
android:id="@+id/editor_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:background="@android:color/white"
android:visibility="invisible">
-
-
\ No newline at end of file
diff --git a/UIReferenceImplementation/src/main/res/layout/smart_guide_layout.xml b/samples/UIReferenceImplementation/src/main/res/layout/smart_guide_layout.xml
similarity index 100%
rename from UIReferenceImplementation/src/main/res/layout/smart_guide_layout.xml
rename to samples/UIReferenceImplementation/src/main/res/layout/smart_guide_layout.xml
diff --git a/UIReferenceImplementation/src/main/res/values/colors.xml b/samples/UIReferenceImplementation/src/main/res/values/colors.xml
similarity index 100%
rename from UIReferenceImplementation/src/main/res/values/colors.xml
rename to samples/UIReferenceImplementation/src/main/res/values/colors.xml
diff --git a/UIReferenceImplementation/src/main/res/values/dimens.xml b/samples/UIReferenceImplementation/src/main/res/values/dimens.xml
similarity index 100%
rename from UIReferenceImplementation/src/main/res/values/dimens.xml
rename to samples/UIReferenceImplementation/src/main/res/values/dimens.xml
diff --git a/samples/batch-mode/build.gradle b/samples/batch-mode/build.gradle
new file mode 100644
index 0000000..9c54d51
--- /dev/null
+++ b/samples/batch-mode/build.gradle
@@ -0,0 +1,36 @@
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+}
+
+android {
+ namespace 'com.myscript.iink.samples.batchmode'
+
+ compileSdk Versions.compileSdk
+
+ buildFeatures {
+ viewBinding true
+ }
+
+ defaultConfig {
+ minSdk Versions.minSdk
+ targetSdk Versions.targetSdk
+
+ applicationId 'com.myscript.iink.samples.batchmode'
+ versionCode project.ext.iinkVersionCode
+ versionName project.ext.iinkVersionName
+
+ vectorDrawables.useSupportLibrary true
+ }
+}
+
+dependencies {
+ implementation "androidx.core:core-ktx:${Versions.androidx_core}"
+ implementation "androidx.activity:activity-ktx:${Versions.androidx_activity}"
+ implementation "androidx.appcompat:appcompat:${Versions.appcompat}"
+ implementation "com.google.android.material:material:${Versions.material}"
+ implementation "com.google.code.gson:gson:${Versions.gson}"
+
+ implementation project(':UIReferenceImplementation')
+ implementation project(':myscript-certificate')
+}
\ No newline at end of file
diff --git a/java/samples/batch-mode-kt/src/main/AndroidManifest.xml b/samples/batch-mode/src/main/AndroidManifest.xml
similarity index 64%
rename from java/samples/batch-mode-kt/src/main/AndroidManifest.xml
rename to samples/batch-mode/src/main/AndroidManifest.xml
index c1d6c93..2e787fa 100644
--- a/java/samples/batch-mode-kt/src/main/AndroidManifest.xml
+++ b/samples/batch-mode/src/main/AndroidManifest.xml
@@ -1,19 +1,16 @@
-
+
+
+
+ android:theme="@style/Theme.AppCompat.Light.DarkActionBar">
diff --git a/java/samples/batch-mode-kt/src/main/assets/conf/pointerEvents/diagram/export.svg b/samples/batch-mode/src/main/assets/conf/pointerEvents/diagram/export.svg
similarity index 100%
rename from java/samples/batch-mode-kt/src/main/assets/conf/pointerEvents/diagram/export.svg
rename to samples/batch-mode/src/main/assets/conf/pointerEvents/diagram/export.svg
diff --git a/java/samples/batch-mode-kt/src/main/assets/conf/pointerEvents/diagram/pointerEvents.json b/samples/batch-mode/src/main/assets/conf/pointerEvents/diagram/pointerEvents.json
similarity index 100%
rename from java/samples/batch-mode-kt/src/main/assets/conf/pointerEvents/diagram/pointerEvents.json
rename to samples/batch-mode/src/main/assets/conf/pointerEvents/diagram/pointerEvents.json
diff --git a/java/samples/batch-mode-kt/src/main/assets/conf/pointerEvents/math/export.tex b/samples/batch-mode/src/main/assets/conf/pointerEvents/math/export.tex
similarity index 100%
rename from java/samples/batch-mode-kt/src/main/assets/conf/pointerEvents/math/export.tex
rename to samples/batch-mode/src/main/assets/conf/pointerEvents/math/export.tex
diff --git a/java/samples/batch-mode-kt/src/main/assets/conf/pointerEvents/math/pointerEvents.json b/samples/batch-mode/src/main/assets/conf/pointerEvents/math/pointerEvents.json
similarity index 100%
rename from java/samples/batch-mode-kt/src/main/assets/conf/pointerEvents/math/pointerEvents.json
rename to samples/batch-mode/src/main/assets/conf/pointerEvents/math/pointerEvents.json
diff --git a/java/samples/batch-mode-kt/src/main/assets/conf/pointerEvents/raw content/export.jiix b/samples/batch-mode/src/main/assets/conf/pointerEvents/raw content/export.jiix
similarity index 100%
rename from java/samples/batch-mode-kt/src/main/assets/conf/pointerEvents/raw content/export.jiix
rename to samples/batch-mode/src/main/assets/conf/pointerEvents/raw content/export.jiix
diff --git a/java/samples/batch-mode-kt/src/main/assets/conf/pointerEvents/raw content/pointerEvents.json b/samples/batch-mode/src/main/assets/conf/pointerEvents/raw content/pointerEvents.json
similarity index 100%
rename from java/samples/batch-mode-kt/src/main/assets/conf/pointerEvents/raw content/pointerEvents.json
rename to samples/batch-mode/src/main/assets/conf/pointerEvents/raw content/pointerEvents.json
diff --git a/java/samples/batch-mode-kt/src/main/assets/conf/pointerEvents/text/en_US/export.txt b/samples/batch-mode/src/main/assets/conf/pointerEvents/text/en_US/export.txt
similarity index 100%
rename from java/samples/batch-mode-kt/src/main/assets/conf/pointerEvents/text/en_US/export.txt
rename to samples/batch-mode/src/main/assets/conf/pointerEvents/text/en_US/export.txt
diff --git a/java/samples/batch-mode-kt/src/main/assets/conf/pointerEvents/text/en_US/pointerEvents.json b/samples/batch-mode/src/main/assets/conf/pointerEvents/text/en_US/pointerEvents.json
similarity index 100%
rename from java/samples/batch-mode-kt/src/main/assets/conf/pointerEvents/text/en_US/pointerEvents.json
rename to samples/batch-mode/src/main/assets/conf/pointerEvents/text/en_US/pointerEvents.json
diff --git a/java/samples/batch-mode-kt/src/main/java/com/myscript/iink/samples/batchmode/IInkApplication.kt b/samples/batch-mode/src/main/java/com/myscript/iink/samples/batchmode/IInkApplication.kt
similarity index 91%
rename from java/samples/batch-mode-kt/src/main/java/com/myscript/iink/samples/batchmode/IInkApplication.kt
rename to samples/batch-mode/src/main/java/com/myscript/iink/samples/batchmode/IInkApplication.kt
index c042ccd..9663635 100644
--- a/java/samples/batch-mode-kt/src/main/java/com/myscript/iink/samples/batchmode/IInkApplication.kt
+++ b/samples/batch-mode/src/main/java/com/myscript/iink/samples/batchmode/IInkApplication.kt
@@ -6,13 +6,11 @@ import com.myscript.iink.Engine
class IInkApplication : Application() {
-
companion object {
-
private var engine: Engine? = null
fun getEngine(): Engine? {
- if (IInkApplication.engine == null) {
+ if (engine == null) {
engine = Engine.create(MyCertificate.getBytes())
}
return engine
diff --git a/java/samples/batch-mode-kt/src/main/java/com/myscript/iink/samples/batchmode/JsonResult.kt b/samples/batch-mode/src/main/java/com/myscript/iink/samples/batchmode/JsonResult.kt
similarity index 87%
rename from java/samples/batch-mode-kt/src/main/java/com/myscript/iink/samples/batchmode/JsonResult.kt
rename to samples/batch-mode/src/main/java/com/myscript/iink/samples/batchmode/JsonResult.kt
index ad9ae4f..041ee20 100644
--- a/java/samples/batch-mode-kt/src/main/java/com/myscript/iink/samples/batchmode/JsonResult.kt
+++ b/samples/batch-mode/src/main/java/com/myscript/iink/samples/batchmode/JsonResult.kt
@@ -8,10 +8,6 @@ class JsonResult {
@SerializedName("events")
private var strokes: ArrayList? = null
- fun JsonResult(strokes: ArrayList?) {
- this.strokes = strokes
- }
-
fun getStrokes(): ArrayList? {
return strokes
}
diff --git a/samples/batch-mode/src/main/java/com/myscript/iink/samples/batchmode/MainActivity.kt b/samples/batch-mode/src/main/java/com/myscript/iink/samples/batchmode/MainActivity.kt
new file mode 100644
index 0000000..9e0d88b
--- /dev/null
+++ b/samples/batch-mode/src/main/java/com/myscript/iink/samples/batchmode/MainActivity.kt
@@ -0,0 +1,379 @@
+package com.myscript.iink.samples.batchmode
+
+import android.content.Intent
+import android.graphics.Typeface
+import android.os.Bundle
+import android.view.View
+import android.widget.ProgressBar
+import android.widget.RadioGroup
+import android.widget.TextView
+import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.AppCompatButton
+import androidx.core.content.res.ResourcesCompat
+import com.google.gson.Gson
+import com.myscript.iink.ConversionState
+import com.myscript.iink.Editor
+import com.myscript.iink.MimeType
+import com.myscript.iink.PointerEvent
+import com.myscript.iink.PointerEventType
+import com.myscript.iink.PointerType
+import com.myscript.iink.Renderer
+import com.myscript.iink.uireferenceimplementation.FontMetricsProvider
+import com.myscript.iink.uireferenceimplementation.FontUtils
+import com.myscript.iink.uireferenceimplementation.ImageLoader
+import com.myscript.iink.uireferenceimplementation.ImagePainter
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.File
+import java.io.FileInputStream
+import java.io.InputStreamReader
+import java.lang.reflect.Array.setBoolean
+import java.nio.file.Files
+
+class MainActivity : AppCompatActivity() {
+
+ private lateinit var renderer : Renderer
+ private lateinit var editor: Editor
+ private lateinit var imagePainter: ImagePainter
+
+ private var useCustomInputFile = false
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContentView(R.layout.activity_main)
+
+ val engine = IInkApplication.getEngine()
+
+ if (engine == null) {
+ // The certificate provided is incorrect, you need to use the one provided by MyScript
+ AlertDialog.Builder(this)
+ .setTitle( getString(R.string.app_error_invalid_certificate_title))
+ .setMessage( getString(R.string.app_error_invalid_certificate_message))
+ .setPositiveButton(android.R.string.ok, null)
+ .show()
+ finishAndRemoveTask() // be sure to end the application
+ return
+ }
+
+ // Configure recognition
+ engine.apply {
+ configuration.apply {
+ val confDir = "zip://${application.packageCodePath}!/assets/conf"
+ setStringArray("configuration-manager.search-path", arrayOf(confDir))
+ setString("content-package.temp-folder", application.cacheDir.absolutePath)
+ // To enable text recognition for a specific language,
+ setString("lang", language);
+ // Configure the engine to disable guides (recommended in batch mode)
+ setBoolean("text.guides.enable", false);
+
+ // Activate handwriting recognition for text and shapes
+ setStringArray("raw-content.recognition.types", arrayOf("text", "shape"))
+
+ // Allow conversion of text and shapes
+ setStringArray("raw-content.convert.types", arrayOf("text", "shape"))
+ }
+ }
+
+ // At creation we have to pre initialise the editor with dpi and screen size
+
+ // Create a renderer with a null render target
+ val displayMetrics = resources.displayMetrics
+ renderer = engine.createRenderer(displayMetrics.xdpi, displayMetrics.ydpi, null).apply {
+ setViewOffset(0.0f, 0.0f)
+ viewScale = 1.0f
+ }
+
+ // Create the editor
+ editor = engine.createEditor(renderer, engine.createToolController()).apply {
+ // The editor requires a font metrics provider and a view size *before* calling setPart()
+ val typefaceMap = mutableMapOf()
+ setFontMetricsProvider(FontMetricsProvider(displayMetrics, typefaceMap))
+ setViewSize(displayMetrics.widthPixels, displayMetrics.heightPixels)
+ }
+
+ // Create a image painter to render in png
+ imagePainter = ImagePainter().apply {
+ setImageLoader(ImageLoader(editor))
+ // load fonts
+ val typefaceMap = provideTypefaces()
+ setTypefaceMap(typefaceMap)
+ editor.setFontMetricsProvider(FontMetricsProvider(applicationContext.resources.displayMetrics, typefaceMap))
+ editor.theme = (".math {font-family: STIX;}")
+ }
+
+ useCustomInputFile = false
+
+ findViewById(R.id.batch_sample_pick_file).setOnClickListener { _ -> pickFile() }
+
+ findViewById(R.id.batch_sample_remove_file).setOnClickListener(this::removeFile)
+
+ findViewById(R.id.batch_sample_execute).setOnClickListener(this::execute)
+ }
+
+ private fun provideTypefaces(): Map {
+ val typefaces = FontUtils.loadFontsFromAssets(application.assets) ?: mutableMapOf()
+ // Map key must be aligned with the font-family used in theme.css
+ val myscriptInterFont = ResourcesCompat.getFont(application, R.font.myscriptinter)
+ if (myscriptInterFont != null) {
+ typefaces["MyScriptInter"] = myscriptInterFont
+ }
+ val stixFont = ResourcesCompat.getFont(application, R.font.stix)
+ if (stixFont != null) {
+ typefaces["STIX"] = stixFont
+ }
+ return typefaces
+ }
+
+ private fun pickFile() {
+ pickFileLauncher.launch(Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
+ type = "application/*"
+ addCategory(Intent.CATEGORY_OPENABLE)
+ })
+ }
+
+ private fun removeFile(removeButton : View) {
+ useCustomInputFile = false
+ removeButton.visibility = View.GONE
+ findViewById(R.id.batch_sample_pick_file).visibility = View.VISIBLE
+ findViewById(R.id.batch_sample_file_description).visibility = View.VISIBLE
+ }
+
+ private val pickFileLauncher: ActivityResultLauncher =
+ registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) {
+ it.data?.data?.also { uri ->
+ val contentResolver = applicationContext.contentResolver
+ contentResolver.openInputStream(uri)?.use { inputStream ->
+ val workingFile = File(applicationContext.cacheDir, customInputFileName)
+ workingFile.delete()
+ Files.copy(inputStream, workingFile.toPath())
+ }
+
+ useCustomInputFile = true
+ findViewById(R.id.batch_sample_remove_file).visibility = View.VISIBLE
+ findViewById(R.id.batch_sample_pick_file).visibility = View.GONE
+ findViewById(R.id.batch_sample_file_description).visibility = View.GONE
+ }
+ }
+
+ @OptIn(DelicateCoroutinesApi::class)
+ private fun execute(executeButton: View) {
+ val partTypeGroup = findViewById(R.id.batch_sample_part_type_group)
+ val partType = when(partTypeGroup.checkedRadioButtonId) {
+ R.id.batch_sample_part_type_math -> "Math"
+ R.id.batch_sample_part_type_diagram -> "Diagram"
+ R.id.batch_sample_part_type_raw -> "Raw Content"
+ else -> "Text"
+ }
+
+ val modeGroup = findViewById(R.id.batch_sample_mode_group)
+ val incremental = when(modeGroup.checkedRadioButtonId) {
+ R.id.batch_sample_mode_incremental -> true
+ else -> false
+ }
+
+ val outputGroup = findViewById(R.id.batch_sample_output_group)
+ val pngOutput = when(outputGroup.checkedRadioButtonId) {
+ R.id.batch_sample_output_png -> true
+ else -> false
+ }
+
+ val outputStyleGroup = findViewById(R.id.batch_sample_output_style_group)
+ val convert = when(outputStyleGroup.checkedRadioButtonId) {
+ R.id.batch_sample_output_style_converted -> true
+ else -> false
+ }
+
+ val progress = findViewById(R.id.batch_sample_progress)
+
+ progress.visibility = View.VISIBLE
+ executeButton.isEnabled = false
+ partTypeGroup.isEnabled = false
+ modeGroup.isEnabled = false
+ outputGroup.isEnabled = false
+ outputStyleGroup.isEnabled = false
+
+ GlobalScope.launch(Dispatchers.Main) {
+ val outputFile = withContext(Dispatchers.IO) {
+ process(partType, incremental, pngOutput, convert)
+ }
+
+ progress.visibility = View.GONE
+ executeButton.isEnabled = true
+ partTypeGroup.isEnabled = true
+ modeGroup.isEnabled = true
+ outputGroup.isEnabled = true
+ outputStyleGroup.isEnabled = true
+
+ if (!outputFile.exists()) {
+ Toast.makeText(applicationContext, "An error occurred when exporting", Toast.LENGTH_LONG).show()
+ return@launch
+ }
+
+ exportedFilePath = outputFile.absolutePath
+
+ saveFileLauncher.launch(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
+ type = "application/*"
+ addCategory(Intent.CATEGORY_OPENABLE)
+ putExtra(Intent.EXTRA_TITLE, outputFile.name)
+ })
+ }
+ }
+
+ private var exportedFilePath: String? = null
+ private val saveFileLauncher: ActivityResultLauncher =
+ registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) {
+ exportedFilePath?.let { exportedFilePath ->
+ val exportedFile = File(exportedFilePath)
+ if (exportedFile.exists()) {
+ it.data?.data?.also { uri ->
+ val contentResolver = applicationContext.contentResolver
+ contentResolver.openOutputStream(uri)?.use { outputStream ->
+ Files.copy(exportedFile.toPath(), outputStream)
+ this.exportedFilePath = null
+ }
+ }
+ }
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+
+ editor.close()
+ renderer.close()
+ }
+
+ private fun process(partType: String, incremental: Boolean, renderToPNG: Boolean, convert: Boolean): File {
+ val engine = requireNotNull(IInkApplication.getEngine())
+
+ // Create a new package
+ val contentPackage = engine.createPackage(iinkPackageName)
+ // Create a new part
+ val contentPart = contentPackage.createPart(partType)
+ // Associate editor with the new part
+ editor.part = contentPart
+
+ // Now we can process pointer events and feed the editor with an array of Pointer Events loaded from the json file
+ loadAndFeedPointerEvents(partType, incremental)
+
+ // Choose the right mimeType to export according to the partType chosen
+ var mimeType = MimeType.PNG
+ if(!renderToPNG) {
+ when (partType) {
+ typeOfPart[0] -> mimeType = MimeType.TEXT // Text
+ typeOfPart[1] -> mimeType = MimeType.LATEX // Math
+ typeOfPart[2] -> mimeType = MimeType.SVG // Diagram
+ typeOfPart[3] -> mimeType = MimeType.JIIX // Raw Content
+ else -> {}
+ }
+ }
+
+ editor.waitForIdle()
+
+ if (convert) {
+ editor.convert(editor.rootBlock, ConversionState.DIGITAL_EDIT)
+ }
+ val outputFile = File(applicationContext.filesDir, "$exportFileName${mimeType.fileExtensions}")
+ editor.export_(editor.rootBlock, outputFile.absolutePath, mimeType, if (mimeType.isImage) imagePainter else null)
+
+ // Closing after using
+ editor.part = null
+ contentPart.close()
+ contentPackage.close()
+ engine.deletePackage(packageName)
+
+ return outputFile
+ }
+
+ private fun loadAndFeedPointerEvents(partType: String, incremental: Boolean = false) {
+ // Loading the content of the pointerEvents JSON file
+ val customInputFile = File(applicationContext.cacheDir, customInputFileName)
+ val inputStream = if (useCustomInputFile && customInputFile.exists()) {
+ FileInputStream(customInputFile)
+ } else {
+ val partTypeLowercase = partType.lowercase()
+ val pointerEventsPath = if (partTypeLowercase == "text") {
+ "conf/pointerEvents/$partTypeLowercase/$language/pointerEvents.json"
+ } else {
+ "conf/pointerEvents/$partTypeLowercase/pointerEvents.json"
+ }
+
+ resources.assets.open(pointerEventsPath)
+ }
+
+ // Mapping the content into a JsonResult class
+ val jsonResult = Gson().fromJson(InputStreamReader(inputStream), JsonResult::class.java)
+
+ // Add each element to a list
+ val pointerEventsList = mutableListOf()
+
+ val xdpi = resources.displayMetrics.xdpi
+ val ydpi = resources.displayMetrics.ydpi
+
+ jsonResult.getStrokes()?.forEach { stroke ->
+ val strokeX = stroke.x
+ val strokeY = stroke.y
+ val strokeT = stroke.t
+ val strokeP = stroke.p
+ val length = stroke.x.size
+
+ for (index in 0 until length) {
+ // Transform the x and y coordinates of the stroke from mm to px
+ // This is needed to be adaptive for each device
+ val x = strokeX[index] / 25.4f * xdpi
+ val y = strokeY[index] / 25.4f * ydpi
+
+ if (incremental) {
+ // In incremental mode we send data direct to the editor
+ when (index) {
+ 0 -> editor.pointerDown(x, y, strokeT[index], strokeP[index], PointerType.PEN, 1)
+ length - 1 -> editor.pointerUp(x, y, strokeT[index], strokeP[index], PointerType.PEN, 1)
+ else -> editor.pointerMove(x, y, strokeT[index], strokeP[index], PointerType.PEN, stroke.pointerId)
+ }
+ } else {
+ // In batch mode we keep data in a array
+ pointerEventsList += PointerEvent().apply {
+ pointerType = stroke.pointerType
+ pointerId = stroke.pointerId
+ eventType = when (index) {
+ 0 -> PointerEventType.DOWN
+ length - 1 -> PointerEventType.UP
+ else -> PointerEventType.MOVE
+ }
+ this.x = x
+ this.y = y
+ t = strokeT[index]
+ f = strokeP[index]
+ }
+ }
+ }
+ }
+ if (!incremental) {
+ editor.pointerEvents(pointerEventsList.toTypedArray(), false)
+ }
+ }
+
+ companion object {
+ private const val TAG = "MainActivity"
+
+ private const val customInputFileName = "customInputFile"
+
+ // /!\ Warning use the real MyScript name of part as this string will be used for part creation
+ private val typeOfPart = listOf("Text", "Math", "Diagram", "Raw Content")
+ private const val iinkPackageName = "package.iink"
+ private const val exportFileName = "export"
+ private const val language = "en_US"
+ }
+}
\ No newline at end of file
diff --git a/java/samples/batch-mode-kt/src/main/java/com/myscript/iink/samples/batchmode/Stroke.kt b/samples/batch-mode/src/main/java/com/myscript/iink/samples/batchmode/Stroke.kt
similarity index 76%
rename from java/samples/batch-mode-kt/src/main/java/com/myscript/iink/samples/batchmode/Stroke.kt
rename to samples/batch-mode/src/main/java/com/myscript/iink/samples/batchmode/Stroke.kt
index 7b0c608..788d0fd 100644
--- a/java/samples/batch-mode-kt/src/main/java/com/myscript/iink/samples/batchmode/Stroke.kt
+++ b/samples/batch-mode/src/main/java/com/myscript/iink/samples/batchmode/Stroke.kt
@@ -7,12 +7,12 @@ import com.myscript.iink.PointerType
import java.util.*
data class Stroke(
- var pointerType: PointerType? = null,
- var pointerId: Int = 0,
- var x: FloatArray,
- var y: FloatArray,
- var t: LongArray,
- var p: FloatArray
+ val pointerType: PointerType,
+ val pointerId: Int = 0,
+ val x: FloatArray,
+ val y: FloatArray,
+ val t: LongArray,
+ val p: FloatArray
) {
override fun toString(): String = "{" +
"\"pointerType\":\"$pointerType\"," +
diff --git a/java/common-kotlin/src/main/res/drawable-v24/ic_launcher_foreground.xml b/samples/batch-mode/src/main/res/drawable-v24/ic_launcher_foreground.xml
similarity index 100%
rename from java/common-kotlin/src/main/res/drawable-v24/ic_launcher_foreground.xml
rename to samples/batch-mode/src/main/res/drawable-v24/ic_launcher_foreground.xml
diff --git a/java/common-kotlin/src/main/res/drawable/ic_launcher_background.xml b/samples/batch-mode/src/main/res/drawable/ic_launcher_background.xml
similarity index 100%
rename from java/common-kotlin/src/main/res/drawable/ic_launcher_background.xml
rename to samples/batch-mode/src/main/res/drawable/ic_launcher_background.xml
diff --git a/samples/batch-mode/src/main/res/font/myscriptinter.xml b/samples/batch-mode/src/main/res/font/myscriptinter.xml
new file mode 100644
index 0000000..5c40ea3
--- /dev/null
+++ b/samples/batch-mode/src/main/res/font/myscriptinter.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/batch-mode/src/main/res/font/myscriptinter_bold.otf b/samples/batch-mode/src/main/res/font/myscriptinter_bold.otf
new file mode 100644
index 0000000..93179f8
Binary files /dev/null and b/samples/batch-mode/src/main/res/font/myscriptinter_bold.otf differ
diff --git a/samples/batch-mode/src/main/res/font/myscriptinter_regular.otf b/samples/batch-mode/src/main/res/font/myscriptinter_regular.otf
new file mode 100644
index 0000000..935f484
Binary files /dev/null and b/samples/batch-mode/src/main/res/font/myscriptinter_regular.otf differ
diff --git a/samples/batch-mode/src/main/res/font/stix.xml b/samples/batch-mode/src/main/res/font/stix.xml
new file mode 100644
index 0000000..b83505f
--- /dev/null
+++ b/samples/batch-mode/src/main/res/font/stix.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/batch-mode/src/main/res/font/stix_general.ttf b/samples/batch-mode/src/main/res/font/stix_general.ttf
new file mode 100644
index 0000000..ac15532
Binary files /dev/null and b/samples/batch-mode/src/main/res/font/stix_general.ttf differ
diff --git a/samples/batch-mode/src/main/res/font/stix_italic.otf b/samples/batch-mode/src/main/res/font/stix_italic.otf
new file mode 100644
index 0000000..b79db47
Binary files /dev/null and b/samples/batch-mode/src/main/res/font/stix_italic.otf differ
diff --git a/samples/batch-mode/src/main/res/layout/activity_main.xml b/samples/batch-mode/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..28c9abc
--- /dev/null
+++ b/samples/batch-mode/src/main/res/layout/activity_main.xml
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/java/common-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/samples/batch-mode/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
similarity index 100%
rename from java/common-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
rename to samples/batch-mode/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
diff --git a/java/common-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/samples/batch-mode/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
similarity index 100%
rename from java/common-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
rename to samples/batch-mode/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
diff --git a/java/common-kotlin/src/main/res/mipmap-hdpi/ic_launcher.png b/samples/batch-mode/src/main/res/mipmap-hdpi/ic_launcher.png
similarity index 100%
rename from java/common-kotlin/src/main/res/mipmap-hdpi/ic_launcher.png
rename to samples/batch-mode/src/main/res/mipmap-hdpi/ic_launcher.png
diff --git a/java/common-kotlin/src/main/res/mipmap-mdpi/ic_launcher.png b/samples/batch-mode/src/main/res/mipmap-mdpi/ic_launcher.png
similarity index 100%
rename from java/common-kotlin/src/main/res/mipmap-mdpi/ic_launcher.png
rename to samples/batch-mode/src/main/res/mipmap-mdpi/ic_launcher.png
diff --git a/java/common-kotlin/src/main/res/mipmap-xhdpi/ic_launcher.png b/samples/batch-mode/src/main/res/mipmap-xhdpi/ic_launcher.png
similarity index 100%
rename from java/common-kotlin/src/main/res/mipmap-xhdpi/ic_launcher.png
rename to samples/batch-mode/src/main/res/mipmap-xhdpi/ic_launcher.png
diff --git a/java/common-kotlin/src/main/res/mipmap-xxhdpi/ic_launcher.png b/samples/batch-mode/src/main/res/mipmap-xxhdpi/ic_launcher.png
similarity index 100%
rename from java/common-kotlin/src/main/res/mipmap-xxhdpi/ic_launcher.png
rename to samples/batch-mode/src/main/res/mipmap-xxhdpi/ic_launcher.png
diff --git a/java/common-kotlin/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/samples/batch-mode/src/main/res/mipmap-xxxhdpi/ic_launcher.png
similarity index 100%
rename from java/common-kotlin/src/main/res/mipmap-xxxhdpi/ic_launcher.png
rename to samples/batch-mode/src/main/res/mipmap-xxxhdpi/ic_launcher.png
diff --git a/samples/batch-mode/src/main/res/values/strings.xml b/samples/batch-mode/src/main/res/values/strings.xml
new file mode 100644
index 0000000..e0017c3
--- /dev/null
+++ b/samples/batch-mode/src/main/res/values/strings.xml
@@ -0,0 +1,33 @@
+
+ Batch Mode
+
+ Invalid certificate
+ Please check certificate data provided at Engine creation.
+
+ Pick a file
+ Choose
+ No file, embedded file will be used
+ Remove Custom File
+
+ Select a part type
+ Text
+ Math
+ Diagram
+ Raw Content
+
+ Mode
+ Batch
+ Incremental
+
+ Output
+ Auto
+ PNG
+ If Auto is selected, Text will be exported to plain text, Math to LaTeX, Diagram to SVG and Raw Content to JIIX.
+
+ Output Style
+ Ink
+ Converted
+
+ Execute!
+
+
\ No newline at end of file
diff --git a/samples/build.gradle b/samples/build.gradle
new file mode 100644
index 0000000..b1ef3f9
--- /dev/null
+++ b/samples/build.gradle
@@ -0,0 +1,124 @@
+import org.apache.commons.io.FileUtils
+import org.apache.commons.io.filefilter.FileFilterUtils
+
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath "com.android.tools.build:gradle:${Versions.android_gradle_plugin}"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}"
+ }
+}
+
+subprojects {
+ afterEvaluate { proj ->
+ if (proj.hasProperty('android')) {
+ configure(android.lintOptions) {
+ abortOnError false
+ }
+ android {
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_21
+ targetCompatibility JavaVersion.VERSION_21
+ }
+
+ ndkVersion Versions.ndk
+ }
+ }
+
+ plugins.withId('com.android.application') {
+ tasks.register('DownloadAndExtractAssets', Copy) {
+ def resourcesURL = gradle.ext.iinkResourcesURL
+ def sourceUrls = ["${resourcesURL}/myscript-iink-recognition-diagram.zip",
+ "${resourcesURL}/myscript-iink-recognition-raw-content.zip",
+ "${resourcesURL}/myscript-iink-recognition-raw-content2.zip",
+ "${resourcesURL}/myscript-iink-recognition-math.zip",
+ "${resourcesURL}/myscript-iink-recognition-math2.zip",
+ "${resourcesURL}/myscript-iink-recognition-text-en_US.zip"]
+ def targetDir = new File(proj.projectDir, "src/main/assets/")
+ def diagramConf = new File(targetDir, "conf/diagram.conf")
+ def rawContentConf = new File(targetDir, "conf/raw-content.conf")
+ def rawContent2Conf = new File(targetDir, "conf/raw-content2.conf")
+ def mathConf = new File(targetDir, "conf/math.conf")
+ def math2Conf = new File(targetDir, "conf/math2.conf")
+ def textConf = new File(targetDir, "conf/en_US.conf")
+
+ if (!diagramConf.exists() || !rawContentConf.exists() || !rawContent2Conf.exists() || !mathConf.exists() || !math2Conf.exists() || !textConf.exists()) {
+ def tmpAssetsDir = new File(proj.projectDir, "tmp-assets/")
+ def zipDir = new File(tmpAssetsDir, "zips")
+
+ if (!tmpAssetsDir.isDirectory())
+ tmpAssetsDir.mkdirs()
+
+ if (!zipDir.isDirectory())
+ zipDir.mkdirs()
+
+ sourceUrls.each { sourceUrl ->
+ ant.get(src: sourceUrl, dest: zipDir.getPath())
+ }
+
+ File[] zipFiles = FileUtils.listFiles(zipDir, FileFilterUtils.suffixFileFilter("zip"), FileFilterUtils.trueFileFilter())
+ zipFiles.each { File zipFile ->
+ from zipTree(zipFile)
+ into tmpAssetsDir
+ }
+ }
+ }
+
+ tasks.register('CopyAssets', Copy) {
+ dependsOn DownloadAndExtractAssets
+ def targetDir = new File(proj.projectDir, "src/main/assets/")
+ def diagramConf = new File(targetDir, "conf/diagram.conf")
+ def rawContentConf = new File(targetDir, "conf/raw-content.conf")
+ def rawContent2Conf = new File(targetDir, "conf/raw-content2.conf")
+ def mathConf = new File(targetDir, "conf/math.conf")
+ def math2Conf = new File(targetDir, "conf/math2.conf")
+ def textConf = new File(targetDir, "conf/en_US.conf")
+
+ if (!diagramConf.exists() || !rawContentConf.exists() || !rawContent2Conf.exists() || !mathConf.exists() || !math2Conf.exists() || !textConf.exists()) {
+ def tmpAssetsDir = new File(proj.projectDir, "tmp-assets/")
+
+ if (!tmpAssetsDir.isDirectory())
+ tmpAssetsDir.mkdirs()
+
+ def recognitionAssetDir = new File(tmpAssetsDir, "recognition-assets/")
+
+ println "Copying downloaded assets from $recognitionAssetDir to $targetDir"
+ from recognitionAssetDir
+ into targetDir
+
+ doLast {
+ tmpAssetsDir.deleteDir()
+ }
+ }
+ }
+
+ preBuild.dependsOn(CopyAssets)
+ }
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ ext {
+ compileSdk = Versions.compileSdk
+ minSdk = Versions.minSdk
+ targetSdk = Versions.targetSdk
+
+ appcompatVersion = Versions.appcompat
+ gsonVersion = Versions.gson
+
+ iinkVersionName = gradle.ext.iinkVersionName
+ iinkVersionCode = gradle.ext.iinkVersionCode
+ }
+}
+
+tasks.register('clean', Delete) {
+ delete rootProject.buildDir
+}
\ No newline at end of file
diff --git a/samples/buildSrc/build.gradle.kts b/samples/buildSrc/build.gradle.kts
new file mode 100644
index 0000000..876c922
--- /dev/null
+++ b/samples/buildSrc/build.gradle.kts
@@ -0,0 +1,7 @@
+plugins {
+ `kotlin-dsl`
+}
+
+repositories {
+ mavenCentral()
+}
diff --git a/samples/buildSrc/src/main/java/Dependencies.kt b/samples/buildSrc/src/main/java/Dependencies.kt
new file mode 100644
index 0000000..797fa55
--- /dev/null
+++ b/samples/buildSrc/src/main/java/Dependencies.kt
@@ -0,0 +1,33 @@
+// Copyright MyScript. All rights reserved.
+
+object Versions {
+ const val android_gradle_plugin = "8.5.0"
+
+ const val kotlin = "1.9.23"
+
+ // configure versions used by dependencies to harmonize and update easily across all components
+ const val compileSdk = 34
+ const val minSdk = 23
+ const val targetSdk = 34
+
+ // native build tools
+ const val ndk = "21.4.7075529"
+ const val cmake = "3.22.1"
+
+ // android
+ const val androidx_core = "1.10.1"
+ const val androidx_activity = "1.7.2"
+ const val androidx_cardview = "1.0.0"
+ const val androidx_preference = "1.2.0"
+ const val androidx_lifecycle = "2.6.1"
+ const val androidx_recyclerview = "1.3.2"
+ const val material = "1.9.0"
+ const val appcompat = "1.6.1"
+ const val annotation = "1.6.0"
+ const val documentfile = "1.0.1"
+ const val recyclerview = "1.3.2"
+
+ // 3rd party
+ const val gson = "2.10.1"
+ const val okhttp = "4.11.0"
+}
\ No newline at end of file
diff --git a/samples/certificate/build_android.gradle b/samples/certificate/build_android.gradle
new file mode 100644
index 0000000..8676f38
--- /dev/null
+++ b/samples/certificate/build_android.gradle
@@ -0,0 +1,15 @@
+plugins {
+ id 'com.android.library'
+}
+
+android {
+ namespace 'com.myscript.certificate'
+
+ compileSdk = Versions.compileSdk
+ defaultConfig {
+ minSdk Versions.minSdk
+ targetSdk Versions.targetSdk
+ versionCode 1
+ versionName '1.0'
+ }
+}
diff --git a/samples/certificate/src/main/AndroidManifest.xml b/samples/certificate/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..9b65eb0
--- /dev/null
+++ b/samples/certificate/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/certificate/src/main/java/com/myscript/certificate/MyCertificate.java b/samples/certificate/src/main/java/com/myscript/certificate/MyCertificate.java
similarity index 54%
rename from certificate/src/main/java/com/myscript/certificate/MyCertificate.java
rename to samples/certificate/src/main/java/com/myscript/certificate/MyCertificate.java
index b393269..7217267 100644
--- a/certificate/src/main/java/com/myscript/certificate/MyCertificate.java
+++ b/samples/certificate/src/main/java/com/myscript/certificate/MyCertificate.java
@@ -12,9 +12,9 @@ public final class MyCertificate
*
* @return The bytes of the certificate.
*/
- public static byte[] getBytes() {
- throw new RuntimeException(
- "Please replace the content of MyCertificate.java with the certificate you received from "
- + "the developer portal");
- }
+ public static byte[] getBytes() {
+ throw new RuntimeException(
+ "Please replace the content of MyCertificate.java with the certificate you received from "
+ + "the developer portal");
+ }
}
\ No newline at end of file
diff --git a/samples/exercise-assessment/build.gradle b/samples/exercise-assessment/build.gradle
new file mode 100644
index 0000000..a72b7ea
--- /dev/null
+++ b/samples/exercise-assessment/build.gradle
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+}
+
+android {
+ namespace 'com.myscript.iink.samples.assessment'
+
+ compileSdk Versions.compileSdk
+
+ defaultConfig {
+ applicationId "com.myscript.iink.samples.assessment"
+
+ minSdk Versions.minSdk
+ targetSdk Versions.targetSdk
+ versionCode project.ext.iinkVersionCode
+ versionName project.ext.iinkVersionName
+ }
+}
+
+dependencies {
+ implementation "androidx.core:core-ktx:${Versions.androidx_core}"
+ implementation "androidx.appcompat:appcompat:${Versions.appcompat}"
+ implementation "com.google.android.material:material:${Versions.material}"
+
+ implementation project(':UIReferenceImplementation')
+ implementation project(':myscript-certificate')
+}
+
+tasks.register('copyCustomMathConfigRecognitionAssets', Copy) {
+ from "${projectDir}/custom_res"
+ into "${projectDir}/src/main/assets"
+}
+
+preBuild.dependsOn(copyCustomMathConfigRecognitionAssets)
\ No newline at end of file
diff --git a/samples/exercise-assessment/custom_res/conf/math.conf b/samples/exercise-assessment/custom_res/conf/math.conf
new file mode 100644
index 0000000..e6b245d
--- /dev/null
+++ b/samples/exercise-assessment/custom_res/conf/math.conf
@@ -0,0 +1,17 @@
+Bundle-Version: 1.0
+Bundle-Name: math
+Configuration-Script:
+ AddResDir ../resources
+ AddResDir /data/user/0/com.myscript.iink.samples.assessment/files
+
+Name: standard
+Type: Math
+Configuration-Script:
+ AddResource math/math-ak.res
+ AddResource math/math-grm-standard.res
+
+Name: standardK8
+Type: Math
+Configuration-Script:
+ AddResource math/math-ak.res
+ AddResource math-grm-standardK8.res
\ No newline at end of file
diff --git a/java/samples/exercise-assessment-kt/src/main/AndroidManifest.xml b/samples/exercise-assessment/src/main/AndroidManifest.xml
similarity index 84%
rename from java/samples/exercise-assessment-kt/src/main/AndroidManifest.xml
rename to samples/exercise-assessment/src/main/AndroidManifest.xml
index 496f955..f0cb577 100644
--- a/java/samples/exercise-assessment-kt/src/main/AndroidManifest.xml
+++ b/samples/exercise-assessment/src/main/AndroidManifest.xml
@@ -3,8 +3,9 @@
~ Copyright (c) MyScript. All rights reserved.
-->
-
+
+
+
-
+
\ No newline at end of file
diff --git a/java/samples/exercise-assessment-kt/src/main/java/com/myscript/iink/samples/assessment/MainActivity.kt b/samples/exercise-assessment/src/main/java/com/myscript/iink/samples/assessment/MainActivity.kt
similarity index 81%
rename from java/samples/exercise-assessment-kt/src/main/java/com/myscript/iink/samples/assessment/MainActivity.kt
rename to samples/exercise-assessment/src/main/java/com/myscript/iink/samples/assessment/MainActivity.kt
index def50ee..ed0e62e 100644
--- a/java/samples/exercise-assessment-kt/src/main/java/com/myscript/iink/samples/assessment/MainActivity.kt
+++ b/samples/exercise-assessment/src/main/java/com/myscript/iink/samples/assessment/MainActivity.kt
@@ -1,6 +1,5 @@
package com.myscript.iink.samples.assessment
-import android.content.DialogInterface
import android.graphics.Color
import android.graphics.Rect
import android.graphics.Typeface
@@ -9,12 +8,9 @@ import android.util.Log
import android.view.*
import android.widget.Toast
import androidx.annotation.DimenRes
-import androidx.annotation.NonNull
-import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
-import androidx.constraintlayout.widget.ConstraintLayout
import com.myscript.iink.*
-import com.myscript.iink.app.common.activities.ErrorActivity
+import com.myscript.iink.samples.assessment.utils.ErrorActivity
import com.myscript.iink.uireferenceimplementation.*
class MainActivity : AppCompatActivity(), IEditorListener {
@@ -32,54 +28,46 @@ class MainActivity : AppCompatActivity(), IEditorListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
+
ErrorActivity.setExceptionHandler(applicationContext)
- // Note: could be managed by domain layer and handled through observable error channel
- // but kept simple as is to avoid adding too much complexity for this special (unrecoverable) error case
- if (MyIInkApplication.getEngine() == null) {
- // the certificate provided in `BatchModule.provideEngine` is most likely incorrect
- AlertDialog.Builder(this)
- .setTitle( getString(R.string.app_error_invalid_certificate_title))
- .setMessage( getString(R.string.app_error_invalid_certificate_message))
- .setPositiveButton(R.string.dialog_ok,
- DialogInterface.OnClickListener {
- _,
- _ ->
- finishAffinity()
- finishAndRemoveTask() // be sure to end the application
- })
- .show()
- return
- }
// Create a new package
- contentPackage = MyIInkApplication.getEngine()?.createPackage(IINK_PACKAGE_NAME)
- if(contentPackage==null){
- return;
- }
-
+ MyIInkApplication.getEngine()?.createPackage(IINK_PACKAGE_NAME)?.let { contentPackage ->
+ this.contentPackage = contentPackage
- // TODO: try different part types: Diagram, Drawing, Math, Text, Text Document.
- answerEditorView1 =findViewById(R.id.problemSolver1).findViewById(R.id.editor_view)
- initWith(answerEditorView1, contentPackage!!,"Math","standard")
+ // TODO: try different part types: Diagram, Drawing, Math, Text, Text Document.
+ findViewById(R.id.problemSolver1).findViewById(com.myscript.iink.uireferenceimplementation.R.id.editor_view)?.let { answerEditorView ->
+ answerEditorView1 = answerEditorView
+ initWith(answerEditorView, contentPackage,"Math","standard")
+ }
- answerEditorView2 =findViewById(R.id.problemSolver2).findViewById(R.id.editor_view)
- initWith(answerEditorView2, contentPackage!!,"Math","standardK8")
+ findViewById(R.id.problemSolver2).findViewById(com.myscript.iink.uireferenceimplementation.R.id.editor_view)?.let { answerEditorView ->
+ answerEditorView2 = answerEditorView
+ initWith(answerEditorView, contentPackage,"Math","standardK8")
+ }
- answerEditorView3 =findViewById(R.id.problemSolver3).findViewById(R.id.editor_view)
- initWith(answerEditorView3, contentPackage!!,"Text")
+ findViewById(R.id.problemSolver3).findViewById(com.myscript.iink.uireferenceimplementation.R.id.editor_view)?.let { answerEditorView ->
+ answerEditorView3 = answerEditorView
+ initWith(answerEditorView, contentPackage,"Text")
+ }
- answerEditorView4 =findViewById(R.id.problemSolver4).findViewById(R.id.editor_view)
- initWith(answerEditorView4, contentPackage!!,"Diagram")
+ findViewById(R.id.problemSolver4).findViewById(com.myscript.iink.uireferenceimplementation.R.id.editor_view)?.let { answerEditorView ->
+ answerEditorView4 = answerEditorView
+ initWith(answerEditorView, contentPackage,"Diagram")
+ }
- answerEditorView5 =findViewById(R.id.problemSolver5).findViewById(R.id.editor_view)
- initWith(answerEditorView5, contentPackage!!,"Drawing")
+ findViewById(R.id.problemSolver5).findViewById(com.myscript.iink.uireferenceimplementation.R.id.editor_view)?.let { answerEditorView ->
+ answerEditorView5 = answerEditorView
+ initWith(answerEditorView, contentPackage,"Drawing")
+ }
+ }
}
- fun initWith(@NonNull editorView: EditorView?,@NonNull contentPackage: ContentPackage,@NonNull partType:String, mathConfig: String="" ){
- if(editorView==null||contentPackage==null||partType.isEmpty())
+ private fun initWith(editorView: EditorView, contentPackage: ContentPackage, partType:String, mathConfig: String="" ){
+ if(partType.isEmpty())
return
- val editorBinding: EditorBinding = EditorBinding(MyIInkApplication.getEngine(),
+ val editorBinding = EditorBinding(MyIInkApplication.getEngine(),
FontUtils.loadFontsFromAssets(application.assets) ?: emptyMap())
val editorData = editorBinding.openEditor(editorView)
@@ -151,6 +139,7 @@ class MainActivity : AppCompatActivity(), IEditorListener {
val horizontalMargin = resources.getDimension(horizontalMarginRes)
val verticalMarginMM = 25.4f * verticalMargin / displayMetrics.ydpi
val horizontalMarginMM = 25.4f * horizontalMargin / displayMetrics.xdpi
+ setNumber("text.margin.top", verticalMarginMM)
setNumber("text.margin.left", horizontalMarginMM)
setNumber("text.margin.right", horizontalMarginMM)
setNumber("math.margin.top", verticalMarginMM)
@@ -225,7 +214,7 @@ class MainActivity : AppCompatActivity(), IEditorListener {
R.id.menu_clear -> editor.clear()
R.id.menu_convert -> {
val conversionState = editor.getSupportedTargetConversionStates(editor.rootBlock)
- if (conversionState != null && conversionState.isNotEmpty()) {
+ if (conversionState.isNotEmpty()) {
editor.convert(editor.rootBlock, conversionState.first())
}
}
diff --git a/samples/exercise-assessment/src/main/java/com/myscript/iink/samples/assessment/MathGrammarK8DynamicRes.kt b/samples/exercise-assessment/src/main/java/com/myscript/iink/samples/assessment/MathGrammarK8DynamicRes.kt
new file mode 100644
index 0000000..c1b1022
--- /dev/null
+++ b/samples/exercise-assessment/src/main/java/com/myscript/iink/samples/assessment/MathGrammarK8DynamicRes.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+package com.myscript.iink.samples.assessment
+
+import com.myscript.iink.Engine
+import java.io.File
+import java.io.IOException
+
+
+class MathGrammarK8DynamicRes
+{
+ private val fileName = "math-grm-standardK8.res"
+ private val k8Grammar = "symbol = 0 1 2 3 4 5 6 7 8 9 + - / ÷ = . , % | ( ) : * x\n" +
+ "leftpar = (\n" +
+ "rightpar = )\n" +
+ "currency_symbol = \$ R € ₹ £\n" +
+ "character ::= identity(symbol)\n" +
+ " | identity(currency_symbol)\n" +
+ "fractionless ::= identity(character)\n" +
+ " | fence (fractionless, leftpar, rightpar)\n" +
+ " | hpair(fractionless, fractionless)\n" +
+ "fractionable ::= identity(character)\n" +
+ " | fence (fractionable, leftpar, rightpar)\n" +
+ " | hpair(fractionable, fractionable)\n" +
+ " | fraction(fractionless, fractionless)\n" +
+ "expression ::= identity(character)\n" +
+ " | fence (expression, leftpar, rightpar)\n" +
+ " | hpair(expression, expression)\n" +
+ " | fraction(fractionable, fractionable)\n" +
+ "start(expression)"
+
+ @Throws(IllegalArgumentException::class, RuntimeException::class, IOException::class)
+ fun build(eng : Engine, filePath : String) {
+ val rab = eng.createRecognitionAssetsBuilder()
+ rab.compile("Math Grammar",k8Grammar)
+ val file: File = File(filePath, File.separator + fileName)
+ rab.store(file.path)
+ }
+}
diff --git a/java/samples/exercise-assessment-kt/src/main/java/com/myscript/iink/samples/assessment/MyIInkApplication.kt b/samples/exercise-assessment/src/main/java/com/myscript/iink/samples/assessment/MyIInkApplication.kt
similarity index 100%
rename from java/samples/exercise-assessment-kt/src/main/java/com/myscript/iink/samples/assessment/MyIInkApplication.kt
rename to samples/exercise-assessment/src/main/java/com/myscript/iink/samples/assessment/MyIInkApplication.kt
diff --git a/java/common-kotlin/src/main/java/com/myscript/iink/app/common/activities/ErrorActivity.kt b/samples/exercise-assessment/src/main/java/com/myscript/iink/samples/assessment/utils/ErrorActivity.kt
similarity index 88%
rename from java/common-kotlin/src/main/java/com/myscript/iink/app/common/activities/ErrorActivity.kt
rename to samples/exercise-assessment/src/main/java/com/myscript/iink/samples/assessment/utils/ErrorActivity.kt
index 509724e..c77959d 100644
--- a/java/common-kotlin/src/main/java/com/myscript/iink/app/common/activities/ErrorActivity.kt
+++ b/samples/exercise-assessment/src/main/java/com/myscript/iink/samples/assessment/utils/ErrorActivity.kt
@@ -2,20 +2,17 @@
* Copyright (c) MyScript. All rights reserved.
*/
-package com.myscript.iink.app.common.activities
+package com.myscript.iink.samples.assessment.utils
-import android.app.Application
import android.content.Context
-import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.os.Process
import android.text.method.ScrollingMovementMethod
import android.widget.Button
import android.widget.TextView
-import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
-import com.myscript.iink.app.common.R
+import com.myscript.iink.samples.assessment.R
import java.io.PrintWriter
import java.io.StringWriter
import kotlin.system.exitProcess
@@ -61,7 +58,7 @@ class ErrorActivity : AppCompatActivity() {
}
}
- private class ExceptionHandler internal constructor(private val context: Context) : Thread.UncaughtExceptionHandler {
+ private class ExceptionHandler(private val context: Context) : Thread.UncaughtExceptionHandler {
override fun uncaughtException(t: Thread, e: Throwable) {
// get message from the root cause.
var root: Throwable? = e
@@ -79,7 +76,7 @@ class ErrorActivity : AppCompatActivity() {
val intent = Intent(context, ErrorActivity::class.java)
intent.putExtra(ERR_TITLE, message)
intent.putExtra(ERR_MESSAGE, trace)
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK;
context.startActivity(intent)
// kill the current activity.
Process.killProcess(Process.myPid())
diff --git a/java/common-kotlin/src/main/java/com/myscript/iink/app/common/utils/LockableScrollView.kt b/samples/exercise-assessment/src/main/java/com/myscript/iink/samples/assessment/utils/LockableScrollView.kt
similarity index 97%
rename from java/common-kotlin/src/main/java/com/myscript/iink/app/common/utils/LockableScrollView.kt
rename to samples/exercise-assessment/src/main/java/com/myscript/iink/samples/assessment/utils/LockableScrollView.kt
index a5e9598..014f103 100644
--- a/java/common-kotlin/src/main/java/com/myscript/iink/app/common/utils/LockableScrollView.kt
+++ b/samples/exercise-assessment/src/main/java/com/myscript/iink/samples/assessment/utils/LockableScrollView.kt
@@ -2,7 +2,7 @@
* Copyright (c) MyScript. All rights reserved.
*/
-package com.myscript.iink.app.common.utils
+package com.myscript.iink.samples.assessment.utils
import android.content.Context
import android.util.AttributeSet
diff --git a/samples/exercise-assessment/src/main/res/drawable-v24/ic_launcher_foreground.xml b/samples/exercise-assessment/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..aa654c2
--- /dev/null
+++ b/samples/exercise-assessment/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/java/samples/exercise-assessment-kt/src/main/res/drawable/ic_answer.xml b/samples/exercise-assessment/src/main/res/drawable/ic_answer.xml
similarity index 100%
rename from java/samples/exercise-assessment-kt/src/main/res/drawable/ic_answer.xml
rename to samples/exercise-assessment/src/main/res/drawable/ic_answer.xml
diff --git a/java/samples/exercise-assessment-kt/src/main/res/drawable/ic_clear.xml b/samples/exercise-assessment/src/main/res/drawable/ic_clear.xml
similarity index 100%
rename from java/samples/exercise-assessment-kt/src/main/res/drawable/ic_clear.xml
rename to samples/exercise-assessment/src/main/res/drawable/ic_clear.xml
diff --git a/java/samples/exercise-assessment-kt/src/main/res/drawable/ic_convert.xml b/samples/exercise-assessment/src/main/res/drawable/ic_convert.xml
similarity index 100%
rename from java/samples/exercise-assessment-kt/src/main/res/drawable/ic_convert.xml
rename to samples/exercise-assessment/src/main/res/drawable/ic_convert.xml
diff --git a/java/samples/exercise-assessment-kt/src/main/res/drawable/ic_equation.xml b/samples/exercise-assessment/src/main/res/drawable/ic_equation.xml
similarity index 100%
rename from java/samples/exercise-assessment-kt/src/main/res/drawable/ic_equation.xml
rename to samples/exercise-assessment/src/main/res/drawable/ic_equation.xml
diff --git a/samples/exercise-assessment/src/main/res/drawable/ic_launcher_background.xml b/samples/exercise-assessment/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..85efc6f
--- /dev/null
+++ b/samples/exercise-assessment/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/java/samples/exercise-assessment-kt/src/main/res/drawable/ic_num_1.xml b/samples/exercise-assessment/src/main/res/drawable/ic_num_1.xml
similarity index 100%
rename from java/samples/exercise-assessment-kt/src/main/res/drawable/ic_num_1.xml
rename to samples/exercise-assessment/src/main/res/drawable/ic_num_1.xml
diff --git a/java/samples/exercise-assessment-kt/src/main/res/drawable/ic_num_2.xml b/samples/exercise-assessment/src/main/res/drawable/ic_num_2.xml
similarity index 100%
rename from java/samples/exercise-assessment-kt/src/main/res/drawable/ic_num_2.xml
rename to samples/exercise-assessment/src/main/res/drawable/ic_num_2.xml
diff --git a/java/samples/exercise-assessment-kt/src/main/res/drawable/ic_num_3.xml b/samples/exercise-assessment/src/main/res/drawable/ic_num_3.xml
similarity index 100%
rename from java/samples/exercise-assessment-kt/src/main/res/drawable/ic_num_3.xml
rename to samples/exercise-assessment/src/main/res/drawable/ic_num_3.xml
diff --git a/java/samples/exercise-assessment-kt/src/main/res/drawable/ic_num_4.xml b/samples/exercise-assessment/src/main/res/drawable/ic_num_4.xml
similarity index 100%
rename from java/samples/exercise-assessment-kt/src/main/res/drawable/ic_num_4.xml
rename to samples/exercise-assessment/src/main/res/drawable/ic_num_4.xml
diff --git a/java/samples/exercise-assessment-kt/src/main/res/drawable/ic_num_5.xml b/samples/exercise-assessment/src/main/res/drawable/ic_num_5.xml
similarity index 100%
rename from java/samples/exercise-assessment-kt/src/main/res/drawable/ic_num_5.xml
rename to samples/exercise-assessment/src/main/res/drawable/ic_num_5.xml
diff --git a/java/samples/exercise-assessment-kt/src/main/res/drawable/ic_redo.xml b/samples/exercise-assessment/src/main/res/drawable/ic_redo.xml
similarity index 100%
rename from java/samples/exercise-assessment-kt/src/main/res/drawable/ic_redo.xml
rename to samples/exercise-assessment/src/main/res/drawable/ic_redo.xml
diff --git a/java/samples/exercise-assessment-kt/src/main/res/drawable/ic_twolinearequation.xml b/samples/exercise-assessment/src/main/res/drawable/ic_twolinearequation.xml
similarity index 100%
rename from java/samples/exercise-assessment-kt/src/main/res/drawable/ic_twolinearequation.xml
rename to samples/exercise-assessment/src/main/res/drawable/ic_twolinearequation.xml
diff --git a/java/samples/exercise-assessment-kt/src/main/res/drawable/ic_undo.xml b/samples/exercise-assessment/src/main/res/drawable/ic_undo.xml
similarity index 100%
rename from java/samples/exercise-assessment-kt/src/main/res/drawable/ic_undo.xml
rename to samples/exercise-assessment/src/main/res/drawable/ic_undo.xml
diff --git a/samples/exercise-assessment/src/main/res/layout/activity_error.xml b/samples/exercise-assessment/src/main/res/layout/activity_error.xml
new file mode 100644
index 0000000..37bdb21
--- /dev/null
+++ b/samples/exercise-assessment/src/main/res/layout/activity_error.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/java/samples/exercise-assessment-kt/src/main/res/layout/activity_main.xml b/samples/exercise-assessment/src/main/res/layout/activity_main.xml
similarity index 98%
rename from java/samples/exercise-assessment-kt/src/main/res/layout/activity_main.xml
rename to samples/exercise-assessment/src/main/res/layout/activity_main.xml
index c759604..7fc6c7c 100644
--- a/java/samples/exercise-assessment-kt/src/main/res/layout/activity_main.xml
+++ b/samples/exercise-assessment/src/main/res/layout/activity_main.xml
@@ -11,7 +11,7 @@
android:layout_height="match_parent"
tools:context=".MainActivity">
-
-
+
diff --git a/java/samples/exercise-assessment-kt/src/main/res/menu/activity_main.xml b/samples/exercise-assessment/src/main/res/menu/activity_main.xml
similarity index 100%
rename from java/samples/exercise-assessment-kt/src/main/res/menu/activity_main.xml
rename to samples/exercise-assessment/src/main/res/menu/activity_main.xml
diff --git a/samples/exercise-assessment/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/samples/exercise-assessment/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..96525d0
--- /dev/null
+++ b/samples/exercise-assessment/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/samples/exercise-assessment/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/samples/exercise-assessment/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..96525d0
--- /dev/null
+++ b/samples/exercise-assessment/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/samples/exercise-assessment/src/main/res/mipmap-hdpi/ic_launcher.png b/samples/exercise-assessment/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a98a138
Binary files /dev/null and b/samples/exercise-assessment/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/samples/exercise-assessment/src/main/res/mipmap-mdpi/ic_launcher.png b/samples/exercise-assessment/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..34ca9b3
Binary files /dev/null and b/samples/exercise-assessment/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/samples/exercise-assessment/src/main/res/mipmap-xhdpi/ic_launcher.png b/samples/exercise-assessment/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..4b3eb36
Binary files /dev/null and b/samples/exercise-assessment/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/samples/exercise-assessment/src/main/res/mipmap-xxhdpi/ic_launcher.png b/samples/exercise-assessment/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..bbea1d0
Binary files /dev/null and b/samples/exercise-assessment/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/samples/exercise-assessment/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/samples/exercise-assessment/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..36639e3
Binary files /dev/null and b/samples/exercise-assessment/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/java/samples/exercise-assessment-kt/src/main/res/values-night/themes.xml b/samples/exercise-assessment/src/main/res/values-night/themes.xml
similarity index 100%
rename from java/samples/exercise-assessment-kt/src/main/res/values-night/themes.xml
rename to samples/exercise-assessment/src/main/res/values-night/themes.xml
diff --git a/java/samples/exercise-assessment-kt/src/main/res/values/colors.xml b/samples/exercise-assessment/src/main/res/values/colors.xml
similarity index 100%
rename from java/samples/exercise-assessment-kt/src/main/res/values/colors.xml
rename to samples/exercise-assessment/src/main/res/values/colors.xml
diff --git a/java/samples/exercise-assessment-kt/src/main/res/values/dimens.xml b/samples/exercise-assessment/src/main/res/values/dimens.xml
similarity index 81%
rename from java/samples/exercise-assessment-kt/src/main/res/values/dimens.xml
rename to samples/exercise-assessment/src/main/res/values/dimens.xml
index 5419f2f..cea5c28 100644
--- a/java/samples/exercise-assessment-kt/src/main/res/values/dimens.xml
+++ b/samples/exercise-assessment/src/main/res/values/dimens.xml
@@ -4,6 +4,6 @@
16dp
- 64dp
+ 0dp
24dp
diff --git a/java/samples/exercise-assessment-kt/src/main/res/values/strings.xml b/samples/exercise-assessment/src/main/res/values/strings.xml
similarity index 100%
rename from java/samples/exercise-assessment-kt/src/main/res/values/strings.xml
rename to samples/exercise-assessment/src/main/res/values/strings.xml
diff --git a/java/samples/exercise-assessment-kt/src/main/res/values/themes.xml b/samples/exercise-assessment/src/main/res/values/themes.xml
similarity index 100%
rename from java/samples/exercise-assessment-kt/src/main/res/values/themes.xml
rename to samples/exercise-assessment/src/main/res/values/themes.xml
diff --git a/java/gradle.properties b/samples/gradle.properties
similarity index 100%
rename from java/gradle.properties
rename to samples/gradle.properties
diff --git a/samples/gradle/wrapper/gradle-wrapper.jar b/samples/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..2c35211
Binary files /dev/null and b/samples/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/java/gradle/wrapper/gradle-wrapper.properties b/samples/gradle/wrapper/gradle-wrapper.properties
similarity index 66%
rename from java/gradle/wrapper/gradle-wrapper.properties
rename to samples/gradle/wrapper/gradle-wrapper.properties
index 12d9434..09523c0 100644
--- a/java/gradle/wrapper/gradle-wrapper.properties
+++ b/samples/gradle/wrapper/gradle-wrapper.properties
@@ -1,9 +1,7 @@
-#
-# Copyright (c) MyScript. All rights reserved.
-#
-#Wed Jan 05 23:43:02 KST 2022
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
\ No newline at end of file
diff --git a/samples/gradlew b/samples/gradlew
new file mode 100755
index 0000000..f5feea6
--- /dev/null
+++ b/samples/gradlew
@@ -0,0 +1,252 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
+' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/java/gradlew.bat b/samples/gradlew.bat
similarity index 75%
rename from java/gradlew.bat
rename to samples/gradlew.bat
index a22658f..9d21a21 100644
--- a/java/gradlew.bat
+++ b/samples/gradlew.bat
@@ -13,8 +13,10 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
-@if "%DEBUG%" == "" @echo off
+@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +27,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -33,20 +36,20 @@ set APP_HOME=%DIRNAME%
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
+if %ERRORLEVEL% equ 0 goto execute
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
goto fail
@@ -54,48 +57,36 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-if exist "%JAVA_EXE%" goto init
+if exist "%JAVA_EXE%" goto execute
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
goto fail
-:init
-@rem Get command-line arguments, handling Windows variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
+if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
diff --git a/samples/hwgeneration/.gitignore b/samples/hwgeneration/.gitignore
new file mode 100644
index 0000000..d1bb52f
--- /dev/null
+++ b/samples/hwgeneration/.gitignore
@@ -0,0 +1,5 @@
+build/
+.gradle/
+.idea/
+assets/
+.DS_Store
\ No newline at end of file
diff --git a/samples/hwgeneration/build.gradle b/samples/hwgeneration/build.gradle
new file mode 100644
index 0000000..b1cb602
--- /dev/null
+++ b/samples/hwgeneration/build.gradle
@@ -0,0 +1,38 @@
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+}
+
+android {
+ namespace 'com.myscript.iink.samples.hwgeneration'
+
+ compileSdk Versions.compileSdk
+
+ buildFeatures {
+ viewBinding true
+ buildConfig true
+ }
+
+ defaultConfig {
+ minSdk Versions.minSdk
+ targetSdk Versions.targetSdk
+
+ applicationId 'com.myscript.iink.samples.hwgeneration'
+ versionCode project.ext.iinkVersionCode
+ versionName project.ext.iinkVersionName
+
+ vectorDrawables.useSupportLibrary true
+ }
+}
+
+dependencies {
+ implementation "androidx.core:core-ktx:${Versions.androidx_core}"
+ implementation "androidx.activity:activity-ktx:${Versions.androidx_activity}"
+ implementation "androidx.appcompat:appcompat:${Versions.appcompat}"
+ implementation "com.google.android.material:material:${Versions.material}"
+
+ implementation project(':UIReferenceImplementation')
+ implementation project(':myscript-certificate')
+
+ implementation "com.google.code.gson:gson:${Versions.gson}"
+}
\ No newline at end of file
diff --git a/samples/hwgeneration/src/main/AndroidManifest.xml b/samples/hwgeneration/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..f3fc842
--- /dev/null
+++ b/samples/hwgeneration/src/main/AndroidManifest.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/hwgeneration/src/main/assets/parts/interactivity.json b/samples/hwgeneration/src/main/assets/parts/interactivity.json
new file mode 100644
index 0000000..e9e14c3
--- /dev/null
+++ b/samples/hwgeneration/src/main/assets/parts/interactivity.json
@@ -0,0 +1,34 @@
+{
+ "raw-content": {
+ "session-time": 100,
+ "classification": {
+ "types": [ "text", "shape", "drawing" ]
+ },
+ "recognition": {
+ "types": [ "text", "shape", "math" ]
+ },
+ "convert": {
+ "shape-on-hold": true,
+ "types": [ "text", "shape", "math" ]
+ },
+ "shape": {
+ "snap-axis": [ "triangle", "rectangle", "rhombus", "parallelogram", "ellipse" ]
+ },
+ "eraser": {
+ "erase-precisely": false,
+ "dynamic-radius": true
+ },
+ "auto-connection": true,
+ "interactive-blocks": {
+ "feedback": [ "math" ]
+ },
+ "pen": {
+ "gestures": [ "scratch-out", "strike-through", "surround", "long-press" ]
+ },
+ "line-pattern": "grid",
+ "guides": {
+ "show": [ "alignment", "text", "square", "square-inside", "image-aspect-ratio", "rotation" ],
+ "snap": [ "alignment", "text", "square", "square-inside", "image-aspect-ratio", "rotation" ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/hwgeneration/src/main/java/com/myscript/iink/samples/hwgeneration/EditorViewModel.kt b/samples/hwgeneration/src/main/java/com/myscript/iink/samples/hwgeneration/EditorViewModel.kt
new file mode 100644
index 0000000..c4a29c1
--- /dev/null
+++ b/samples/hwgeneration/src/main/java/com/myscript/iink/samples/hwgeneration/EditorViewModel.kt
@@ -0,0 +1,315 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.samples.hwgeneration
+
+import android.app.Application
+import android.view.View
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.myscript.iink.ContentPackage
+import com.myscript.iink.ContentPart
+import com.myscript.iink.ContentSelection
+import com.myscript.iink.Editor
+import com.myscript.iink.EditorError
+import com.myscript.iink.Engine
+import com.myscript.iink.GestureAction
+import com.myscript.iink.IEditorListener
+import com.myscript.iink.IGestureHandler
+import com.myscript.iink.NativeObjectHandle
+import com.myscript.iink.PointerEvent
+import com.myscript.iink.PointerEventType
+import com.myscript.iink.PointerTool
+import com.myscript.iink.PointerType
+import com.myscript.iink.graphics.Transform
+import com.myscript.iink.uireferenceimplementation.EditorBinding
+import com.myscript.iink.uireferenceimplementation.EditorView
+import com.myscript.iink.uireferenceimplementation.FontUtils
+import com.myscript.iink.uireferenceimplementation.InputController
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.File
+import java.io.IOException
+import java.io.Reader
+
+data class PartHistoryState(val canUndo: Boolean = false, val canRedo: Boolean = false)
+
+data class OnLongPress(val show: Boolean = false, val x: Float = 0f, val y: Float = 0f)
+
+class EditorViewModel(application: Application) : AndroidViewModel(application) {
+
+ private val _partHistoryState = MutableLiveData(PartHistoryState())
+ val partHistoryState: LiveData
+ get() = _partHistoryState
+
+ private val _isWriting = MutableLiveData(false)
+ val isWriting: LiveData
+ get() = _isWriting
+
+ private val _onLongPress = MutableLiveData(OnLongPress())
+ val onLongPress: LiveData
+ get() = _onLongPress
+
+ private val _isSelectionMode = MutableLiveData(false)
+ val isSelectionMode: LiveData
+ get() = _isSelectionMode
+
+ private val _selection = MutableLiveData(null)
+ val selection: LiveData
+ get() = _selection
+
+ private val editorListener: IEditorListener = object : IEditorListener {
+ private fun notifyUndoRedo(editor: Editor) {
+ val canUndo = editor.canUndo()
+ val canRedo = editor.canRedo()
+ viewModelScope.launch(Dispatchers.Main) {
+ _partHistoryState.value = PartHistoryState(canUndo, canRedo)
+ }
+ }
+
+ override fun partChanging(editor: Editor, oldPart: ContentPart?, newPart: ContentPart?) = Unit
+ override fun partChanged(editor: Editor) {
+ notifyUndoRedo(editor)
+ }
+
+ override fun contentChanged(editor: Editor, blockIds: Array) {
+ notifyUndoRedo(editor)
+ }
+
+ override fun onError(editor: Editor, blockId: String, err: EditorError, message: String) = Unit
+
+ override fun selectionChanged(editor: Editor) {
+ viewModelScope.launch(Dispatchers.Main) {
+ _selection.value = editor.selection
+ }
+ }
+
+ override fun activeBlockChanged(editor: Editor, blockId: String) = Unit
+ }
+
+ private val gestureHandler = object : IGestureHandler {
+ private fun handleGesture(): GestureAction {
+ return if (_isWriting.value == true) {
+ GestureAction.ADD_STROKE
+ } else {
+ GestureAction.APPLY_GESTURE
+ }
+ }
+
+ override fun onTap(editor: Editor, tool: PointerTool?, gestureStrokeId: String, x: Float, y: Float): GestureAction = handleGesture()
+ override fun onDoubleTap(editor: Editor, tool: PointerTool?, gestureStrokeIds: Array, x: Float, y: Float): GestureAction = handleGesture()
+
+ override fun onLongPress(editor: Editor, tool: PointerTool?, gestureStrokeId: String, x: Float, y: Float): GestureAction {
+ val handleGesture = handleGesture()
+ if (handleGesture != GestureAction.IGNORE) {
+ viewModelScope.launch(Dispatchers.Main) {
+ _onLongPress.value = OnLongPress(true, x, y)
+ }
+ }
+ return handleGesture
+ }
+
+ override fun onUnderline(editor: Editor, tool: PointerTool?, gestureStrokeId: String, selection: NativeObjectHandle): GestureAction = handleGesture()
+ override fun onSurround(editor: Editor, tool: PointerTool?, gestureStrokeId: String, selection: NativeObjectHandle): GestureAction = handleGesture()
+ override fun onJoin(editor: Editor, tool: PointerTool?, gestureStrokeId: String, before: NativeObjectHandle, after: NativeObjectHandle): GestureAction = handleGesture()
+ override fun onInsert(editor: Editor, tool: PointerTool?, gestureStrokeId: String, before: NativeObjectHandle, after: NativeObjectHandle): GestureAction = handleGesture()
+ override fun onStrikethrough(editor: Editor, tool: PointerTool?, gestureStrokeId: String, selection: NativeObjectHandle): GestureAction = handleGesture()
+ override fun onScratch(editor: Editor, tool: PointerTool?, gestureStrokeId: String, selection: NativeObjectHandle): GestureAction = handleGesture()
+ }
+
+ private var contentPackage: ContentPackage? = null
+ private var contentPart: ContentPart? = null
+ private var editor: Editor? = null
+
+ fun openEditor(engine: Engine, editorView: EditorView) {
+ val context = editorView.context.applicationContext
+
+ val typefaceMap = FontUtils.loadFontsFromAssets(context.assets)
+ editorView.setTypefaces(typefaceMap)
+
+ val editorBinding = EditorBinding(engine, typefaceMap)
+ val editorData = editorBinding.openEditor(editorView)
+
+ val editor = requireNotNull(editorData.editor)
+ this.editor = editor
+
+ editorData.inputController?.inputMode = InputController.INPUT_MODE_AUTO
+
+ val file = File(context.filesDir, "file.iink")
+
+ contentPackage = try {
+ engine.createPackage(file)
+ } catch (e: IOException) {
+ engine.openPackage(file)
+ }
+ contentPart = if (contentPackage?.partCount == 0) {
+ contentPackage?.createPart("Raw Content")
+ } else {
+ contentPackage?.getPart(0)
+ }
+
+ editor.addListener(editorListener)
+
+ // wait for view size initialization before setting part
+ editorView.post {
+ editorView.let { editorView ->
+ editorView.renderer?.let { renderer ->
+ renderer.setViewOffset(0f, 0f)
+ renderer.viewScale = 1f
+ editorView.visibility = View.VISIBLE
+
+ val configuration = context.assets.open("parts/interactivity.json").bufferedReader().use(Reader::readText)
+ editor.configuration.inject(configuration)
+ editor.part = contentPart
+ editor.setGestureHandler(gestureHandler)
+
+ setSelectionMode(false)
+ }
+ }
+ }
+ }
+
+ fun closeEditor() {
+ contentPackage?.save()
+
+ editor?.let {
+ it.renderer.close()
+ it.setGestureHandler(null)
+ it.removeListener(editorListener)
+ it.close()
+ }
+
+ contentPart?.close()
+ contentPackage?.close()
+
+ reset()
+ }
+
+ private fun reset() {
+ synchronized(toWrite) {
+ toWrite.clear()
+ }
+ viewModelScope.launch(Dispatchers.Main) {
+ _isWriting.value = false
+ }
+ }
+
+ override fun onCleared() {
+ closeEditor()
+ super.onCleared()
+ }
+
+ fun undo() {
+ editor?.undo()
+ }
+
+ fun redo() {
+ editor?.redo()
+ }
+
+ fun clear() {
+ editor?.clear()
+ }
+
+ fun setSelectionMode(enabled: Boolean) {
+ if (enabled) {
+ editor?.toolController?.setToolForType(PointerType.PEN, PointerTool.SELECTOR)
+ } else {
+ editor?.toolController?.setToolForType(PointerType.PEN, PointerTool.PEN)
+ editor?.setSelection(null)
+ }
+ viewModelScope.launch(Dispatchers.Main) {
+ _isSelectionMode.value = enabled
+ }
+ }
+
+ fun isEmpty(): Boolean {
+ return editor?.isEmpty(null) == true
+ }
+
+ fun transform(): Transform {
+ return editor?.renderer?.viewTransform ?: Transform()
+ }
+
+ private val toWrite = mutableListOf>()
+
+ fun write(eventsToStack: Array) {
+ val editor = requireNotNull(editor)
+
+ synchronized(toWrite) {
+ toWrite.add(eventsToStack)
+ }
+
+ if (_isWriting.value == true) return
+
+ viewModelScope.launch(Dispatchers.Default) {
+ setSelectionMode(false)
+
+ withContext(Dispatchers.Main) {
+ _isWriting.value = true
+ }
+
+ while(synchronized(toWrite) { toWrite.isNotEmpty() }) {
+ val events = synchronized(toWrite) {
+ toWrite.removeAt(0)
+ }
+ try {
+ events.forEachIndexed { index, pointerEvent ->
+ withContext(Dispatchers.Main) {
+ when (pointerEvent.eventType) {
+ PointerEventType.DOWN -> {
+ editor.pointerDown(pointerEvent.x, pointerEvent.y, pointerEvent.t, pointerEvent.f, pointerEvent.pointerType, pointerEvent.pointerId)
+ }
+ PointerEventType.MOVE -> {
+ editor.pointerMove(pointerEvent.x, pointerEvent.y, pointerEvent.t, pointerEvent.f, pointerEvent.pointerType, pointerEvent.pointerId)
+ }
+ PointerEventType.UP -> {
+ editor.pointerUp(pointerEvent.x, pointerEvent.y, pointerEvent.t, pointerEvent.f, pointerEvent.pointerType, pointerEvent.pointerId)
+ }
+ PointerEventType.CANCEL -> {
+ editor.pointerCancel(pointerEvent.pointerId)
+ }
+ }
+ }
+
+ val delay = if (events.size < index + 1) {
+ events[index + 1].t - pointerEvent.t
+ } else 10
+ Thread.sleep(delay)
+ }
+ } catch (e: IllegalStateException) {
+ reset()
+ return@launch
+ }
+ }
+
+ withContext(Dispatchers.Main) {
+ _isWriting.value = false
+ }
+ }
+ }
+
+ fun saveAs(file: File) {
+ contentPackage?.saveAs(file)
+ }
+
+ fun onLongPressConsummed() {
+ _onLongPress.value = OnLongPress()
+ }
+}
+
+class EditorViewModelFactory(private val application: Application) : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T {
+ return when {
+ modelClass.isAssignableFrom(EditorViewModel::class.java) -> {
+ EditorViewModel(application) as T
+ }
+ else -> throw IllegalArgumentException("Unknown ViewModel $modelClass")
+ }
+ }
+}
diff --git a/samples/hwgeneration/src/main/java/com/myscript/iink/samples/hwgeneration/GenerationViewModel.kt b/samples/hwgeneration/src/main/java/com/myscript/iink/samples/hwgeneration/GenerationViewModel.kt
new file mode 100644
index 0000000..48d8fe7
--- /dev/null
+++ b/samples/hwgeneration/src/main/java/com/myscript/iink/samples/hwgeneration/GenerationViewModel.kt
@@ -0,0 +1,270 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.samples.hwgeneration
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.myscript.iink.ContentSelection
+import com.myscript.iink.Engine
+import com.myscript.iink.HandwritingGenerator
+import com.myscript.iink.HandwritingGeneratorError
+import com.myscript.iink.HandwritingResult
+import com.myscript.iink.IHandwritingGeneratorListener
+import com.myscript.iink.MimeType
+import com.myscript.iink.PointerEvent
+import com.myscript.iink.PredefinedHandwritingProfileId
+import com.myscript.iink.graphics.Transform
+import com.myscript.iink.samples.hwgeneration.None.word
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.File
+import java.util.UUID
+
+open class HWRResult(val word: String, val events: Array)
+object None: HWRResult("", emptyArray())
+
+data class Message(
+ val type: Type,
+ val message: String,
+ val exception: Exception? = null,
+) {
+ enum class Type { NOTIFICATION, ERROR }
+
+ internal val id = UUID.randomUUID()
+}
+
+data class GenerationProfile(
+ val id: PredefinedHandwritingProfileId = PredefinedHandwritingProfileId.DEFAULT,
+ val profilePath: String? = null) {
+
+ companion object {
+ fun fromId(id: PredefinedHandwritingProfileId): GenerationProfile {
+ return GenerationProfile(id, null)
+ }
+
+ fun fromPath(path: String): GenerationProfile {
+ return GenerationProfile(PredefinedHandwritingProfileId.DEFAULT, path)
+ }
+ }
+}
+
+class GenerationViewModel(application: Application, private val engine: Engine) : AndroidViewModel(application) {
+
+ private val generator: HandwritingGenerator
+
+ private val _isGenerating = MutableLiveData(false)
+ val isGenerating: LiveData
+ get() = _isGenerating
+
+ private val _isProfileBuilding = MutableLiveData(false)
+ val isProfileBuilding: LiveData
+ get() = _isProfileBuilding
+
+ private val _hwrResults = MutableLiveData>()
+ val hwrResults: LiveData>
+ get() = _hwrResults
+
+ private var _message = MutableLiveData(null)
+ val message: LiveData
+ get() = _message
+
+ init {
+ _isGenerating.value = false
+
+ generator = engine.createHandwritingGenerator()
+ }
+
+ private interface HWResultListener {
+ fun onResult(result: HWRResult)
+ }
+
+ fun cancel() {
+ generator.cancel()
+ viewModelScope.launch(Dispatchers.Main) {
+ _isGenerating.value = false
+ _hwrResults.value = emptyList()
+ }
+ }
+
+ fun getProfiles(): List {
+ val userProfiles = File(getApplication().filesDir, PROFILE_FOLDER).listFiles()?.map { file ->
+ GenerationProfile.fromPath(file.absolutePath)
+ } ?: emptyList()
+
+ val predefinedProfiles = PredefinedHandwritingProfileId.entries.toList().map {
+ GenerationProfile.fromId(it)
+ }
+
+ return userProfiles + predefinedProfiles
+ }
+
+ fun buildProfile(iinkFile: File) {
+ buildProfileInternal(iinkFile = iinkFile)
+ }
+
+ fun buildProfile(selection: ContentSelection) {
+ buildProfileInternal(selection = selection)
+ }
+
+ private fun buildProfileInternal(selection: ContentSelection? = null, iinkFile: File? = null) {
+ viewModelScope.launch(Dispatchers.Main) {
+ _isProfileBuilding.value = true
+
+ withContext(Dispatchers.IO) {
+ val builder = generator.createHandwritingProfileBuilder()
+
+ val profile = try {
+ if (selection != null) {
+ builder.createFromSelection(selection)
+ } else if (iinkFile != null && iinkFile.exists()) {
+ builder.createFromFile(iinkFile.absolutePath)
+ } else {
+ return@withContext
+ }
+ } catch (e: IllegalArgumentException) {
+ notify(Message(Message.Type.ERROR, "Error while building profile ${e.message}", e))
+ return@withContext
+ }
+
+ val profileDirectory = File(getApplication().filesDir, PROFILE_FOLDER)
+ if (!profileDirectory.exists()) {
+ profileDirectory.mkdirs()
+ }
+ val profileId = UUID.randomUUID()
+ val profileFile = File(profileDirectory, "$profileId.profile")
+ builder.store(profile, profileFile.absolutePath)
+
+ notify(Message(Message.Type.NOTIFICATION, "Profile built with ID $profileId"))
+ }
+
+ _isProfileBuilding.value = false
+ }
+ }
+
+ fun generateHandwriting(inputSentence: String, generationProfile: GenerationProfile, inputTextSize: Float, inputX: Float, inputY: Float, width: Float, transform: Transform) {
+ if (inputSentence.isEmpty()) return
+
+ viewModelScope.launch(Dispatchers.Main) {
+ _isGenerating.value = true
+
+ withContext(Dispatchers.IO) {
+ generate(inputSentence, generationProfile, inputTextSize, inputX, inputY, width, transform, object : HWResultListener {
+ override fun onResult(result: HWRResult) {
+ viewModelScope.launch(Dispatchers.Main) {
+ val previousList: MutableList = _hwrResults.value?.toMutableList() ?: mutableListOf()
+ previousList.add(result)
+ _hwrResults.value = previousList
+ }
+ }
+ })
+ }
+ _isGenerating.value = false
+ }
+ }
+
+ private fun generate(sentence: String, generationProfile: GenerationProfile, textSize: Float, xOffset: Float, yOffset: Float, width: Float, transform: Transform, listener: HWResultListener): HWRResult {
+ val builder = generator.createHandwritingProfileBuilder()
+
+ val profile = if (generationProfile.profilePath != null && File(generationProfile.profilePath).exists()) {
+ builder.load(generationProfile.profilePath)
+ } else {
+ builder.createFromId(generationProfile.id)
+ }
+
+ val indexLock = Any()
+ var currentPointerEventIndex = 0
+ var wordIndex = 0
+
+ val words = sentence.split(" ").filter { it.isNotEmpty() }
+
+ generator.setListener(object: IHandwritingGeneratorListener {
+ override fun onPartialResult(generator: HandwritingGenerator, result: HandwritingResult) {
+ val allEvents = result.toPointerEvents(transform)
+ val newEvents = allEvents.drop(synchronized(indexLock) { currentPointerEventIndex })
+
+ listener.onResult(HWRResult(words[wordIndex], newEvents.toTypedArray()))
+
+ synchronized(indexLock) {
+ currentPointerEventIndex += newEvents.size
+ wordIndex++
+ }
+ }
+ override fun onEnd(generator: HandwritingGenerator) = Unit
+ override fun onError(generator: HandwritingGenerator, code: HandwritingGeneratorError, message: String) {
+ notify(Message(Message.Type.ERROR, "Error $code due to $message"))
+ }
+ override fun onUnsupportedCharacter(generator: HandwritingGenerator, label: String, character: String, index: Int) = Unit
+ })
+
+ generator.start("Text", profile, engine.createParameterSet().apply {
+ setBoolean("handwriting-generation.session.force-new-line", false)
+
+ val invertTransform = Transform(transform).apply {
+ invert()
+ }
+ val offsetMM = invertTransform.apply(xOffset, yOffset)
+ val widthMM = invertTransform.apply(width, 0f)
+ setNumber("handwriting-generation.session.width-mm", widthMM.x - offsetMM.x)
+ setNumber("handwriting-generation.session.left-x-mm", offsetMM.x)
+ setNumber("handwriting-generation.session.origin-y-mm", offsetMM.y)
+
+ setNumber("handwriting-generation.session.line-gap-mm", textSize * LINE_GAP_RATIO)
+ setNumber("handwriting-generation.session.x-height-mm", textSize)
+ })
+
+ words.forEach { word ->
+ generator.add(word, MimeType.TEXT)
+ }
+
+ generator.end()
+ generator.waitForIdle()
+
+ val result = generator.getResult()
+ val events = result.toPointerEvents(transform)
+
+ return HWRResult(word, events)
+ }
+
+ private fun notify(message: Message) {
+ synchronized(_message) {
+ viewModelScope.launch(Dispatchers.Main) {
+ _message.value = message
+ }
+ }
+ }
+
+ fun dismissMessage(message: Message) {
+ synchronized(_message) {
+ viewModelScope.launch(Dispatchers.Main) {
+ val currentError = _message.value
+ if (currentError?.id == message.id) {
+ _message.value = null
+ }
+ }
+ }
+ }
+
+ companion object {
+ private const val LINE_GAP_RATIO = 3
+
+ private const val PROFILE_FOLDER = "profiles"
+ }
+}
+
+class GenerationViewModelFactory(private val application: Application, private val engine: Engine) : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T {
+ return when {
+ modelClass.isAssignableFrom(GenerationViewModel::class.java) -> {
+ GenerationViewModel(application, engine) as T
+ }
+ else -> throw IllegalArgumentException("Unknown ViewModel $modelClass")
+ }
+ }
+}
diff --git a/samples/hwgeneration/src/main/java/com/myscript/iink/samples/hwgeneration/IInkApplication.kt b/samples/hwgeneration/src/main/java/com/myscript/iink/samples/hwgeneration/IInkApplication.kt
new file mode 100644
index 0000000..a88f191
--- /dev/null
+++ b/samples/hwgeneration/src/main/java/com/myscript/iink/samples/hwgeneration/IInkApplication.kt
@@ -0,0 +1,28 @@
+package com.myscript.iink.samples.hwgeneration
+
+import android.app.Application
+import com.myscript.certificate.MyCertificate
+import com.myscript.iink.Engine
+
+class IInkApplication : Application() {
+
+ companion object {
+ private var engine: Engine? = null
+
+ fun getEngine(): Engine? {
+ if (engine == null) {
+ engine = Engine.create(MyCertificate.getBytes())
+ }
+ return engine
+ }
+
+ fun close() {
+ engine?.close()
+ }
+ }
+
+ override fun onTerminate() {
+ close()
+ super.onTerminate()
+ }
+}
\ No newline at end of file
diff --git a/samples/hwgeneration/src/main/java/com/myscript/iink/samples/hwgeneration/MainActivity.kt b/samples/hwgeneration/src/main/java/com/myscript/iink/samples/hwgeneration/MainActivity.kt
new file mode 100644
index 0000000..2a65d98
--- /dev/null
+++ b/samples/hwgeneration/src/main/java/com/myscript/iink/samples/hwgeneration/MainActivity.kt
@@ -0,0 +1,442 @@
+// Copyright @ MyScript. All rights reserved.
+package com.myscript.iink.samples.hwgeneration
+
+import android.annotation.SuppressLint
+import android.app.ProgressDialog
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Build.VERSION_CODES
+import android.os.Bundle
+import android.text.Html
+import android.util.Log
+import android.view.ActionMode
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.BaseAdapter
+import android.widget.EditText
+import android.widget.ImageView
+import android.widget.Spinner
+import android.widget.TextView
+import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.documentfile.provider.DocumentFile
+import androidx.lifecycle.coroutineScope
+import com.google.android.material.slider.Slider
+import com.myscript.iink.Engine
+import com.myscript.iink.PredefinedHandwritingProfileId
+import com.myscript.iink.samples.hwgeneration.databinding.ActivityMainBinding
+import com.myscript.iink.uireferenceimplementation.EditorView
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.File
+import java.nio.file.Files
+import java.util.UUID
+
+
+class MainActivity : AppCompatActivity() {
+
+ private var engine: Engine? = null
+
+ private var editorView: EditorView? = null
+
+ private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
+
+ private val editorViewModel: EditorViewModel by viewModels {
+ EditorViewModelFactory(application)
+ }
+
+ private val generationViewModel: GenerationViewModel by viewModels {
+ GenerationViewModelFactory(application, requireNotNull(engine))
+ }
+
+ private val profileBuildingProgress by lazy {
+ @Suppress("DEPRECATION")
+ ProgressDialog(this).apply {
+ setMessage("Building Handwriting Profile...")
+ setCancelable(false)
+ }
+ }
+
+ private var actionMode: ActionMode? = null
+ private val actionModeCallback = object : ActionMode.Callback {
+ override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.action_menu, menu)
+ return true
+ }
+
+ override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
+ menu.findItem(R.id.action_menu_build_profile).isEnabled = generationViewModel.isProfileBuilding.value != true
+ return false
+ }
+
+ override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.action_menu_build_profile -> {
+ editorViewModel.selection.value?.let {
+ generationViewModel.buildProfile(it)
+ }
+ true
+ }
+ else -> false
+ }
+ }
+
+ override fun onDestroyActionMode(mode: ActionMode) {
+ editorViewModel.setSelectionMode(false)
+ }
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ engine = IInkApplication.getEngine()
+
+ if (engine == null) {
+ // The certificate provided is incorrect, you need to use the one provided by MyScript
+ AlertDialog.Builder(this)
+ .setTitle( getString(R.string.app_error_invalid_certificate_title))
+ .setMessage( getString(R.string.app_error_invalid_certificate_message))
+ .setPositiveButton(android.R.string.ok, null)
+ .show()
+ return
+ }
+
+ // configure recognition
+ engine?.configuration?.let { configuration ->
+ val confDir = "zip://$packageCodePath!/assets/conf"
+ val hwResDir = "zip://$packageCodePath!/assets/resources/handwriting_generation"
+ configuration.setStringArray("configuration-manager.search-path", arrayOf(confDir, hwResDir))
+ configuration.setString("content-package.temp-folder", File(filesDir, "tmp").absolutePath)
+ }
+
+ setContentView(binding.root)
+
+ supportActionBar?.title = getString(R.string.app_name)
+
+ editorView = findViewById(com.myscript.iink.uireferenceimplementation.R.id.editor_view)
+
+ engine?.let { engine ->
+ editorView?.let { editorView ->
+ editorViewModel.openEditor(engine, editorView)
+ }
+ }
+
+ editorViewModel.isWriting.observe(this) { isWriting ->
+ invalidateOptionsMenu()
+ binding.readOnlyLayer.visibility = if (isWriting) View.VISIBLE else View.GONE
+ }
+
+ editorViewModel.partHistoryState.observe(this) {
+ invalidateOptionsMenu()
+ }
+
+ editorViewModel.onLongPress.observe(this) { (happened, x, y) ->
+ if (happened) {
+ editorViewModel.onLongPressConsummed()
+ displayInput(x, y)
+ }
+ }
+
+ editorViewModel.isSelectionMode.observe(this) { isSelectionMode ->
+ if (isSelectionMode) {
+ actionMode = startActionMode(actionModeCallback)
+ } else {
+ if (actionMode != null) {
+ actionMode?.finish()
+ actionMode = null
+ }
+ }
+ }
+
+ try {
+ // Dummy check to verify handwriting generation resource and proper certificate.
+ val generationViewModel = this.generationViewModel
+ } catch (e: IllegalStateException) {
+ AlertDialog.Builder(this)
+ .setTitle(getString(R.string.app_error_missing_resource_title))
+ .setMessage(Html.fromHtml(getString(R.string.app_error_missing_resource_message)))
+ .setPositiveButton(android.R.string.ok, null)
+ .show()
+ return
+ }
+
+ generationViewModel.hwrResults.observe(this) { hwrResults ->
+ if (hwrResults.isNotEmpty()) {
+ editorViewModel.write(hwrResults.last().events)
+ }
+ }
+
+ generationViewModel.isProfileBuilding.observe(this) { isProfileBuilding ->
+ if (isProfileBuilding) {
+ if (!profileBuildingProgress.isShowing) {
+ profileBuildingProgress.show()
+ }
+
+ } else {
+ if (profileBuildingProgress.isShowing) {
+ profileBuildingProgress.dismiss()
+ }
+ actionMode?.finish()
+ }
+ invalidateOptionsMenu()
+ actionMode?.invalidate()
+ binding.readOnlyLayer.visibility = if (isProfileBuilding) View.VISIBLE else View.GONE
+ }
+
+ generationViewModel.message.observe(this) { message ->
+ if (message == null) {
+ return@observe
+ }
+ Log.d("MainActivity", message.toString(), message.exception)
+ when (message.type) {
+ Message.Type.ERROR ->
+ AlertDialog.Builder(this)
+ .setMessage(message.message)
+ .setPositiveButton(android.R.string.ok, null)
+ .show()
+ else -> Toast.makeText(applicationContext, message.message, Toast.LENGTH_LONG).show()
+ }
+ generationViewModel.dismissMessage(message)
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ menuInflater.inflate(R.menu.main_menu, menu)
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
+ val canUndo = editorViewModel.partHistoryState.value?.canUndo ?: false
+ val canRedo = editorViewModel.partHistoryState.value?.canRedo ?: false
+ val isWriting = editorViewModel.isWriting.value ?: false
+ val isProfileBuilding = try { generationViewModel.isProfileBuilding.value } catch (e: Exception) { false } ?: false
+ val isEditorEmpty = editorViewModel.isEmpty()
+
+ menu?.findItem(R.id.editor_menu_undo)?.isEnabled = !isProfileBuilding && !isWriting && canUndo
+ menu?.findItem(R.id.editor_menu_redo)?.isEnabled = !isProfileBuilding && !isWriting && canRedo
+ menu?.findItem(R.id.editor_menu_clear)?.isEnabled = !isProfileBuilding && !isWriting && !isEditorEmpty
+
+ menu?.findItem(R.id.editor_menu_save_as)?.isEnabled = !isProfileBuilding && !isWriting && !isEditorEmpty
+ menu?.findItem(R.id.editor_menu_go)?.isEnabled = !isProfileBuilding && !isWriting
+
+ menu?.findItem(R.id.editor_menu_build_profile_from_file)?.isEnabled = !isProfileBuilding && !isWriting
+ menu?.findItem(R.id.editor_menu_build_profile)?.isEnabled = !isProfileBuilding && !isWriting && !isEditorEmpty
+
+ return super.onPrepareOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.editor_menu_undo -> {
+ editorViewModel.undo()
+ true
+ }
+ R.id.editor_menu_redo -> {
+ editorViewModel.redo()
+ true
+ }
+ R.id.editor_menu_clear -> {
+ editorViewModel.clear()
+ true
+ }
+ R.id.editor_menu_go -> {
+ displayInput()
+ true
+ }
+ R.id.editor_menu_build_profile_from_file -> {
+ importRequest.launch("*/*")
+ true
+ }
+ R.id.editor_menu_save_as -> {
+ saveAs()
+ true
+ }
+ R.id.editor_menu_build_profile -> {
+ editorViewModel.setSelectionMode(true)
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ override fun onDestroy() {
+ generationViewModel.cancel()
+ editorViewModel.closeEditor()
+
+ editorView?.let {
+ it.setOnTouchListener(null)
+ it.editor = null
+ }
+
+ // IInkApplication has the ownership, do not close here
+ engine = null
+
+ super.onDestroy()
+ }
+
+ private fun saveAs() {
+ val exportedFile = File(cacheDir, EXPORTED_FILE_NAME)
+ editorViewModel.saveAs(exportedFile)
+
+ val i = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
+ type = "application/*"
+ addCategory(Intent.CATEGORY_OPENABLE)
+ putExtra(Intent.EXTRA_TITLE, exportedFile.name)
+ }
+
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.O) {
+ exportLauncher.launch(i)
+ } else {
+ Toast.makeText(applicationContext, "Not able to save file on this Android version", Toast.LENGTH_LONG).show()
+ }
+ }
+
+ @RequiresApi(VERSION_CODES.O)
+ val exportLauncher: ActivityResultLauncher =
+ registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) {
+ it.data?.data?.also { uri ->
+ val contentResolver = applicationContext.contentResolver
+ val inputFile = File(cacheDir, EXPORTED_FILE_NAME)
+ contentResolver.openOutputStream(uri)?.use { outputStream ->
+ Files.copy(inputFile.toPath(), outputStream)
+ }
+ }
+ }
+
+ private val importRequest = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
+ if (uri == null) return@registerForActivityResult
+ val mimeType = DocumentFile.fromSingleUri(this@MainActivity, uri)?.type ?: contentResolver.getType(uri)
+ when (mimeType) {
+ "binary/octet-stream",
+ "application/zip",
+ "application/octet-stream",
+ "application/binary",
+ "application/x-zip" -> lifecycle.coroutineScope.launch {
+ processUriFile(uri, File(cacheDir, "${UUID.randomUUID()}.iink")) { file ->
+ generationViewModel.buildProfile(file)
+ }
+ }
+ else -> Toast.makeText(applicationContext, "Not able to open file", Toast.LENGTH_LONG).show()
+
+ }
+ }
+
+ private suspend fun Context.processUriFile(uri: Uri, file: File, logic: (File) -> Unit) {
+ withContext(Dispatchers.IO) {
+ runCatching {
+ contentResolver.openInputStream(uri)?.use { inputStream ->
+ file.outputStream().use { outputStream ->
+ inputStream.copyTo(outputStream)
+ }
+ }
+ }
+ }
+ try {
+ logic(file)
+ } finally {
+ file.deleteOnExit()
+ }
+ }
+
+ private fun displayInput(x: Float = X_OFFSET_DEFAULT_PX, y: Float = Y_OFFSET_DEFAULT_PX) {
+ val dialogBuilder = AlertDialog.Builder(this)
+ val dialogView = layoutInflater.inflate(R.layout.dialog_input, null)
+ dialogBuilder.setView(dialogView)
+ dialogBuilder.setTitle("Handwriting Generation Input")
+
+ val inputText = dialogView.findViewById(R.id.text_input)
+
+ val spinner = dialogView.findViewById(R.id.profile_dropdown)
+ val adapter = StyleAdapter(this@MainActivity, generationViewModel.getProfiles())
+ spinner.setAdapter(adapter)
+
+ val inputTextSize = dialogView.findViewById(R.id.text_size)
+
+ dialogBuilder.setPositiveButton(android.R.string.ok) { _, _ ->
+ val textSize = inputTextSize.value
+ val profile = spinner.selectedItem as GenerationProfile
+ generationViewModel.generateHandwriting(inputText.text.toString().trim(), profile, textSize, x, y, editorView?.width?.toFloat() ?: 0f, editorViewModel.transform())
+ }
+ dialogBuilder.setNegativeButton(android.R.string.cancel, null)
+
+ val alertDialog = dialogBuilder.create()
+ alertDialog.show()
+ }
+
+ companion object {
+ private const val EXPORTED_FILE_NAME = "export.iink"
+
+ private const val X_OFFSET_DEFAULT_PX = 50f
+ private const val Y_OFFSET_DEFAULT_PX = 200f
+ }
+}
+
+class StyleAdapter(private val context: Context, private val profiles: List) : BaseAdapter() {
+
+ override fun getCount(): Int {
+ return profiles.size
+ }
+
+ override fun getItem(position: Int): GenerationProfile {
+ return profiles[position]
+ }
+
+ override fun getItemId(position: Int): Long {
+ return position.toLong()
+ }
+
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
+ val view = convertView ?: LayoutInflater.from(context).inflate(R.layout.style_spinner_item, parent, false)
+
+ val generationProfile = getItem(position)
+
+ val profileImage = view.findViewById(R.id.profile_image)
+ val profileText = view.findViewById(R.id.profile_text)
+
+ if (generationProfile.profilePath != null) {
+ profileImage.visibility = View.GONE
+ profileText.visibility = View.VISIBLE
+ profileText.text = File(generationProfile.profilePath).nameWithoutExtension
+ } else {
+ profileImage.setImageResource(when (generationProfile.id) {
+ PredefinedHandwritingProfileId.DEFAULT -> R.drawable.generation_0
+ PredefinedHandwritingProfileId.PROFILE_1 -> R.drawable.generation_1
+ PredefinedHandwritingProfileId.PROFILE_2 -> R.drawable.generation_2
+ PredefinedHandwritingProfileId.PROFILE_3 -> R.drawable.generation_3
+ PredefinedHandwritingProfileId.PROFILE_4 -> R.drawable.generation_4
+ PredefinedHandwritingProfileId.PROFILE_5 -> R.drawable.generation_5
+ PredefinedHandwritingProfileId.PROFILE_6 -> R.drawable.generation_6
+ PredefinedHandwritingProfileId.PROFILE_7 -> R.drawable.generation_7
+ PredefinedHandwritingProfileId.PROFILE_8 -> R.drawable.generation_8
+ PredefinedHandwritingProfileId.PROFILE_9 -> R.drawable.generation_9
+ PredefinedHandwritingProfileId.PROFILE_10 -> R.drawable.generation_10
+ PredefinedHandwritingProfileId.PROFILE_11 -> R.drawable.generation_11
+ PredefinedHandwritingProfileId.PROFILE_12 -> R.drawable.generation_12
+ PredefinedHandwritingProfileId.PROFILE_13 -> R.drawable.generation_13
+ PredefinedHandwritingProfileId.PROFILE_14 -> R.drawable.generation_14
+ })
+ profileImage.visibility = View.VISIBLE
+ profileText.visibility = View.GONE
+ }
+
+ return view
+ }
+
+ override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup?): View {
+ return getView(position, convertView, parent)
+ }
+}
\ No newline at end of file
diff --git a/samples/hwgeneration/src/main/res/drawable-v24/ic_launcher_foreground.xml b/samples/hwgeneration/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..aa654c2
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/hwgeneration/src/main/res/drawable/generation_0.xml b/samples/hwgeneration/src/main/res/drawable/generation_0.xml
new file mode 100644
index 0000000..ea162b6
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable/generation_0.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
diff --git a/samples/hwgeneration/src/main/res/drawable/generation_1.xml b/samples/hwgeneration/src/main/res/drawable/generation_1.xml
new file mode 100644
index 0000000..592bef8
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable/generation_1.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/samples/hwgeneration/src/main/res/drawable/generation_10.xml b/samples/hwgeneration/src/main/res/drawable/generation_10.xml
new file mode 100644
index 0000000..9702b4d
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable/generation_10.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/samples/hwgeneration/src/main/res/drawable/generation_11.xml b/samples/hwgeneration/src/main/res/drawable/generation_11.xml
new file mode 100644
index 0000000..b3b7d7a
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable/generation_11.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/samples/hwgeneration/src/main/res/drawable/generation_12.xml b/samples/hwgeneration/src/main/res/drawable/generation_12.xml
new file mode 100644
index 0000000..fa17376
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable/generation_12.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/samples/hwgeneration/src/main/res/drawable/generation_13.xml b/samples/hwgeneration/src/main/res/drawable/generation_13.xml
new file mode 100644
index 0000000..04bf43d
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable/generation_13.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/samples/hwgeneration/src/main/res/drawable/generation_14.xml b/samples/hwgeneration/src/main/res/drawable/generation_14.xml
new file mode 100644
index 0000000..aa30405
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable/generation_14.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
diff --git a/samples/hwgeneration/src/main/res/drawable/generation_2.xml b/samples/hwgeneration/src/main/res/drawable/generation_2.xml
new file mode 100644
index 0000000..d0d2c68
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable/generation_2.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
diff --git a/samples/hwgeneration/src/main/res/drawable/generation_3.xml b/samples/hwgeneration/src/main/res/drawable/generation_3.xml
new file mode 100644
index 0000000..96e882d
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable/generation_3.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
diff --git a/samples/hwgeneration/src/main/res/drawable/generation_4.xml b/samples/hwgeneration/src/main/res/drawable/generation_4.xml
new file mode 100644
index 0000000..950a8ec
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable/generation_4.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
diff --git a/samples/hwgeneration/src/main/res/drawable/generation_5.xml b/samples/hwgeneration/src/main/res/drawable/generation_5.xml
new file mode 100644
index 0000000..03dd6f7
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable/generation_5.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
diff --git a/samples/hwgeneration/src/main/res/drawable/generation_6.xml b/samples/hwgeneration/src/main/res/drawable/generation_6.xml
new file mode 100644
index 0000000..8584568
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable/generation_6.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/samples/hwgeneration/src/main/res/drawable/generation_7.xml b/samples/hwgeneration/src/main/res/drawable/generation_7.xml
new file mode 100644
index 0000000..a7a1b0f
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable/generation_7.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/samples/hwgeneration/src/main/res/drawable/generation_8.xml b/samples/hwgeneration/src/main/res/drawable/generation_8.xml
new file mode 100644
index 0000000..4c14b59
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable/generation_8.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
diff --git a/samples/hwgeneration/src/main/res/drawable/generation_9.xml b/samples/hwgeneration/src/main/res/drawable/generation_9.xml
new file mode 100644
index 0000000..592a3dc
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable/generation_9.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
diff --git a/samples/hwgeneration/src/main/res/drawable/ic_delete.xml b/samples/hwgeneration/src/main/res/drawable/ic_delete.xml
new file mode 100644
index 0000000..d88caee
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable/ic_delete.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/samples/hwgeneration/src/main/res/drawable/ic_go.xml b/samples/hwgeneration/src/main/res/drawable/ic_go.xml
new file mode 100644
index 0000000..917dca8
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable/ic_go.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/samples/hwgeneration/src/main/res/drawable/ic_launcher_background.xml b/samples/hwgeneration/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..85efc6f
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/samples/hwgeneration/src/main/res/drawable/ic_pen.xml b/samples/hwgeneration/src/main/res/drawable/ic_pen.xml
new file mode 100644
index 0000000..5fdcdd6
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable/ic_pen.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/hwgeneration/src/main/res/drawable/ic_redo.xml b/samples/hwgeneration/src/main/res/drawable/ic_redo.xml
new file mode 100644
index 0000000..be0f5ca
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable/ic_redo.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/samples/hwgeneration/src/main/res/drawable/ic_select.xml b/samples/hwgeneration/src/main/res/drawable/ic_select.xml
new file mode 100644
index 0000000..72d0117
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable/ic_select.xml
@@ -0,0 +1,25 @@
+
+
+
\ No newline at end of file
diff --git a/samples/hwgeneration/src/main/res/drawable/ic_undo.xml b/samples/hwgeneration/src/main/res/drawable/ic_undo.xml
new file mode 100644
index 0000000..2e0ff2f
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/drawable/ic_undo.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/samples/hwgeneration/src/main/res/layout/activity_main.xml b/samples/hwgeneration/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..3b17357
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/layout/activity_main.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/hwgeneration/src/main/res/layout/dialog_input.xml b/samples/hwgeneration/src/main/res/layout/dialog_input.xml
new file mode 100644
index 0000000..2fbdae3
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/layout/dialog_input.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/hwgeneration/src/main/res/layout/style_spinner_item.xml b/samples/hwgeneration/src/main/res/layout/style_spinner_item.xml
new file mode 100644
index 0000000..cedbb7a
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/layout/style_spinner_item.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/hwgeneration/src/main/res/menu/action_menu.xml b/samples/hwgeneration/src/main/res/menu/action_menu.xml
new file mode 100644
index 0000000..b2cb5c4
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/menu/action_menu.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/samples/hwgeneration/src/main/res/menu/main_menu.xml b/samples/hwgeneration/src/main/res/menu/main_menu.xml
new file mode 100644
index 0000000..8765cf3
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/menu/main_menu.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/hwgeneration/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/samples/hwgeneration/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..96525d0
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/samples/hwgeneration/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/samples/hwgeneration/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..96525d0
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/samples/hwgeneration/src/main/res/mipmap-hdpi/ic_launcher.png b/samples/hwgeneration/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a98a138
Binary files /dev/null and b/samples/hwgeneration/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/samples/hwgeneration/src/main/res/mipmap-mdpi/ic_launcher.png b/samples/hwgeneration/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..34ca9b3
Binary files /dev/null and b/samples/hwgeneration/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/samples/hwgeneration/src/main/res/mipmap-xhdpi/ic_launcher.png b/samples/hwgeneration/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..4b3eb36
Binary files /dev/null and b/samples/hwgeneration/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/samples/hwgeneration/src/main/res/mipmap-xxhdpi/ic_launcher.png b/samples/hwgeneration/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..bbea1d0
Binary files /dev/null and b/samples/hwgeneration/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/samples/hwgeneration/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/samples/hwgeneration/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..36639e3
Binary files /dev/null and b/samples/hwgeneration/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/samples/hwgeneration/src/main/res/values/strings.xml b/samples/hwgeneration/src/main/res/values/strings.xml
new file mode 100644
index 0000000..8a6230f
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/values/strings.xml
@@ -0,0 +1,19 @@
+
+ Handwriting Generation Sample
+
+ Invalid certificate
+ Please check certificate data provided at Engine creation.
+
+ Missing Handwriting Generation Rights
+ Please check that you have a dedicated certificate and that you have included the handwriting generation resource.
+
+ Go
+ Undo
+ Redo
+ Clear
+ Save
+
+ Build Profile from file
+ Build Profile
+
+
\ No newline at end of file
diff --git a/samples/hwgeneration/src/main/res/values/styles.xml b/samples/hwgeneration/src/main/res/values/styles.xml
new file mode 100644
index 0000000..38779a0
--- /dev/null
+++ b/samples/hwgeneration/src/main/res/values/styles.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/samples/keyboard-input/.gitignore b/samples/keyboard-input/.gitignore
new file mode 100644
index 0000000..d1bb52f
--- /dev/null
+++ b/samples/keyboard-input/.gitignore
@@ -0,0 +1,5 @@
+build/
+.gradle/
+.idea/
+assets/
+.DS_Store
\ No newline at end of file
diff --git a/samples/keyboard-input/build.gradle b/samples/keyboard-input/build.gradle
new file mode 100644
index 0000000..0b1cbb1
--- /dev/null
+++ b/samples/keyboard-input/build.gradle
@@ -0,0 +1,36 @@
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+}
+
+android {
+ namespace 'com.myscript.iink.samples.keyboardinput'
+
+ compileSdk Versions.compileSdk
+
+ buildFeatures {
+ viewBinding true
+ buildConfig true
+ }
+
+ defaultConfig {
+ minSdk Versions.minSdk
+ targetSdk Versions.targetSdk
+
+ applicationId 'com.myscript.iink.samples.keyboardinput'
+ versionCode project.ext.iinkVersionCode
+ versionName project.ext.iinkVersionName
+
+ vectorDrawables.useSupportLibrary true
+ }
+}
+
+dependencies {
+ implementation "androidx.core:core-ktx:${Versions.androidx_core}"
+ implementation "androidx.activity:activity-ktx:${Versions.androidx_activity}"
+ implementation "androidx.appcompat:appcompat:${Versions.appcompat}"
+ implementation "com.google.android.material:material:${Versions.material}"
+
+ implementation project(':UIReferenceImplementation')
+ implementation project(':myscript-certificate')
+}
\ No newline at end of file
diff --git a/samples/keyboard-input/src/main/AndroidManifest.xml b/samples/keyboard-input/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..f3fc842
--- /dev/null
+++ b/samples/keyboard-input/src/main/AndroidManifest.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/keyboard-input/src/main/java/com/myscript/iink/samples/keyboardinput/IInkApplication.kt b/samples/keyboard-input/src/main/java/com/myscript/iink/samples/keyboardinput/IInkApplication.kt
new file mode 100644
index 0000000..9b57221
--- /dev/null
+++ b/samples/keyboard-input/src/main/java/com/myscript/iink/samples/keyboardinput/IInkApplication.kt
@@ -0,0 +1,28 @@
+package com.myscript.iink.samples.keyboardinput
+
+import android.app.Application
+import com.myscript.certificate.MyCertificate
+import com.myscript.iink.Engine
+
+class IInkApplication : Application() {
+
+ companion object {
+ private var engine: Engine? = null
+
+ fun getEngine(): Engine? {
+ if (engine == null) {
+ engine = Engine.create(MyCertificate.getBytes())
+ }
+ return engine
+ }
+
+ fun close() {
+ engine?.close()
+ }
+ }
+
+ override fun onTerminate() {
+ close()
+ super.onTerminate()
+ }
+}
\ No newline at end of file
diff --git a/samples/keyboard-input/src/main/java/com/myscript/iink/samples/keyboardinput/MainActivity.kt b/samples/keyboard-input/src/main/java/com/myscript/iink/samples/keyboardinput/MainActivity.kt
new file mode 100644
index 0000000..389a2e3
--- /dev/null
+++ b/samples/keyboard-input/src/main/java/com/myscript/iink/samples/keyboardinput/MainActivity.kt
@@ -0,0 +1,518 @@
+// Copyright @ MyScript. All rights reserved.
+package com.myscript.iink.samples.keyboardinput
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.net.Uri
+import android.os.Build
+import android.os.Build.VERSION_CODES
+import android.os.Bundle
+import android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
+import android.util.TypedValue
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.view.inputmethod.InputMethodManager
+import android.widget.EditText
+import android.widget.FrameLayout
+import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.res.ResourcesCompat
+import androidx.documentfile.provider.DocumentFile
+import androidx.lifecycle.coroutineScope
+import androidx.lifecycle.lifecycleScope
+import com.myscript.iink.ContentBlock
+import com.myscript.iink.ContentPackage
+import com.myscript.iink.ContentPart
+import com.myscript.iink.ContentSelection
+import com.myscript.iink.Editor
+import com.myscript.iink.EditorError
+import com.myscript.iink.Engine
+import com.myscript.iink.GestureAction
+import com.myscript.iink.IEditorListener
+import com.myscript.iink.IGestureHandler
+import com.myscript.iink.MimeType
+import com.myscript.iink.NativeObjectHandle
+import com.myscript.iink.PlaceholderController
+import com.myscript.iink.PointerTool
+import com.myscript.iink.graphics.Rectangle
+import com.myscript.iink.samples.keyboardinput.databinding.ActivityMainBinding
+import com.myscript.iink.uireferenceimplementation.EditorBinding
+import com.myscript.iink.uireferenceimplementation.EditorData
+import com.myscript.iink.uireferenceimplementation.EditorView
+import com.myscript.iink.uireferenceimplementation.FontUtils
+import com.myscript.iink.uireferenceimplementation.InputController
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.ByteArrayOutputStream
+import java.io.File
+import java.io.FileOutputStream
+import java.nio.file.Files
+import java.util.UUID
+import kotlin.math.roundToInt
+
+private fun EditText.getTextWithLineBreaks() : String {
+ return (0 until layout.lineCount).joinToString("") {
+ val line = layout.text.subSequence(layout.getLineStart(it), layout.getLineVisibleEnd(it))
+ if ((line.isEmpty() || (line.first() != '\n' && line.last() != '\n')) && it < layout.lineCount - 1) {
+ "$line\n"
+ } else {
+ line
+ }
+ }
+}
+
+class MainActivity : AppCompatActivity() {
+ private var engine: Engine? = null
+ private var contentPackage: ContentPackage? = null
+ private var contentPart: ContentPart? = null
+
+ private var editorData: EditorData? = null
+ private var editorView: EditorView? = null
+
+ private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
+
+ private var currentlyEditedView = -1
+ private var currentlyEditedPlaceholder: String? = null
+
+ private val editorListener = object : IEditorListener {
+ override fun partChanging(editor: Editor, oldPart: ContentPart?, newPart: ContentPart?) = Unit
+ override fun partChanged(editor: Editor) = Unit
+ override fun contentChanged(editor: Editor, blockIds: Array) {
+ invalidateOptionsMenu()
+ }
+ override fun onError(editor: Editor, blockId: String, err: EditorError, message: String) = Unit
+ override fun selectionChanged(editor: Editor) = Unit
+ override fun activeBlockChanged(editor: Editor, blockId: String) = Unit
+ }
+
+ private val gestureHandler = object : IGestureHandler {
+ override fun onTap(editor: Editor, tool: PointerTool?, gestureStrokeId: String, x: Float, y: Float): GestureAction {
+ val rootBlock = editor.rootBlock ?: return GestureAction.APPLY_GESTURE
+ val transform = editor.renderer.viewTransform ?: return GestureAction.APPLY_GESTURE
+ transform.invert()
+ val pointMM = transform.apply(x, y)
+ val blockIds = getBlocksAt(editor, rootBlock, pointMM.x, pointMM.y)
+ val handled = blockIds.isNotEmpty()
+ lifecycleScope.launch(Dispatchers.Main) {
+ blockIds.firstOrNull()?.let {
+ editor.getBlockById(it)?.let { imageBlock ->
+ val data = editor.placeholderController.getUserData(imageBlock)
+ editor.renderer.viewTransform?.let { transform ->
+ val topLeft = transform.apply(imageBlock.box.x, imageBlock.box.y)
+ editTextBlockAt(topLeft.x, topLeft.y, text = data)
+ editor.placeholderController.setVisible(imageBlock, false)
+ currentlyEditedPlaceholder = imageBlock.id
+ }
+ }
+ }
+ }
+ return if (handled) GestureAction.IGNORE else GestureAction.APPLY_GESTURE
+ }
+
+ private fun getBlocksAt(editor: Editor, contentBlock: ContentBlock, x: Float, y: Float): List {
+ val blocksAtCoordinates = mutableListOf()
+ contentBlock.children.forEach {
+ val box = it.box
+ if (x >= box.x && x <= box.x + box.width && y >= box.y && y <= box.y + box.height && editor.placeholderController.isPlaceholder(it)) {
+ blocksAtCoordinates.add(it.id)
+ }
+ blocksAtCoordinates.addAll(getBlocksAt(editor, it, x, y))
+ }
+ return blocksAtCoordinates
+ }
+
+ override fun onDoubleTap(editor: Editor, tool: PointerTool?, gestureStrokeIds: Array, x: Float, y: Float): GestureAction = GestureAction.APPLY_GESTURE
+
+ override fun onLongPress(editor: Editor, tool: PointerTool?, gestureStrokeId: String, x: Float, y: Float): GestureAction {
+ editor.pointerCancel(editorData?.inputController?.previousPointerId ?: 0)
+ lifecycleScope.launch(Dispatchers.Main) {
+ val fingerSizeInMM = 10f
+ val fingerSizeInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, fingerSizeInMM, resources.displayMetrics)
+ val correctedY = y - fingerSizeInPixels / 2
+ editTextBlockAt(x = x, y = correctedY)
+ }
+
+ return GestureAction.IGNORE
+ }
+
+ override fun onUnderline(editor: Editor, tool: PointerTool?, gestureStrokeId: String, selection: NativeObjectHandle): GestureAction = GestureAction.APPLY_GESTURE
+
+ override fun onSurround(editor: Editor, tool: PointerTool?, gestureStrokeId: String, selection: NativeObjectHandle): GestureAction = GestureAction.APPLY_GESTURE
+
+ override fun onJoin(editor: Editor, tool: PointerTool?, gestureStrokeId: String, before: NativeObjectHandle, after: NativeObjectHandle): GestureAction = GestureAction.APPLY_GESTURE
+
+ override fun onInsert(editor: Editor, tool: PointerTool?, gestureStrokeId: String, before: NativeObjectHandle, after: NativeObjectHandle): GestureAction = GestureAction.APPLY_GESTURE
+
+ override fun onStrikethrough(editor: Editor, tool: PointerTool?, gestureStrokeId: String, selection: NativeObjectHandle): GestureAction = GestureAction.APPLY_GESTURE
+
+ override fun onScratch(editor: Editor, tool: PointerTool?, gestureStrokeId: String, selection: NativeObjectHandle): GestureAction = GestureAction.APPLY_GESTURE
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ engine = IInkApplication.getEngine()
+
+ if (engine == null) {
+ // The certificate provided is incorrect, you need to use the one provided by MyScript
+ AlertDialog.Builder(this)
+ .setTitle( getString(R.string.app_error_invalid_certificate_title))
+ .setMessage( getString(R.string.app_error_invalid_certificate_message))
+ .setPositiveButton(android.R.string.ok, null)
+ .show()
+ finishAndRemoveTask() // be sure to end the application
+ return
+ }
+
+ // configure recognition
+ engine?.configuration?.let { configuration ->
+ val confDir = "zip://$packageCodePath!/assets/conf"
+ configuration.setStringArray("configuration-manager.search-path", arrayOf(confDir))
+ configuration.setString("content-package.temp-folder", File(filesDir, "tmp").absolutePath)
+
+ configuration.setString("raw-content.line-pattern", "grid")
+ }
+
+ setContentView(binding.root)
+
+ val typefaceMap = FontUtils.loadFontsFromAssets(applicationContext.assets)
+
+ editorView = findViewById(com.myscript.iink.uireferenceimplementation.R.id.editor_view).apply {
+ setTypefaces(typefaceMap)
+ }
+
+ val editorBinding = EditorBinding(engine, typefaceMap)
+ editorData = editorBinding.openEditor(editorView)
+
+ val editor = requireNotNull(editorData?.editor)
+ editor.setGestureHandler(gestureHandler)
+ editor.addListener(editorListener)
+
+ editorData?.inputController?.inputMode = InputController.INPUT_MODE_AUTO
+
+ val file = File(filesDir, "file.iink")
+ engine?.deletePackage(file)
+ contentPackage = engine?.createPackage(file)
+ contentPart = contentPackage?.createPart("Raw Content")
+
+ supportActionBar?.title = getString(R.string.app_name)
+
+ // wait for view size initialization before setting part
+ editorView?.post {
+ editorView?.let { editorView ->
+ editorView.renderer?.let { renderer ->
+ renderer.setViewOffset(0f, 0f)
+ renderer.viewScale = 1f
+ editorView.visibility = View.VISIBLE
+ editor.part = contentPart
+ }
+ }
+ }
+
+ with(binding) {
+ container.setOnTouchListener { _, event ->
+ var handled = false
+ if (currentlyEditedView > -1) {
+ findViewById(currentlyEditedView)?.let { view ->
+ val area = Rect(view.x.roundToInt(), view.y.roundToInt(), view.width, view.height)
+ if (!area.contains(event.x.roundToInt(), event.y.roundToInt())) {
+ currentlyEditedView = -1
+ addEditTextAsImage(view, view.x, view.y)
+ handled = true
+ }
+ }
+ }
+ handled
+ }
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ val inflater = menuInflater
+ inflater.inflate(R.menu.main_menu, menu)
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
+
+ menu?.findItem(R.id.editor_menu_undo)?.isEnabled = editorData?.editor?.canUndo() ?: false
+ menu?.findItem(R.id.editor_menu_redo)?.isEnabled = editorData?.editor?.canRedo() ?: false
+ menu?.findItem(R.id.editor_menu_clear)?.isEnabled = editorData?.editor?.isEmpty(null) == false
+
+ menu?.findItem(R.id.editor_menu_save)?.isEnabled = editorData?.editor?.isEmpty(null) == false
+
+
+ return super.onPrepareOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.editor_menu_undo -> {
+ editorData?.editor?.undo()
+ true
+ }
+ R.id.editor_menu_redo -> {
+ editorData?.editor?.redo()
+ true
+ }
+ R.id.editor_menu_clear -> {
+ if (currentlyEditedView > -1) {
+ currentlyEditedPlaceholder = null
+ val editText = findViewById(currentlyEditedView)
+ findViewById(R.id.container).removeView(editText)
+ }
+ editorData?.editor?.clear()
+ true
+ }
+ R.id.editor_menu_open -> {
+ importRequest.launch("*/*")
+ true
+ }
+ R.id.editor_menu_save -> {
+ export()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ override fun onDestroy() {
+ editorData?.editor?.let {
+ it.renderer.close()
+ it.removeListener(editorListener)
+ it.setGestureHandler(null)
+ it.close()
+ }
+
+ editorView?.let {
+ it.setOnTouchListener(null)
+ it.editor = null
+ }
+
+ contentPart?.close()
+ contentPart = null
+
+ contentPackage?.close()
+ contentPackage = null
+
+ // IInkApplication has the ownership, do not close here
+ engine = null
+
+ super.onDestroy()
+ }
+
+ private fun editTextBlockAt(x: Float, y: Float, width: Float = 0f, text: String = ""): EditText {
+ val editText = EditText(this).apply {
+ id = View.generateViewId()
+ }
+
+ editText.setText(text)
+ editText.setSelection(text.length)
+ addViewAt(editText, x, y, width)
+
+ currentlyEditedView = editText.id
+ displayKeyboard(editText)
+
+ return editText
+ }
+
+ private fun addViewAt(view: View, x: Float, y: Float, width: Float = 0f, height: Float = 0f) {
+ val container = findViewById(R.id.container)
+
+ val viewScale = editorData?.editor?.renderer?.viewScale ?: 1f
+ view.scaleX = viewScale
+ view.scaleY = viewScale
+ view.pivotX = 0f
+
+ container.addView(view)
+
+ view.layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT).apply {
+ leftMargin = x.roundToInt()
+ topMargin = y.roundToInt()
+
+ if (width > 0) {
+ this.width = (width / view.scaleX).roundToInt()
+ }
+ if (height > 0) {
+ this.height = (height / view.scaleY).roundToInt()
+ }
+ }
+
+ // When typing, edittext grow bigger if unrestrained.
+ // When zoomed, it won't respect screen border so we need to artificially set it.
+ if (view is EditText && view.scaleX != 1f) {
+ view.maxWidth = ((container.width - x) / view.scaleX).roundToInt()
+ }
+ }
+
+ private fun addEditTextAsImage(editText: EditText, x: Float, y: Float) {
+ hideKeyboard()
+
+ // Before creating image to remove cursor
+ editText.clearFocus()
+ editText.inputType = editText.inputType or TYPE_TEXT_FLAG_NO_SUGGESTIONS
+ editText.clearComposingText()
+
+ // Remove background to remove underline
+ editText.background = ResourcesCompat.getDrawable(resources, android.R.color.transparent, theme)
+
+ // Add implicit line breaks to final text.
+ addViewAsImage(editText, x, y, editText.getTextWithLineBreaks())
+ val container = findViewById(R.id.container)
+ container.postDelayed({
+ container.removeView(editText)
+ }, 50)
+ }
+
+ private fun addViewAsImage(view: View, x: Float, y: Float, data: String) {
+ val editor = editorData?.editor ?: return
+ val bitmap = bitmapFromView(view)
+ val imageFile = fileFromBitmap(bitmap)
+
+ val rectangle = Rectangle(x, y, bitmap.width.toFloat(), bitmap.height.toFloat())
+
+ val placeholderId = currentlyEditedPlaceholder
+ if (placeholderId != null) {
+ editor.getBlockById(placeholderId)?.use { block ->
+ editor.placeholderController.update(block, rectangle, imageFile.absolutePath, MimeType.PNG, data)
+ editor.placeholderController.setVisible(block, true)
+ }
+ currentlyEditedPlaceholder = null
+ } else {
+ editor.placeholderController.add(rectangle, imageFile.absolutePath, MimeType.PNG, data, false, PlaceholderController.PlaceholderInteractivityOptions()).also(ContentBlock::close)
+ }
+ }
+
+ private fun bitmapFromView(view: View): Bitmap {
+ val returnedBitmap = Bitmap.createBitmap((view.width * view.scaleX).roundToInt(), (view.height * view.scaleY).roundToInt(), Bitmap.Config.ARGB_8888)
+ val canvas = Canvas(returnedBitmap)
+ val bgDrawable = view.background
+ if (bgDrawable != null) {
+ bgDrawable.draw(canvas)
+ } else {
+ canvas.drawColor(ResourcesCompat.getColor(resources, android.R.color.transparent, theme))
+ }
+ canvas.scale(view.scaleX, view.scaleY)
+ view.draw(canvas)
+ return returnedBitmap
+ }
+
+ private fun fileFromBitmap(bitmap: Bitmap, fileName: String = "export.png"): File {
+ // create a file to write bitmap data
+ val imageFile = File(cacheDir, fileName)
+ imageFile.delete()
+ imageFile.createNewFile()
+
+ // Convert bitmap to byte array
+ val bos = ByteArrayOutputStream()
+ bitmap.compress(Bitmap.CompressFormat.PNG, 0 /*ignored for PNG*/, bos)
+
+ // write the bytes in file
+ val fos = FileOutputStream(imageFile)
+ fos.write(bos.toByteArray())
+ fos.flush()
+ fos.close()
+
+ return imageFile
+ }
+
+ private fun displayKeyboard(view: View) {
+ view.requestFocus()
+ val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
+ imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
+ }
+
+ // No need to force hide if using `adjustPan`
+ private fun hideKeyboard() {
+ val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
+ imm.hideSoftInputFromWindow(currentFocus?.windowToken, 0)
+ }
+
+ private fun export() {
+ val exportedFile = File(cacheDir, EXPORTED_FILE_NAME)
+ contentPackage?.saveAs(exportedFile)
+
+ val i = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
+ type = "application/*"
+ addCategory(Intent.CATEGORY_OPENABLE)
+ putExtra(Intent.EXTRA_TITLE, exportedFile.name)
+ }
+
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.O) {
+ exportLauncher.launch(i)
+ } else {
+ Toast.makeText(applicationContext, "Not able to save file on this Android version", Toast.LENGTH_LONG).show()
+ }
+ }
+
+ @RequiresApi(VERSION_CODES.O)
+ val exportLauncher: ActivityResultLauncher =
+ registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) {
+ it.data?.data?.also { uri ->
+ val contentResolver = applicationContext.contentResolver
+ val inputFile = File(cacheDir, EXPORTED_FILE_NAME)
+ contentResolver.openOutputStream(uri)?.use { outputStream ->
+ Files.copy(inputFile.toPath(), outputStream)
+ }
+ }
+ }
+
+ private val importRequest = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
+ if (uri == null) return@registerForActivityResult
+ val mimeType = DocumentFile.fromSingleUri(this@MainActivity, uri)?.type ?: contentResolver.getType(uri)
+ when (mimeType) {
+ "binary/octet-stream",
+ "application/zip",
+ "application/octet-stream",
+ "application/binary",
+ "application/x-zip" -> lifecycle.coroutineScope.launch {
+ processUriFile(uri, File(cacheDir, "${UUID.randomUUID()}.iink")) { file ->
+ contentPart?.close()
+ contentPackage?.close()
+
+ contentPackage = engine?.openPackage(file)
+ contentPart = contentPackage?.getPart(0)
+ editorData?.editor?.part = contentPart
+ editorData?.renderer?.setViewOffset(0f, 0f)
+ }
+ }
+ else -> Toast.makeText(applicationContext, "Not able to open file", Toast.LENGTH_LONG).show()
+
+ }
+ }
+
+ private suspend fun Context.processUriFile(uri: Uri, file: File, logic: (File) -> Unit) {
+ withContext(Dispatchers.IO) {
+ runCatching {
+ contentResolver.openInputStream(uri)?.use { inputStream ->
+ file.outputStream().use { outputStream ->
+ inputStream.copyTo(outputStream)
+ }
+ }
+ }
+ }
+ try {
+ logic(file)
+ } finally {
+ file.deleteOnExit()
+ }
+ }
+
+ companion object {
+ private const val EXPORTED_FILE_NAME = "export.iink"
+ }
+}
\ No newline at end of file
diff --git a/samples/keyboard-input/src/main/res/drawable-v24/ic_launcher_foreground.xml b/samples/keyboard-input/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..aa654c2
--- /dev/null
+++ b/samples/keyboard-input/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/keyboard-input/src/main/res/drawable/ic_delete.xml b/samples/keyboard-input/src/main/res/drawable/ic_delete.xml
new file mode 100644
index 0000000..d88caee
--- /dev/null
+++ b/samples/keyboard-input/src/main/res/drawable/ic_delete.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/samples/keyboard-input/src/main/res/drawable/ic_launcher_background.xml b/samples/keyboard-input/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..85efc6f
--- /dev/null
+++ b/samples/keyboard-input/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/samples/keyboard-input/src/main/res/drawable/ic_redo.xml b/samples/keyboard-input/src/main/res/drawable/ic_redo.xml
new file mode 100644
index 0000000..be0f5ca
--- /dev/null
+++ b/samples/keyboard-input/src/main/res/drawable/ic_redo.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/samples/keyboard-input/src/main/res/drawable/ic_undo.xml b/samples/keyboard-input/src/main/res/drawable/ic_undo.xml
new file mode 100644
index 0000000..2e0ff2f
--- /dev/null
+++ b/samples/keyboard-input/src/main/res/drawable/ic_undo.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/samples/keyboard-input/src/main/res/layout/activity_main.xml b/samples/keyboard-input/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..230d9d3
--- /dev/null
+++ b/samples/keyboard-input/src/main/res/layout/activity_main.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/keyboard-input/src/main/res/menu/main_menu.xml b/samples/keyboard-input/src/main/res/menu/main_menu.xml
new file mode 100644
index 0000000..d3faaa6
--- /dev/null
+++ b/samples/keyboard-input/src/main/res/menu/main_menu.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/keyboard-input/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/samples/keyboard-input/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..96525d0
--- /dev/null
+++ b/samples/keyboard-input/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/samples/keyboard-input/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/samples/keyboard-input/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..96525d0
--- /dev/null
+++ b/samples/keyboard-input/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/samples/keyboard-input/src/main/res/mipmap-hdpi/ic_launcher.png b/samples/keyboard-input/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a98a138
Binary files /dev/null and b/samples/keyboard-input/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/samples/keyboard-input/src/main/res/mipmap-mdpi/ic_launcher.png b/samples/keyboard-input/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..34ca9b3
Binary files /dev/null and b/samples/keyboard-input/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/samples/keyboard-input/src/main/res/mipmap-xhdpi/ic_launcher.png b/samples/keyboard-input/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..4b3eb36
Binary files /dev/null and b/samples/keyboard-input/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/samples/keyboard-input/src/main/res/mipmap-xxhdpi/ic_launcher.png b/samples/keyboard-input/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..bbea1d0
Binary files /dev/null and b/samples/keyboard-input/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/samples/keyboard-input/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/samples/keyboard-input/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..36639e3
Binary files /dev/null and b/samples/keyboard-input/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/samples/keyboard-input/src/main/res/values/strings.xml b/samples/keyboard-input/src/main/res/values/strings.xml
new file mode 100644
index 0000000..9ac47bb
--- /dev/null
+++ b/samples/keyboard-input/src/main/res/values/strings.xml
@@ -0,0 +1,15 @@
+
+ Keyboard Input Sample
+
+ Invalid certificate
+ Please check certificate data provided at Engine creation.
+
+ Undo
+ Redo
+ Clear
+ Open
+ Save
+
+ Active Pen
+
+
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/build.gradle b/samples/offscreen-interactivity/build.gradle
new file mode 100644
index 0000000..c19ee10
--- /dev/null
+++ b/samples/offscreen-interactivity/build.gradle
@@ -0,0 +1,41 @@
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+}
+
+android {
+ namespace 'com.myscript.iink.offscreen.demo'
+
+ compileSdk Versions.compileSdk
+
+ buildFeatures {
+ viewBinding true
+ }
+
+ defaultConfig {
+ minSdk Versions.minSdk
+ targetSdk Versions.targetSdk
+
+ applicationId 'com.myscript.iink.offscreen.demo'
+ }
+}
+
+dependencies {
+ implementation "androidx.appcompat:appcompat:${Versions.appcompat}"
+ implementation "androidx.activity:activity-ktx:${Versions.androidx_activity}"
+ implementation "androidx.core:core-ktx:${Versions.androidx_core}"
+ implementation "com.google.android.material:material:${Versions.material}"
+ implementation "com.google.code.gson:gson:${Versions.gson}"
+
+ implementation "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.androidx_lifecycle}"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.androidx_lifecycle}"
+ implementation "androidx.lifecycle:lifecycle-livedata-ktx:${Versions.androidx_lifecycle}"
+
+ implementation project(':UIReferenceImplementation')
+ implementation project(':myscript-certificate')
+
+ testImplementation 'junit:junit:4.13.2'
+ testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1'
+ testImplementation 'androidx.arch.core:core-testing:2.2.0'
+}
+
diff --git a/samples/offscreen-interactivity/src/main/AndroidManifest.xml b/samples/offscreen-interactivity/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..26d9da0
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/AndroidManifest.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/main/assets/part_conf.json b/samples/offscreen-interactivity/src/main/assets/part_conf.json
new file mode 100644
index 0000000..48b2b6e
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/assets/part_conf.json
@@ -0,0 +1,36 @@
+{
+ "raw-content": {
+ "configuration": {
+ "analyzer": {
+ "bundle": "raw-content2",
+ "name": "standard"
+ },
+ "math": {
+ "bundle": "math2",
+ "name": "standard"
+ }
+ },
+ "classification": {
+ "types": [ "text", "drawing", "math", "shape" ]
+ },
+ "recognition": {
+ "types": [ "text", "math", "shape" ]
+ },
+ "pen": {
+ "gestures": [ "scratch-out", "strike-through" ]
+ }
+ },
+ "export": {
+ "jiix": {
+ "text": {
+ "chars": true,
+ "words": true
+ },
+ "bounding-box": true,
+ "strokes": false,
+ "glyphs": false,
+ "primitives": false,
+ "ids": true
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/main/java/com/microsoft/device/ink/InkView.kt b/samples/offscreen-interactivity/src/main/java/com/microsoft/device/ink/InkView.kt
new file mode 100644
index 0000000..f5912e8
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/java/com/microsoft/device/ink/InkView.kt
@@ -0,0 +1,468 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+/*
+ * https://github.com/microsoft/surface-duo-sdk/blob/main/inksdk/ink/src/main/java/com/microsoft/device/ink/InkView.kt
+ */
+
+package com.microsoft.device.ink
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.DashPathEffect
+import android.graphics.Paint
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
+import android.graphics.SurfaceTexture
+import android.util.AttributeSet
+import android.util.TypedValue
+import android.view.Surface
+import android.view.TextureView
+import com.myscript.iink.offscreen.demo.R
+import java.util.UUID
+
+// constants
+const val minPointsForValidStroke = 2
+const val defaultHoverStrokeWidth = 5f
+
+class InkView constructor(
+ context: Context,
+ attributeSet: AttributeSet
+) :
+ TextureView(context, attributeSet), TextureView.SurfaceTextureListener {
+
+ private var surface: Surface? = null
+ private var inputManager: InputManager
+ private var drawCanvas: Canvas = Canvas()
+ private lateinit var canvasBitmap: Bitmap
+ private val currentStrokePaint = Paint()
+ private val strokeList = mutableListOf()
+ private val overridePaint: Paint
+ private val clearPaint: Paint
+ private val hoverPaint = Paint()
+ private val hoverEraserPaint = Paint()
+
+ // attributes
+ private var enablePressure = false
+ private var minStrokeWidth = 1f
+ private var maxStrokeWidth = 10f
+
+ // properties
+ var color = Color.GRAY
+ set(value) {
+ field = value
+ currentStrokePaint.color = value
+ hoverPaint.color = currentStrokePaint.color
+ }
+
+ var strokeWidth: Float
+ get() {
+ return minStrokeWidth
+ }
+ set(value) {
+ minStrokeWidth = value
+ // cache eraser hover radius
+ val radius = (maxStrokeWidth - minStrokeWidth) / 2
+ hoverEraserPaint.setPathEffect(
+ DashPathEffect(
+ floatArrayOf(
+ radius,
+ radius,
+ radius,
+ radius
+ ),
+ 0f
+ )
+ )
+ }
+
+ var strokeWidthMax: Float
+ get() {
+ return maxStrokeWidth
+ }
+ set(value) {
+ maxStrokeWidth = value
+ // cache eraser hover radius
+ val radius = (maxStrokeWidth - minStrokeWidth) / 2
+ hoverEraserPaint.setPathEffect(
+ DashPathEffect(
+ floatArrayOf(
+ radius,
+ radius,
+ radius,
+ radius
+ ),
+ 0f
+ )
+ )
+ }
+
+ var pressureEnabled: Boolean
+ get() {
+ return enablePressure
+ }
+ set(value) {
+ enablePressure = value
+ }
+
+ var dynamicPaintHandler: DynamicPaintHandler? = null
+ var strokesListener: StrokesListener? = null
+
+ interface DynamicPaintHandler {
+ fun generatePaintFromPenInfo(penInfo: InputManager.PenInfo): Paint
+ }
+
+ interface StrokesListener {
+ fun onStrokeAdded(brush: Brush)
+ }
+
+ data class Brush(
+ val id: String = UUID.randomUUID().toString(),
+ val color: Int = Color.BLACK,
+ val strokeWidth: Float = 0f,
+ val strokeWidthMax: Float = 0f,
+ val paintHandler: DynamicPaintHandler? = null,
+ val stroke: InputManager.ExtendedStroke
+ )
+
+ init {
+ // handle attributes
+ context.theme.obtainStyledAttributes(attributeSet, R.styleable.InkView, 0, 0)
+ .apply {
+ try {
+ enablePressure = getBoolean(R.styleable.InkView_enable_pressure, enablePressure)
+ color = getColor(R.styleable.InkView_ink_color, color)
+ minStrokeWidth = getFloat(R.styleable.InkView_min_stroke_width, minStrokeWidth)
+ maxStrokeWidth = getFloat(R.styleable.InkView_max_stroke_width, maxStrokeWidth)
+ } finally {
+ recycle()
+ }
+ }
+ isOpaque = false // make the texture view transparent!
+ this.surfaceTextureListener = this
+
+ // setup blend modes
+ overridePaint = Paint()
+ overridePaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC)
+ clearPaint = Paint()
+ clearPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
+
+ inputManager = createInputManager()
+
+ initCurrentStrokePaint()
+ initHoverPaint()
+ }
+
+ private fun createInputManager(): InputManager {
+ return InputManager(
+ this,
+ object : InputManager.PenInputHandler {
+ override fun strokeStarted(
+ penInfo: InputManager.PenInfo,
+ stroke: InputManager.ExtendedStroke
+ ) {
+ redrawTexture()
+ }
+
+ override fun strokeUpdated(
+ penInfo: InputManager.PenInfo,
+ stroke: InputManager.ExtendedStroke
+ ) {
+ redrawTexture()
+ }
+
+ override fun strokeCompleted(
+ penInfo: InputManager.PenInfo,
+ stroke: InputManager.ExtendedStroke
+ ) {
+ redrawTexture()
+ strokeList += stroke
+ strokesListener?.onStrokeAdded(
+ Brush(
+ color = color,
+ strokeWidth = strokeWidth,
+ strokeWidthMax = strokeWidthMax,
+ paintHandler = dynamicPaintHandler,
+ stroke = stroke)
+ )
+ }
+ },
+ object : InputManager.PenHoverHandler {
+ override fun hoverStarted(penInfo: InputManager.PenInfo) {
+ drawHover(
+ penInfo.x,
+ penInfo.y,
+ (minStrokeWidth + maxStrokeWidth) / 2,
+ penInfo.pointerType
+ )
+ }
+
+ override fun hoverMoved(penInfo: InputManager.PenInfo) {
+ drawHover(
+ penInfo.x,
+ penInfo.y,
+ (minStrokeWidth + maxStrokeWidth) / 2,
+ penInfo.pointerType
+ )
+ }
+
+ override fun hoverEnded(penInfo: InputManager.PenInfo) {
+ redrawTexture()
+ }
+ }
+ )
+ }
+
+ private fun initCurrentStrokePaint() {
+ currentStrokePaint.color = color
+ currentStrokePaint.isAntiAlias = true
+ // Set stroke width based on display density.
+ currentStrokePaint.strokeWidth = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ minStrokeWidth,
+ resources.displayMetrics
+ )
+
+ currentStrokePaint.style = Paint.Style.STROKE
+ currentStrokePaint.strokeJoin = Paint.Join.ROUND
+ currentStrokePaint.strokeCap = Paint.Cap.ROUND
+ }
+
+ private fun initHoverPaint() {
+ hoverPaint.color = currentStrokePaint.color
+ hoverPaint.isAntiAlias = true
+ // Set stroke width based on display density.
+ hoverPaint.strokeWidth = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ defaultHoverStrokeWidth,
+ resources.displayMetrics
+ )
+
+ hoverPaint.style = Paint.Style.STROKE
+ hoverPaint.strokeJoin = Paint.Join.ROUND
+ hoverPaint.strokeCap = Paint.Cap.ROUND
+
+ // Eraser hover indicator
+ hoverEraserPaint.color = Color.LTGRAY
+ hoverEraserPaint.isAntiAlias = true
+ hoverEraserPaint.strokeWidth = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ defaultHoverStrokeWidth,
+ resources.displayMetrics
+ )
+ hoverEraserPaint.style = Paint.Style.STROKE
+ hoverEraserPaint.strokeJoin = Paint.Join.ROUND
+ hoverEraserPaint.strokeCap = Paint.Cap.ROUND
+ }
+
+ fun saveBitmap(): Bitmap {
+ val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+ val saveCanvas = Canvas(bitmap)
+ drawStroke()
+ saveCanvas.drawBitmap(canvasBitmap, 0f, 0f, overridePaint)
+ return bitmap
+ }
+
+ fun drawStrokes(brushes: List) {
+
+ // reset canvas
+ drawCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
+ strokeList.clear()
+ inputManager.currentStroke = InputManager.ExtendedStroke()
+
+ // draw each of the brush strokes
+ for (brush in brushes) {
+ brush.stroke.lastPointReferenced = 0
+ strokeList.add(brush.stroke)
+
+ color = brush.color
+ dynamicPaintHandler = brush.paintHandler
+ inputManager.currentStroke = brush.stroke
+
+ drawStroke()
+ }
+ redrawTexture()
+ }
+
+ private fun updateStrokeWidth(pressure: Float) {
+ currentStrokePaint.strokeWidth = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ minStrokeWidth + ((maxStrokeWidth - minStrokeWidth) * pressure),
+ resources.displayMetrics
+ )
+ }
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ super.onSizeChanged(w, h, oldw, oldh)
+ canvasBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+ drawCanvas = Canvas(canvasBitmap)
+ redrawTexture()
+ }
+
+ fun drawHover(
+ cx: Float,
+ cy: Float,
+ radius: Float,
+ pointerType: InputManager.PointerType = InputManager.PointerType.UNKNOWN
+ ) {
+
+ val canvas: Canvas = surface?.lockHardwareCanvas() ?: return
+ try {
+ // Copy image to the canvas
+ canvas.drawBitmap(canvasBitmap, 0f, 0f, overridePaint)
+ if (pointerType == InputManager.PointerType.PEN_ERASER) {
+ canvas.drawCircle(cx, cy, radius, hoverEraserPaint)
+ } else {
+ canvas.drawCircle(cx, cy, radius, hoverPaint)
+ }
+ } finally {
+ // Publish the frame. If we overrun the consumer, frames will be dropped,
+ // so on a sufficiently fast device the animation will run at faster than
+ // the display refresh rate.
+ //
+ // If the SurfaceTexture has been destroyed, this will throw an exception.
+ try {
+ surface?.unlockCanvasAndPost(canvas)
+ } catch (iae: IllegalArgumentException) {
+ return
+ }
+ }
+ }
+
+ fun redrawTexture() {
+ drawStroke()
+ val canvas: Canvas = surface?.lockHardwareCanvas() ?: return
+ try {
+ // Copy image to the canvas
+ canvas.drawBitmap(canvasBitmap, 0f, 0f, overridePaint)
+ } finally {
+ // Publish the frame. If we overrun the consumer, frames will be dropped,
+ // so on a sufficiently fast device the animation will run at faster than
+ // the display refresh rate.
+ //
+ // If the SurfaceTexture has been destroyed, this will throw an exception.
+ try {
+ surface?.unlockCanvasAndPost(canvas)
+ } catch (iae: IllegalArgumentException) {
+ return
+ }
+ }
+ }
+
+ private fun drawStroke() {
+
+ val stroke = inputManager.currentStroke
+ val points = stroke.getPoints()
+
+ if (strokeList.isEmpty() && points.isEmpty()) {
+ return
+ }
+
+ if (points.size < minPointsForValidStroke) {
+ return
+ }
+
+ // update the drawCanvas with the latest stroke data
+ var startPoint = points[stroke.lastPointReferenced]
+ for (i in stroke.lastPointReferenced + 1 until points.size) {
+ val penInfo = stroke.getPenInfo(points[i])
+ if (penInfo != null) {
+ when {
+ penInfo.pointerType == InputManager.PointerType.PEN_ERASER -> {
+ drawCanvas.drawCircle(penInfo.x, penInfo.y, 30f, clearPaint)
+ }
+ dynamicPaintHandler != null -> {
+ dynamicPaintHandler?.let { paintHandler ->
+ val paint = paintHandler.generatePaintFromPenInfo(penInfo)
+ hoverPaint.color = paint.color
+ drawCanvas.drawLine(
+ startPoint.x,
+ startPoint.y,
+ penInfo.x,
+ penInfo.y,
+ paint
+ )
+ }
+ }
+ enablePressure -> {
+ updateStrokeWidth(penInfo.pressure)
+ drawCanvas.drawLine(
+ startPoint.x,
+ startPoint.y,
+ penInfo.x,
+ penInfo.y,
+ currentStrokePaint
+ )
+ }
+ else -> {
+ drawCanvas.drawLine(
+ startPoint.x,
+ startPoint.y,
+ penInfo.x,
+ penInfo.y,
+ currentStrokePaint
+ )
+ }
+ }
+
+ startPoint = points[i]
+ }
+ }
+ stroke.lastPointReferenced = points.size - 1
+ }
+
+ /**
+ * Invoked when a [TextureView]'s SurfaceTexture is ready for use.
+ *
+ * @param surface The surface returned by
+ * [android.view.TextureView.getSurfaceTexture]
+ * @param width The width of the surface
+ * @param height The height of the surface
+ */
+ override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
+ if (width > 0 && height > 0) {
+ this.surface = Surface(surface)
+ redrawTexture()
+ } else {
+ this.surface?.release()
+ this.surface = null
+ }
+ }
+
+ /**
+ * Invoked when the [SurfaceTexture]'s buffers size changed.
+ *
+ * @param surface The surface returned by
+ * [android.view.TextureView.getSurfaceTexture]
+ * @param width The new width of the surface
+ * @param height The new height of the surface
+ */
+ override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
+ }
+
+ /**
+ * Invoked when the specified [SurfaceTexture] is about to be destroyed.
+ * If returns true, no rendering should happen inside the surface texture after this method
+ * is invoked. If returns false, the client needs to call [SurfaceTexture.release].
+ * Most applications should return true.
+ *
+ * @param surface The surface about to be destroyed
+ */
+ override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
+ this.surface?.release()
+ return true
+ }
+
+ /**
+ * Invoked when the specified [SurfaceTexture] is updated through
+ * [SurfaceTexture.updateTexImage].
+ *
+ * @param surface The surface just updated
+ */
+ override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
+ }
+}
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/main/java/com/microsoft/device/ink/InputManager.kt b/samples/offscreen-interactivity/src/main/java/com/microsoft/device/ink/InputManager.kt
new file mode 100644
index 0000000..13a7b62
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/java/com/microsoft/device/ink/InputManager.kt
@@ -0,0 +1,205 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+/*
+ * https://github.com/microsoft/surface-duo-sdk/blob/main/inksdk/ink/src/main/java/com/microsoft/device/ink/InputManager.kt
+ */
+
+package com.microsoft.device.ink
+
+import android.annotation.SuppressLint
+import android.os.SystemClock
+import android.view.MotionEvent
+import android.view.View
+
+class InputManager(
+ view: View,
+ private val penInputHandler: PenInputHandler,
+ private val penHoverHandler: PenHoverHandler? = null,
+ private val timeOffset: Long = System.currentTimeMillis() - SystemClock.uptimeMillis()) {
+
+ var currentStroke = ExtendedStroke()
+
+ init {
+ setupInputEvents(view)
+ currentStroke.reset()
+ }
+
+ interface PenInputHandler {
+ fun strokeStarted(penInfo: PenInfo, stroke: ExtendedStroke)
+ fun strokeUpdated(penInfo: PenInfo, stroke: ExtendedStroke)
+ fun strokeCompleted(penInfo: PenInfo, stroke: ExtendedStroke)
+ }
+
+ interface PenHoverHandler {
+ fun hoverStarted(penInfo: PenInfo)
+ fun hoverMoved(penInfo: PenInfo)
+ fun hoverEnded(penInfo: PenInfo)
+ }
+
+ enum class PointerType {
+ MOUSE,
+ FINGER,
+ PEN_TIP,
+ PEN_ERASER,
+ UNKNOWN
+ }
+
+ class Point(
+ val x: Float,
+ val y: Float,
+ )
+
+ data class PenInfo(
+ val pointerType: PointerType,
+ val x: Float,
+ val y: Float,
+ val timestamp: Long,
+ val pressure: Float,
+ val orientation: Float,
+ val tilt: Float,
+ val primaryButtonState: Boolean,
+ val secondaryButtonState: Boolean
+ ) {
+ companion object {
+ fun createFromEvent(event: MotionEvent, timeOffset: Long): PenInfo {
+ val pointerType: PointerType = when (event.getToolType(0)) {
+ MotionEvent.TOOL_TYPE_FINGER -> PointerType.FINGER
+ MotionEvent.TOOL_TYPE_MOUSE -> PointerType.MOUSE
+ MotionEvent.TOOL_TYPE_STYLUS -> PointerType.PEN_TIP
+ MotionEvent.TOOL_TYPE_ERASER -> PointerType.PEN_ERASER
+ else -> PointerType.UNKNOWN
+ }
+
+ return PenInfo(
+ pointerType = pointerType,
+ x = event.x,
+ y = event.y,
+ timestamp = timeOffset + event.eventTime,
+ pressure = event.pressure,
+ orientation = event.orientation,
+ tilt = event.getAxisValue(MotionEvent.AXIS_TILT),
+ primaryButtonState = ((event.buttonState and MotionEvent.BUTTON_PRIMARY) > 0)
+ or ((event.buttonState and MotionEvent.BUTTON_STYLUS_PRIMARY) > 0),
+ secondaryButtonState = ((event.buttonState and MotionEvent.BUTTON_SECONDARY) > 0)
+ or ((event.buttonState and MotionEvent.BUTTON_STYLUS_SECONDARY) > 0)
+ )
+ }
+
+ fun createFromHistoryEvent(event: MotionEvent, pos: Int, timeOffset: Long): PenInfo {
+ val pointerType: PointerType = when (event.getToolType(0)) {
+ MotionEvent.TOOL_TYPE_FINGER -> PointerType.FINGER
+ MotionEvent.TOOL_TYPE_MOUSE -> PointerType.MOUSE
+ MotionEvent.TOOL_TYPE_STYLUS -> PointerType.PEN_TIP
+ MotionEvent.TOOL_TYPE_ERASER -> PointerType.PEN_ERASER
+ else -> PointerType.UNKNOWN
+ }
+
+ return PenInfo(
+ pointerType = pointerType,
+ x = event.getHistoricalX(pos),
+ y = event.getHistoricalY(pos),
+ timestamp = timeOffset + event.getHistoricalEventTime(pos),
+ pressure = event.getHistoricalPressure(pos),
+ orientation = event.getHistoricalOrientation(pos),
+ tilt = event.getHistoricalAxisValue(MotionEvent.AXIS_TILT, pos),
+ primaryButtonState = ((event.buttonState and MotionEvent.BUTTON_PRIMARY) > 0)
+ or ((event.buttonState and MotionEvent.BUTTON_STYLUS_PRIMARY) > 0),
+ secondaryButtonState = ((event.buttonState and MotionEvent.BUTTON_SECONDARY) > 0)
+ or ((event.buttonState and MotionEvent.BUTTON_STYLUS_SECONDARY) > 0)
+ )
+ }
+ }
+ }
+
+ class ExtendedStroke {
+ private var builder = mutableListOf()
+ private var penInfos = HashMap()
+
+ private var _lastPointReferenced = 0
+ var lastPointReferenced: Int
+ get() = _lastPointReferenced
+ set(value) {
+ _lastPointReferenced = value
+ }
+
+ fun addPoint(penInfo: PenInfo) {
+ val point = Point(penInfo.x, penInfo.y)
+ builder.add(point)
+ penInfos[builder.lastIndex] = penInfo // hash codes don't serialize well, so use index
+ }
+
+ fun getPoints(): List {
+ return builder
+ }
+
+ fun getPenInfo(point: Point): PenInfo? {
+ return penInfos[builder.indexOf(point)]
+ }
+
+ fun reset() {
+ builder.clear()
+ lastPointReferenced = 0
+ penInfos.clear()
+ }
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ private fun setupInputEvents(view: View) {
+
+ view.setOnGenericMotionListener { _: View, event: MotionEvent ->
+ var consumed = true
+ if (penHoverHandler == null) {
+ consumed = false
+ } else {
+ val penInfo = PenInfo.createFromEvent(event, timeOffset)
+
+ when (event.actionMasked) {
+ MotionEvent.ACTION_HOVER_MOVE -> {
+ for (i in 0 until event.historySize) {
+ penHoverHandler.hoverMoved(PenInfo.createFromHistoryEvent(event, i, timeOffset))
+ }
+ penHoverHandler.hoverMoved(penInfo)
+ }
+ MotionEvent.ACTION_HOVER_ENTER -> {
+ penHoverHandler.hoverStarted(penInfo)
+ }
+ MotionEvent.ACTION_HOVER_EXIT -> {
+ penHoverHandler.hoverEnded(penInfo)
+ }
+ else -> consumed = false
+ }
+ }
+ consumed
+ }
+ view.setOnTouchListener { _: View, event: MotionEvent ->
+ var consumed = true
+ val penInfo = PenInfo.createFromEvent(event, timeOffset)
+
+ when (event.actionMasked) {
+ MotionEvent.ACTION_DOWN -> {
+ currentStroke = ExtendedStroke()
+ currentStroke.addPoint(penInfo)
+ penInputHandler.strokeStarted(penInfo, currentStroke)
+ }
+ MotionEvent.ACTION_MOVE -> {
+
+ for (i in 0 until event.historySize) {
+ currentStroke.addPoint(PenInfo.createFromHistoryEvent(event, i, timeOffset))
+ }
+ currentStroke.addPoint(penInfo)
+ penInputHandler.strokeUpdated(penInfo, currentStroke)
+ }
+ MotionEvent.ACTION_UP -> {
+ currentStroke.addPoint(penInfo)
+ penInputHandler.strokeCompleted(penInfo, currentStroke)
+ }
+ else -> consumed = false
+ }
+
+ consumed
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/InkFormat.kt b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/InkFormat.kt
new file mode 100644
index 0000000..7f6ca91
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/InkFormat.kt
@@ -0,0 +1,13 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.demo.ink.serialization
+
+data class InkFormat(
+ val timestamp: String,
+ val X: List,
+ val Y: List,
+ val T: List,
+ val F: List,
+ val type: String,
+ val id: String
+)
diff --git a/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/InkParser.kt b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/InkParser.kt
new file mode 100644
index 0000000..99ad520
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/InkParser.kt
@@ -0,0 +1,80 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.demo.ink.serialization
+
+import com.google.gson.Gson
+import com.microsoft.device.ink.InkView.Brush
+import com.microsoft.device.ink.InputManager
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+fun List.json(): String {
+ val inkFormats = mapNotNull { brush ->
+ val stroke = brush.stroke
+ val points = stroke.getPoints()
+
+ val initialTimestamp = stroke.getPenInfo(points.first())?.timestamp ?: return@mapNotNull null
+
+ val deltas = points.mapNotNull { point ->
+ stroke.getPenInfo(point)?.timestamp?.let { timestamp -> timestamp - initialTimestamp }
+ }
+
+ InkFormat(
+ timestamp = formatTimestamp(initialTimestamp),
+ X = points.map { point -> point.x },
+ Y = points.map { point -> point.y},
+ T = deltas,
+ F = points.map { point -> stroke.getPenInfo(point)?.pressure ?: 0f },
+ type = "stroke",
+ id = brush.id
+ )
+ }
+
+ val inkRoot = InkRoot(
+ version = "3",
+ type = "Drawing",
+ id = "MainBlock",
+ items = inkFormats
+ )
+
+ return Gson().toJson(inkRoot)
+}
+
+fun parseJson(json: String): List {
+ val inkRoot = Gson().fromJson(json, InkRoot::class.java)
+ return inkRoot.items.map { item ->
+ val initialTimestamp = stringToTimestamp(item.timestamp)
+ val stroke = InputManager.ExtendedStroke()
+
+ item.X.forEachIndexed { index, x ->
+ val penInfo = InputManager.PenInfo(
+ pointerType = InputManager.PointerType.PEN_TIP,
+ x = x,
+ y = item.Y[index],
+ timestamp = initialTimestamp + item.T[index],
+ pressure = item.F[index],
+ orientation = 0f,
+ tilt = 0f,
+ primaryButtonState = false,
+ secondaryButtonState = false
+ )
+ stroke.addPoint(penInfo)
+ }
+
+ Brush(id = item.id, stroke = stroke)
+ }
+}
+
+private const val DATE_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS"
+private val simpleDateFormat = SimpleDateFormat(DATE_FORMAT, Locale.US)
+
+fun formatTimestamp(timestamp: Long): String {
+ val date = Date(timestamp)
+ return simpleDateFormat.format(date)
+}
+
+fun stringToTimestamp(dateString: String): Long {
+ val date = simpleDateFormat.parse(dateString)
+ return date?.time ?: 0L
+}
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/InkRoot.kt b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/InkRoot.kt
new file mode 100644
index 0000000..e7e013b
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/InkRoot.kt
@@ -0,0 +1,10 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.demo.ink.serialization
+
+data class InkRoot(
+ val version: String,
+ val type: String,
+ val id: String,
+ val items: List
+)
diff --git a/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/jiix/BoundingBox.kt b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/jiix/BoundingBox.kt
new file mode 100644
index 0000000..a1ef965
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/jiix/BoundingBox.kt
@@ -0,0 +1,16 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.demo.ink.serialization.jiix
+
+import com.google.gson.annotations.SerializedName
+
+data class BoundingBox(
+ @SerializedName("x")
+ val x: Float,
+ @SerializedName("y")
+ val y: Float,
+ @SerializedName("width")
+ val width: Float,
+ @SerializedName("height")
+ val height: Float
+)
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/jiix/Element.kt b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/jiix/Element.kt
new file mode 100644
index 0000000..48e573d
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/jiix/Element.kt
@@ -0,0 +1,20 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.demo.ink.serialization.jiix
+
+import com.google.gson.annotations.SerializedName
+
+data class Element(
+ @SerializedName("id")
+ val id: String,
+ @SerializedName("type")
+ val type: String,
+ @SerializedName("label")
+ val label: String,
+ @SerializedName("bounding-box")
+ val boundingBox: BoundingBox,
+ @SerializedName("words")
+ val words: List? = null,
+ @SerializedName("expressions")
+ val expressions: List? = null
+)
diff --git a/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/jiix/Expression.kt b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/jiix/Expression.kt
new file mode 100644
index 0000000..f597697
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/jiix/Expression.kt
@@ -0,0 +1,10 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.demo.ink.serialization.jiix
+
+import com.google.gson.annotations.SerializedName
+
+data class Expression(
+ @SerializedName("bounding-box")
+ val boundingBox: BoundingBox? = null
+)
diff --git a/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/jiix/RecognitionRoot.kt b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/jiix/RecognitionRoot.kt
new file mode 100644
index 0000000..b03b9df
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/jiix/RecognitionRoot.kt
@@ -0,0 +1,14 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.demo.ink.serialization.jiix
+
+import com.google.gson.annotations.SerializedName
+
+data class RecognitionRoot(
+ @SerializedName("type")
+ val type: String,
+ @SerializedName("bounding-box")
+ val boundingBox: BoundingBox,
+ @SerializedName("elements")
+ val elements: List? = null
+)
diff --git a/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/jiix/Solver.kt b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/jiix/Solver.kt
new file mode 100644
index 0000000..b4c7390
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/jiix/Solver.kt
@@ -0,0 +1,37 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.demo.ink.serialization.jiix
+
+import com.google.gson.annotations.SerializedName
+
+data class ItemSolver(
+ @SerializedName("timestamp")
+ val timestamp: String? = null,
+ @SerializedName("X")
+ val X: List? = null,
+ @SerializedName("Y")
+ val Y: List? = null,
+)
+
+data class OperandSolver(
+ @SerializedName("label")
+ val label: String? = null,
+ @SerializedName("bounding-box")
+ val boundingBox: BoundingBox? = null,
+ @SerializedName("solver-output")
+ val solverOutput: Boolean = false,
+ @SerializedName("items")
+ val items: List? = null
+)
+
+data class ExpressionSolver(
+ @SerializedName("operands")
+ val operands: List? = null
+)
+
+data class SolverRoot(
+ @SerializedName("type")
+ val type: String,
+ @SerializedName("expressions")
+ val expressions: List? = null
+)
diff --git a/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/jiix/StrokeExt.kt b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/jiix/StrokeExt.kt
new file mode 100644
index 0000000..61f84fa
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/jiix/StrokeExt.kt
@@ -0,0 +1,98 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.demo.ink.serialization.jiix
+
+import com.microsoft.device.ink.InkView
+import com.microsoft.device.ink.InputManager
+import com.myscript.iink.PointerEvent
+import com.myscript.iink.PointerEventType
+import com.myscript.iink.PointerType
+import com.myscript.iink.demo.inksample.util.DisplayMetricsConverter
+
+val InputManager.PointerType.toIInkPointerType: PointerType
+ get() = when (this) {
+ InputManager.PointerType.MOUSE -> PointerType.MOUSE
+ InputManager.PointerType.FINGER -> PointerType.PEN // we should allow only PointerType.PEN for writing (crashes if we use PointerType.TOUCH)
+ InputManager.PointerType.PEN_TIP -> PointerType.PEN
+ InputManager.PointerType.PEN_ERASER -> PointerType.ERASER
+ InputManager.PointerType.UNKNOWN -> PointerType.TOUCH
+ }
+
+fun InputManager.PenInfo.toPointerEvent(
+ pointerEventType: PointerEventType,
+ pointerId: Int = 0
+): PointerEvent {
+ return PointerEvent(
+ pointerEventType,
+ x,
+ y,
+ timestamp,
+ pressure,
+ pointerType.toIInkPointerType,
+ pointerId
+ )
+}
+
+fun InputManager.ExtendedStroke.toPointerEvents(): List {
+ val points = getPoints()
+ return points.mapIndexedNotNull { index, point ->
+ val pointerEventType = when (index) {
+ 0 -> PointerEventType.DOWN
+ points.lastIndex -> PointerEventType.UP
+ else -> PointerEventType.MOVE
+ }
+ getPenInfo(point)?.toPointerEvent(
+ pointerEventType = pointerEventType,
+ )
+ }
+}
+
+fun List.toBrush(converter: DisplayMetricsConverter?): InkView.Brush {
+ return InkView.Brush(
+ stroke = InputManager.ExtendedStroke().also { extendedStroke ->
+ map { pointerEvent ->
+ extendedStroke.addPoint(
+ InputManager.PenInfo(
+ pointerType = InputManager.PointerType.PEN_TIP,
+ x = converter?.x_mm2px(pointerEvent.x) ?: pointerEvent.x,
+ y = converter?.y_mm2px(pointerEvent.y) ?: pointerEvent.y,
+ timestamp = pointerEvent.t,
+ pressure = pointerEvent.f,
+ orientation = 0f,
+ tilt = 0f,
+ primaryButtonState = false,
+ secondaryButtonState = false
+ )
+ )
+ }
+ }
+ )
+}
+
+fun PointerEvent.convertPointerEvent(converter: DisplayMetricsConverter?): PointerEvent {
+ return PointerEvent(eventType, converter?.x_px2mm(x) ?: x, converter?.y_px2mm(y) ?: y, t, f, pointerType, pointerId)
+}
+
+fun Word.toScreenCoordinates(converter: DisplayMetricsConverter?): Word {
+ return if (this.boundingBox != null) {
+ this.copy(
+ boundingBox = BoundingBox(
+ x = converter?.x_mm2px(this.boundingBox.x) ?: this.boundingBox.x,
+ y = converter?.y_mm2px(this.boundingBox.y) ?: this.boundingBox.y,
+ width = converter?.x_mm2px(this.boundingBox.width) ?: this.boundingBox.width,
+ height = converter?.y_mm2px(this.boundingBox.height) ?: this.boundingBox.height
+ )
+ )
+ } else {
+ this
+ }
+}
+
+fun BoundingBox.toScreenCoordinates(converter: DisplayMetricsConverter?): BoundingBox {
+ return BoundingBox(
+ x = converter?.x_mm2px(this.x) ?: this.x,
+ y = converter?.y_mm2px(this.y) ?: this.y,
+ width = converter?.x_mm2px(this.width) ?: this.width,
+ height = converter?.y_mm2px(this.height) ?: this.height
+ )
+}
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/jiix/Word.kt b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/jiix/Word.kt
new file mode 100644
index 0000000..f262af3
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/ink/serialization/jiix/Word.kt
@@ -0,0 +1,14 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.demo.ink.serialization.jiix
+
+import com.google.gson.annotations.SerializedName
+
+data class Word(
+ @SerializedName("label")
+ val label: String? = null,
+ @SerializedName("candidates")
+ val candidates: List? = null,
+ @SerializedName("bounding-box")
+ val boundingBox: BoundingBox? = null
+)
diff --git a/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/InkApplication.kt b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/InkApplication.kt
new file mode 100644
index 0000000..abaac8f
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/InkApplication.kt
@@ -0,0 +1,33 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.demo.inksample
+
+import android.app.Application
+import com.myscript.certificate.MyCertificate
+import com.myscript.iink.Engine
+import java.io.File
+
+class InkApplication: Application() {
+
+ var engine: Engine? = null
+
+ override fun onCreate() {
+ super.onCreate()
+
+ engine = Engine.create(MyCertificate.getBytes()).apply {
+ configuration.let { conf ->
+ val confDir = "zip://${packageCodePath}!/assets/conf"
+ conf.setStringArray("configuration-manager.search-path", arrayOf(confDir))
+ val tempDir = File(cacheDir, "tmp")
+ conf.setString("content-package.temp-folder", tempDir.absolutePath)
+ conf.setBoolean("offscreen-editor.history-manager.enable", true);
+ }
+ }
+ }
+
+ override fun onTerminate() {
+ super.onTerminate()
+ engine?.close()
+ engine = null
+ }
+}
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/data/InkRepository.kt b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/data/InkRepository.kt
new file mode 100644
index 0000000..dbe7c15
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/data/InkRepository.kt
@@ -0,0 +1,11 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.demo.inksample.data
+
+/**
+ * Repository to read/write ink saved locally
+ */
+interface InkRepository {
+ fun readInkFromFile(): String?
+ fun saveInkToFile(jsonString: String)
+}
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/data/InkRepositoryImpl.kt b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/data/InkRepositoryImpl.kt
new file mode 100644
index 0000000..86452f7
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/data/InkRepositoryImpl.kt
@@ -0,0 +1,25 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.demo.inksample.data
+
+import java.io.BufferedReader
+import java.io.File
+import java.io.FileNotFoundException
+
+class InkRepositoryImpl(private val dataDir: File): InkRepository {
+
+ private val inkFile: File
+ get() = File(dataDir, "current.json")
+
+ override fun readInkFromFile(): String? {
+ return try {
+ inkFile.bufferedReader().use(BufferedReader::readText)
+ } catch (e: FileNotFoundException) {
+ null
+ }
+ }
+
+ override fun saveInkToFile(jsonString: String) {
+ inkFile.writeText(jsonString)
+ }
+}
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/ui/InkViewModel.kt b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/ui/InkViewModel.kt
new file mode 100644
index 0000000..7139446
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/ui/InkViewModel.kt
@@ -0,0 +1,735 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.demo.inksample.ui
+
+import android.graphics.PointF
+import android.util.DisplayMetrics
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
+import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.viewmodel.initializer
+import androidx.lifecycle.viewmodel.viewModelFactory
+import com.google.gson.Gson
+import com.microsoft.device.ink.InkView
+import com.myscript.iink.ContentPart
+import com.myscript.iink.EditorError
+import com.myscript.iink.Engine
+import com.myscript.iink.IOffscreenEditorListener
+import com.myscript.iink.IOffscreenGestureHandler
+import com.myscript.iink.ItemIdCombinationModifier
+import com.myscript.iink.ItemIdHelper
+import com.myscript.iink.MimeType
+import com.myscript.iink.OffscreenEditor
+import com.myscript.iink.OffscreenGestureAction
+import com.myscript.iink.demo.ink.serialization.jiix.BoundingBox
+import com.myscript.iink.demo.ink.serialization.jiix.RecognitionRoot
+import com.myscript.iink.demo.ink.serialization.jiix.SolverRoot
+import com.myscript.iink.demo.ink.serialization.jiix.convertPointerEvent
+import com.myscript.iink.demo.ink.serialization.jiix.toBrush
+import com.myscript.iink.demo.ink.serialization.jiix.toPointerEvents
+import com.myscript.iink.demo.ink.serialization.jiix.toScreenCoordinates
+import com.myscript.iink.demo.ink.serialization.json
+import com.myscript.iink.demo.ink.serialization.parseJson
+import com.myscript.iink.demo.inksample.InkApplication
+import com.myscript.iink.demo.inksample.data.InkRepository
+import com.myscript.iink.demo.inksample.data.InkRepositoryImpl
+import com.myscript.iink.demo.inksample.util.DisplayMetricsConverter
+import com.myscript.iink.demo.inksample.util.autoCloseable
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.File
+import java.util.concurrent.Executors
+
+enum class BlockType {
+ TEXT,
+ MATH,
+ SOLVING
+}
+
+data class Stroke(
+ val points: List
+)
+
+data class RecognitionItem(
+ val text: String,
+ val type: BlockType,
+ val boundingBox: BoundingBox?,
+ val strokes: List = emptyList()
+)
+
+data class RecognitionFeedback(
+ val isVisible: Boolean = false,
+ val items: List = emptyList()
+)
+
+enum class EditorHistoryAction {
+ ADD,
+ REMOVE
+}
+
+data class EditorHistoryItem(
+ val editorHistoryAction: EditorHistoryAction,
+ val strokes: List,
+ val iinkHistoryId: String
+)
+
+data class EditorHistoryState(
+ val canUndo: Boolean = false,
+ val canRedo: Boolean = false
+)
+
+/**
+ * ViewModel responsible for maintaining the state of the OffScreenInteractivity demo application.
+ *
+ * This ViewModel is designed to interact with the iink engine, manage offscreen editing, process ink gestures,
+ * handle ink recognition, and keep the link & mapping between the application ink model and iink's model
+ */
+class InkViewModel(
+ private val repository: InkRepository,
+ private val engine: Engine?,
+ private val dataDir: File,
+ private val exportConfiguration: String,
+ private val workDispatcher: CoroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher(),
+ private val uiDispatcher: CoroutineDispatcher = Dispatchers.Main,
+ private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
+ private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
+) : ViewModel() {
+
+ private val _strokes: MutableLiveData> = MutableLiveData(emptyList())
+ val strokes: LiveData>
+ get() = _strokes
+
+ private val undoRedoStack = mutableListOf>()
+ private var undoRedoIndex = 0
+ private val strokeIdsMappingDeleted: MutableMap = mutableMapOf()
+ private val _editorHistoryState = MutableLiveData(EditorHistoryState())
+ val editorHistoryState: LiveData
+ get() = _editorHistoryState
+
+ // The iinkModel and recognitionFeedback are straightforward methods for debugging and showcasing, providing visual representation for easier understanding.
+ // While this is not the method your app should use to display recognition, it can provide a starting point or guide on how to accomplish this.
+ private val _recognitionFeedback: MutableLiveData = MutableLiveData(RecognitionFeedback())
+ val recognitionFeedback: LiveData
+ get() = _recognitionFeedback
+
+ private val _iinkModel: MutableLiveData = MutableLiveData(EMPTY_HTML)
+ val iinkModel: LiveData
+ get() = _iinkModel
+
+ private val _iinkJIIX: MutableLiveData = MutableLiveData()
+ val iinkJIIX: LiveData
+ get() = _iinkJIIX
+
+ private var offscreenEditor by autoCloseable()
+ private var currentPart by autoCloseable()
+
+ // Through the use of itemIdHelper and strokeIdsMapping, we aim to underscore the importance of maintaining a connection between your application's stroke model and the one used by iink.
+ // ItemIdHelper facilitates the handling of item ids such as strokes & partial strokes that are associated with the offscreen editor.
+ // If you wish to reflect updates from the editor's strokes in your application's strokes, maintaining a mapping will facilitate this process.
+ private var itemIdHelper by autoCloseable()
+
+ // This function converts stroke points from pixels, which are used in device coordinates,
+ // to millimeters, which are used in offscreen editor coordinates, and it can perform this conversion in reverse as well.
+ private var converter: DisplayMetricsConverter? = null
+ // Maps the data model IDs of the iink offscreen editor to the data model IDs of the application.
+ private val strokeIdsMapping: MutableMap = mutableMapOf()
+
+ val contentFile: File
+ get() = File(dataDir, "OffscreenEditor.iink")
+
+ var displayMetrics: DisplayMetrics? = null
+ set(value) {
+ field = value
+ converter = if (value != null) {
+ DisplayMetricsConverter(value)
+ } else {
+ null
+ }
+ }
+
+ private var offScreenGestureHandler: IOffscreenGestureHandler? = object : IOffscreenGestureHandler {
+ override fun onUnderline(
+ editor: OffscreenEditor,
+ gestureStrokeId: String,
+ itemIds: Array
+ ): OffscreenGestureAction {
+ Log.d(TAG, "IOffscreenGestureHandler onUnderline gesture detected")
+ return OffscreenGestureAction.ADD_STROKE
+ }
+
+ override fun onSurround(
+ editor: OffscreenEditor,
+ gestureStrokeId: String,
+ itemIds: Array
+ ): OffscreenGestureAction {
+ Log.d(TAG, "IOffscreenGestureHandler onSurround gesture detected")
+ return OffscreenGestureAction.ADD_STROKE
+ }
+
+ override fun onJoin(
+ editor: OffscreenEditor,
+ gestureStrokeId: String,
+ before: Array,
+ after: Array
+ ): OffscreenGestureAction {
+ Log.d(TAG, "IOffscreenGestureHandler onJoin gesture detected")
+ return OffscreenGestureAction.ADD_STROKE
+ }
+
+ override fun onInsert(
+ editor: OffscreenEditor,
+ gestureStrokeId: String,
+ before: Array,
+ after: Array
+ ): OffscreenGestureAction {
+ Log.d(TAG, "IOffscreenGestureHandler onInsert gesture detected")
+ return OffscreenGestureAction.ADD_STROKE
+ }
+
+ override fun onStrikethrough(
+ editor: OffscreenEditor,
+ gestureStrokeId: String,
+ itemIds: Array
+ ): OffscreenGestureAction {
+ Log.d(TAG, "IOffscreenGestureHandler onStrikethrough gesture detected")
+ val itemIdHelper = itemIdHelper ?: return OffscreenGestureAction.ADD_STROKE
+
+ viewModelScope.launch(uiDispatcher) {
+ val remainingStrokes = _strokes.value?.toMutableList() ?: mutableListOf()
+ val strokesToRemove = mutableListOf()
+
+ // With workDispatcher, this snippet below will not be processed in parallel but rather one at a time.
+ // This is especially useful when you want to ensure that tasks are executed in a specific order,
+ // or when tasks have side-effects that must be isolated to a single thread.
+ withContext(workDispatcher) {
+ // ItemIds may refer to partial strokes, retrieve the corresponding full strokes ids
+ val fullStrokeIds = itemIds.map(itemIdHelper::getFullItemId)
+
+ fullStrokeIds.forEach { strokeId ->
+ val appStrokeId = strokeIdsMapping[strokeId]
+ remainingStrokes.firstOrNull { it.id == appStrokeId }?.let { strokeBrush ->
+ strokesToRemove.add(strokeBrush)
+ }
+ }
+
+ // Erase the gesture stroke (gestureStrokeId) and the erased strokes (fullItemIds) in your application
+ (fullStrokeIds + gestureStrokeId).forEach { strokeId ->
+ strokeIdsMapping.remove(strokeId)?.let { appStrokeId ->
+ strokeIdsMappingDeleted[strokeId] = appStrokeId
+ val strokeBrush = remainingStrokes.firstOrNull { it.id == appStrokeId }
+ remainingStrokes.remove(strokeBrush)
+ }
+ }
+
+ // Erase the scratched strokes (fullItemIds) in offscreen editor
+ val strokeIdsToErase = (fullStrokeIds - gestureStrokeId).toTypedArray()
+ offscreenEditor?.erase(strokeIdsToErase)
+ }
+ removeGestureFromUndoRedoStack(gestureStrokeId)
+ _editorHistoryState.value = addToUndoRedoStack(EditorHistoryAction.REMOVE, strokesToRemove, iinkHistoryId())
+
+ _strokes.value = remainingStrokes
+ }
+ // Discard the gesture stroke (gestureStrokeId) in offscreen editor
+ return OffscreenGestureAction.IGNORE
+ }
+
+ override fun onScratch(
+ editor: OffscreenEditor,
+ gestureStrokeId: String,
+ itemIds: Array
+ ): OffscreenGestureAction {
+ val itemIdHelper = itemIdHelper ?: return OffscreenGestureAction.ADD_STROKE
+
+ viewModelScope.launch(uiDispatcher) {
+ val remainingStrokes = _strokes.value?.toMutableList() ?: mutableListOf()
+ val strokesToRemove = mutableListOf()
+
+ withContext(workDispatcher) {
+ // Retrieve the full stroke ids
+ val fullItemIds = itemIds.map { itemId ->
+ if (itemIdHelper.isPartialItem(itemId))
+ itemIdHelper.getFullItemId(itemId)
+ else
+ itemId
+ }.toTypedArray()
+
+ // Compute the difference between full strokes and erased partial strokes to get remaining item ids
+ val remainingItemIds = itemIdHelper.combine(fullItemIds, itemIds, ItemIdCombinationModifier.DIFFERENCE)
+
+ // Retrieve the points for remaining item ids
+ val remainingItemEvents = remainingItemIds.map { remainingItemId ->
+ itemIdHelper.getPointsForItemId(remainingItemId).toList()
+ }
+
+ // If remaining items exist, replace strokes, else erase strokes
+ val newItemIds = if (remainingItemEvents.isNotEmpty()) {
+ offscreenEditor?.replaceStrokes(
+ fullItemIds,
+ remainingItemEvents.flatten().toTypedArray()
+ )
+ } else {
+ offscreenEditor?.erase(fullItemIds)
+ emptyArray()
+ }
+
+ fullItemIds.forEach { strokeId ->
+ val appStrokeId = strokeIdsMapping[strokeId]
+ remainingStrokes.firstOrNull { it.id == appStrokeId }?.let {
+ strokesToRemove.add(it)
+ }
+ }
+
+ // Erase the erased strokes and gesture strokes in your application
+ (fullItemIds + gestureStrokeId).forEach { strokeId ->
+ strokeIdsMapping.remove(strokeId)?.let { appStrokeId ->
+ strokeIdsMappingDeleted[strokeId] = appStrokeId
+ val strokeBrush = remainingStrokes.firstOrNull { it.id == appStrokeId }
+ remainingStrokes.remove(strokeBrush)
+ }
+ }
+
+ // Convert remaining events to brushes and map to remaining item ids
+ val brushes = remainingItemEvents.map { pointerEvents -> pointerEvents.toBrush(converter) }
+ newItemIds?.zip(brushes)?.forEach { (iinkStrokeId, brush) ->
+ strokeIdsMapping[iinkStrokeId] = brush.id
+ }
+
+ // Add back the remaining strokes
+ remainingStrokes.addAll(brushes)
+ }
+ removeGestureFromUndoRedoStack(gestureStrokeId)
+ _editorHistoryState.value = addToUndoRedoStack(EditorHistoryAction.REMOVE, strokesToRemove, iinkHistoryId())
+
+ _strokes.value = remainingStrokes
+ }
+ return OffscreenGestureAction.IGNORE
+ }
+ }
+
+ private val offScreenEditorListener: IOffscreenEditorListener = object : IOffscreenEditorListener {
+ override fun partChanged(editor: OffscreenEditor) {
+ // no-op
+ }
+
+ // This method is triggered when the content in the editor changes.
+ // It could be due to new strokes added, existing strokes updated or deleted etc.
+ // The blockIds parameter contains the ids of the blocks that were changed.
+ override fun contentChanged(editor: OffscreenEditor, blockIds: Array) {
+ viewModelScope.launch(uiDispatcher) {
+ val htmlExport = withContext(defaultDispatcher) {
+ offscreenEditor?.export_(emptyArray(), MimeType.HTML)
+ }
+
+ _iinkModel.value = htmlExport ?: EMPTY_HTML
+
+ // Export the content as JIIX (JSON format that describes the recognition result).
+ // The computation is offloaded to a CPU bounded dispatcher as it may be a potentially long-running operation.
+ val exportedData = withContext(defaultDispatcher) {
+ offscreenEditor?.export_(emptyArray(), MimeType.JIIX)
+ }
+ // Parse the exported JIIX content and map it to a list of words that are displayed on the screen.
+ // If the exportedData is null, an empty list is used instead.
+ val adjustedWords = if (exportedData == null) {
+ emptyList()
+ } else {
+ val recognitionRoot = Gson().fromJson(exportedData, RecognitionRoot::class.java)
+
+ // Filter out elements that are null or contain any null list of words
+ val recognitionResult = mutableListOf()
+
+ recognitionRoot.elements?.forEach { element ->
+ if (element.expressions != null) {
+
+ val isSolvable = try {
+ offscreenEditor?.mathSolverController?.getAvailableActions(element.id)?.contains("numerical-computation") == true
+ } catch (e: IllegalArgumentException) {
+ false
+ }
+
+ // If possible, get solver output and strokes.
+ if (isSolvable) {
+ val solverJIIX = offscreenEditor?.mathSolverController?.getActionOutput(element.id, "numerical-computation", MimeType.JIIX, engine?.createParameterSet()?.apply {
+ setBoolean("export.jiix.strokes", true)
+ setStringArray("math.solver.numerical-computation", arrayOf("at-right-of-equal-sign"))
+ setBoolean("math.solver.enable-syntactic-correction", false)
+ setBoolean("math.solver.display-implicit-multiply", false)
+ })
+
+ val solverRoot = Gson().fromJson(solverJIIX, SolverRoot::class.java)
+ if (solverRoot.type == "Math") {
+ solverRoot.expressions?.forEach { expression ->
+ expression.operands?.forEach { operand ->
+ if (operand.solverOutput) {
+ val strokes = mutableListOf()
+ operand.items?.forEach loop@{ item ->
+ val xPoints = item.X ?: return@loop
+ val yPoints = item.Y ?: return@loop
+ val converter = converter ?: return@loop
+
+ val points = mutableListOf()
+ for (index in xPoints.indices) {
+ points.add(PointF(
+ converter.x_mm2px(xPoints[index]),
+ converter.y_mm2px(yPoints[index])))
+ }
+ strokes.add(Stroke(points))
+ }
+
+ recognitionResult.add(
+ RecognitionItem(
+ text = operand.label ?: "",
+ type = BlockType.SOLVING,
+ boundingBox = operand.boundingBox?.toScreenCoordinates(converter),
+ strokes = strokes
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+
+ recognitionResult.add(RecognitionItem(
+ text = "",
+ type = BlockType.MATH,
+ boundingBox = element.boundingBox.toScreenCoordinates(converter)
+ ))
+ }
+ if (element.words != null) {
+ recognitionResult.addAll(
+ element.words.map { word ->
+ RecognitionItem(
+ text = word.label ?: "",
+ type = BlockType.TEXT,
+ boundingBox = word.boundingBox?.toScreenCoordinates(converter)
+ )
+ }
+ )
+ }
+ }
+
+ recognitionResult
+ }
+
+ _iinkJIIX.value = withContext(defaultDispatcher) {
+ val engine = engine ?: return@withContext ""
+
+ offscreenEditor?.export_(emptyArray(), MimeType.JIIX, engine.createParameterSet().apply {
+ setBoolean("export.jiix.strokes", false)
+ setBoolean("export.jiix.bounding-box", false)
+ setBoolean("export.jiix.glyphs", false)
+ setBoolean("export.jiix.primitives", false)
+ setBoolean("export.jiix.text.chars", false)
+ setBoolean("export.jiix.text.words", false)
+
+ })
+ } ?: ""
+ _recognitionFeedback.value = _recognitionFeedback.value?.copy(items = adjustedWords)
+ }
+ }
+
+ override fun onError(editor: OffscreenEditor, blockId: String, err: EditorError, message: String) {
+ Log.e(TAG, "IOffscreenEditorListener error (${err.name}): $message")
+ }
+ }
+
+ init {
+ dataDir.mkdirs()
+ offscreenEditor = engine?.createOffscreenEditor(1f, 1f)
+ offscreenEditor?.let { editor ->
+ itemIdHelper = engine?.createItemIdHelper(editor)
+ // Enable text and gestures recognition
+ val configuration = editor.configuration
+ configuration.inject(exportConfiguration)
+ editor.addListener(offScreenEditorListener)
+ editor.setGestureHandler(offScreenGestureHandler)
+ }
+
+ // Create a package with a new part
+ viewModelScope.launch(uiDispatcher) {
+ currentPart = withContext(ioDispatcher) {
+ try {
+ if (contentFile.exists()) {
+ loadInk()
+ engine?.openPackage(contentFile).use { contentPackage ->
+ contentPackage?.getPart(0)
+ }
+ } else {
+ engine?.createPackage(contentFile).use { contentPackage ->
+ contentPackage?.createPart("Raw Content")
+ }
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Error while retrieving ContentPart", e)
+ null
+ }
+ }
+ offscreenEditor?.part = currentPart
+ }
+ }
+
+ private fun addToUndoRedoStack(action: EditorHistoryAction, strokes: List, iinkHistoryId: String): EditorHistoryState {
+ return addToUndoRedoStack(listOf(EditorHistoryItem(action, strokes, iinkHistoryId)))
+ }
+
+ private fun addToUndoRedoStack(editorHistoryItems: List): EditorHistoryState {
+ synchronized(undoRedoStack) {
+ if (undoRedoStack.isNotEmpty()) {
+ for (i in (undoRedoStack.size - 1).downTo(undoRedoIndex)) {
+ undoRedoStack.removeAt(i)
+ }
+ }
+ undoRedoStack.add(undoRedoIndex++, editorHistoryItems)
+
+ return EditorHistoryState(
+ canUndo = undoRedoIndex > 0,
+ canRedo = undoRedoIndex < undoRedoStack.size
+ )
+ }
+ }
+
+ private fun addStrokesForUndoRedo(initialStrokes: List, strokesToAdd: List): List {
+ strokesToAdd.map(InkView.Brush::id).forEach { appStrokeId ->
+ strokeIdsMappingDeleted.entries.firstOrNull { entry ->
+ entry.value == appStrokeId
+ }?.key?.also { id ->
+ strokeIdsMapping[id] = appStrokeId
+ strokeIdsMappingDeleted.remove(id)
+ }
+ }
+
+ return initialStrokes + strokesToAdd
+ }
+
+ private fun removeStrokesForUndoRedo(initialStrokes: List, strokesToRemove: List): List {
+ val strokeIdsToRemove = strokesToRemove.map(InkView.Brush::id)
+
+ strokeIdsToRemove.forEach { id ->
+ strokeIdsMapping.remove(id)?.let { appStrokeId ->
+ strokeIdsMappingDeleted[id] = appStrokeId
+ }
+ }
+
+ return initialStrokes.filter {
+ it.id !in strokeIdsToRemove
+ }
+ }
+
+ private fun clearUndoRedoStack(): EditorHistoryState {
+ synchronized(undoRedoStack) {
+ undoRedoStack.clear()
+ undoRedoIndex = 0
+ return EditorHistoryState()
+ }
+ }
+
+ private fun removeGestureFromUndoRedoStack(gestureId: String): EditorHistoryState {
+ synchronized(undoRedoStack) {
+ run gestureFinder@{
+ undoRedoStack.asReversed().forEach { historyItems ->
+ historyItems.forEach { item ->
+ if (item.strokes.any { it.id == gestureId }) {
+ undoRedoStack.remove(historyItems)
+ return@gestureFinder
+ }
+ }
+ }
+ }
+
+ --undoRedoIndex
+ return EditorHistoryState(
+ canUndo = undoRedoIndex > 0,
+ canRedo = undoRedoIndex < undoRedoStack.size
+ )
+ }
+ }
+
+ fun undo() {
+ viewModelScope.launch(uiDispatcher) {
+ if (undoRedoIndex == 0 || undoRedoStack.isEmpty()) return@launch
+
+ val undoItems = synchronized(undoRedoStack){
+ undoRedoStack[--undoRedoIndex]
+ }
+ undoItems.forEach { item ->
+ val initialStrokes = strokes.value ?: emptyList()
+ _strokes.value = when (item.editorHistoryAction) {
+ EditorHistoryAction.ADD -> removeStrokesForUndoRedo(initialStrokes, item.strokes)
+ EditorHistoryAction.REMOVE -> addStrokesForUndoRedo(initialStrokes, item.strokes)
+ }
+
+ var continueUndoing = true
+ do {
+ continueUndoing = iinkHistoryId() != item.iinkHistoryId
+ offscreenEditor?.historyManager?.undo()
+ } while (continueUndoing)
+ }
+
+ _editorHistoryState.value = EditorHistoryState(
+ canUndo = undoRedoIndex > 0,
+ canRedo = undoRedoIndex < undoRedoStack.size
+ )
+ }
+ }
+
+ fun redo() {
+ viewModelScope.launch(uiDispatcher) {
+ if (undoRedoIndex == undoRedoStack.size || undoRedoStack.isEmpty()) return@launch
+
+ val redoItems = synchronized(undoRedoStack) {
+ undoRedoStack[undoRedoIndex++]
+ }
+ redoItems.forEach { item ->
+ val initialStrokes = strokes.value ?: emptyList()
+ _strokes.value = when (item.editorHistoryAction) {
+ EditorHistoryAction.ADD -> addStrokesForUndoRedo(initialStrokes, item.strokes)
+ EditorHistoryAction.REMOVE -> removeStrokesForUndoRedo(initialStrokes, item.strokes)
+ }
+
+ do {
+ offscreenEditor?.historyManager?.redo()
+ } while (iinkHistoryId() != item.iinkHistoryId)
+ }
+
+ _editorHistoryState.value = EditorHistoryState(
+ canUndo = undoRedoIndex > 0,
+ canRedo = undoRedoIndex < undoRedoStack.size
+ )
+ }
+ }
+
+ fun clearInk() {
+ viewModelScope.launch(uiDispatcher) {
+ if (_strokes.value?.isEmpty() == true) return@launch
+
+ offscreenEditor?.clear()
+
+ strokeIdsMappingDeleted.putAll(strokeIdsMapping)
+ strokeIdsMapping.clear()
+
+ _editorHistoryState.value = addToUndoRedoStack(EditorHistoryAction.REMOVE, _strokes.value ?: emptyList(), iinkHistoryId())
+
+ _strokes.value = emptyList()
+ }
+ }
+
+ fun loadInk() {
+ viewModelScope.launch(uiDispatcher) {
+ _editorHistoryState.value = clearUndoRedoStack()
+
+ val jsonString = withContext(ioDispatcher) {
+ repository.readInkFromFile()
+ }
+
+ _strokes.value = if (jsonString != null) {
+ val brushes = parseJson(jsonString)
+ offscreenEditor?.clear()
+ strokeIdsMapping.clear()
+
+ val pointerEvents = withContext(defaultDispatcher) {
+ brushes.flatMap { brush ->
+ brush.stroke.toPointerEvents().map { pointerEvent ->
+ pointerEvent.convertPointerEvent(converter)
+ }
+ }.toTypedArray()
+ }
+
+ // OffscreenEditor requires a non empty array
+ if (pointerEvents.isNotEmpty()) {
+ val addedStrokes = offscreenEditor?.addStrokes(pointerEvents, false)
+ if (addedStrokes != null) {
+ brushes.forEachIndexed { index, brush ->
+ if (index in addedStrokes.indices) {
+ strokeIdsMapping[addedStrokes[index]] = brush.id
+ }
+ }
+ }
+ }
+ brushes
+ } else {
+ offscreenEditor?.clear()
+ strokeIdsMapping.clear()
+ emptyList()
+ }
+ }
+ }
+
+ fun saveInk(callback: () -> Unit) {
+ viewModelScope.launch(uiDispatcher) {
+ withContext(ioDispatcher) {
+ _strokes.value?.json()?.let { jsonString ->
+ repository.saveInkToFile(jsonString)
+ }
+ engine?.openPackage(contentFile).use { contentPackage ->
+ contentPackage?.save()
+ }
+ }
+ callback.invoke()
+ }
+ }
+
+ fun toggleRecognition(isVisible: Boolean) {
+ viewModelScope.launch(uiDispatcher) {
+ _recognitionFeedback.value = _recognitionFeedback.value?.copy(isVisible = isVisible)
+ }
+ }
+
+ fun addStroke(brush: InkView.Brush) {
+ viewModelScope.launch(uiDispatcher) {
+ _strokes.also { it.value = it.value?.plus(brush) }
+ val pointerEvents = brush.stroke.toPointerEvents().map { pointerEvent ->
+ pointerEvent.convertPointerEvent(converter)
+ }.toTypedArray()
+
+ offscreenEditor?.addStrokes(pointerEvents, true)?.firstNotNullOf { strokeId ->
+ strokeIdsMapping[strokeId] = brush.id
+ }
+
+ _editorHistoryState.value = addToUndoRedoStack(EditorHistoryAction.ADD, listOf(brush), iinkHistoryId())
+ }
+ }
+
+ private fun iinkHistoryId(): String {
+ val historyManager = requireNotNull(offscreenEditor?.historyManager)
+ return historyManager.getUndoRedoIdAt(historyManager.undoRedoStackIndex - 1)
+ }
+
+ @VisibleForTesting
+ public override fun onCleared() {
+ super.onCleared()
+ currentPart = null
+ offScreenGestureHandler = null
+ offscreenEditor = null
+ itemIdHelper = null
+ }
+
+ companion object {
+ private const val TAG = "InkViewModel"
+ private const val EMPTY_HTML = """
+
+ iink model
+
+
+
+"""
+
+ val Factory = viewModelFactory {
+ initializer {
+ val application = checkNotNull(this[APPLICATION_KEY])
+ val engine = checkNotNull((application as InkApplication).engine)
+ val dataDir = File(application.filesDir, "data")
+
+ val config = application.assets.open("part_conf.json").bufferedReader().use { it.readText() }
+ InkViewModel(InkRepositoryImpl(dataDir), engine, dataDir, config)
+ }
+ }
+ }
+}
diff --git a/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/ui/MainActivity.kt b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/ui/MainActivity.kt
new file mode 100644
index 0000000..64cf799
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/ui/MainActivity.kt
@@ -0,0 +1,171 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.demo.inksample.ui
+
+import android.annotation.SuppressLint
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.os.Bundle
+import android.text.method.ScrollingMovementMethod
+import android.view.View
+import android.webkit.WebSettings
+import android.webkit.WebViewClient
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.ShareCompat
+import androidx.core.content.FileProvider
+import androidx.core.view.doOnLayout
+import com.microsoft.device.ink.InkView
+import com.myscript.iink.offscreen.demo.databinding.MainActivityBinding
+import java.io.File
+
+class MainActivity : AppCompatActivity() {
+
+ private val binding by lazy { MainActivityBinding.inflate(layoutInflater) }
+
+ private val inkViewModel: InkViewModel by viewModels { InkViewModel.Factory }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(binding.root)
+
+ binding.iinkModelPreview.apply {
+ webViewClient = WebViewClient()
+ settings.apply {
+ javaScriptEnabled = true
+ cacheMode = WebSettings.LOAD_NO_CACHE
+ }
+ }
+
+ inkViewModel.displayMetrics = resources.displayMetrics
+
+ // In the Microsoft surface Duo sample (which serves as basis for this demo), the code in InkView may require some time to prepare after a rotation.
+ // If we do not account for this delay, the ViewModel may transmit the strokes prematurely,
+ // at a time when the canvas is not yet available.
+ binding.inkView.doOnLayout {
+ // Be aware that calling drawStrokes in this context may not be optimal for performance,
+ // as it triggers a complete redraw with each LiveData update.
+ // While this method serves as a quick demonstration of how strokes are drawn, your application should be designed to handle this more efficiently
+ inkViewModel.strokes.observe(this, binding.inkView::drawStrokes)
+ }
+ inkViewModel.recognitionFeedback.observe(this, ::onRecognitionUpdate)
+ inkViewModel.iinkModel.observe(this, ::onIInkModelUpdate)
+ inkViewModel.editorHistoryState.observe(this, ::onUndoRedoStateUpdate)
+ inkViewModel.iinkJIIX.observe(this, ::onIInkJIIXUpdate)
+ }
+
+ override fun onStart() {
+ super.onStart()
+
+ with(binding) {
+ inkView.strokesListener = StrokesListener()
+ undoBtn.setOnClickListener { inkViewModel.undo() }
+ redoBtn.setOnClickListener { inkViewModel.redo() }
+ clearInkBtn.setOnClickListener { inkViewModel.clearInk() }
+ exportBtn.setOnClickListener {
+ inkViewModel.saveInk {
+ val iinkFile = inkViewModel.contentFile
+ val exportedFile = File(cacheDir, iinkFile.name)
+ iinkFile.copyTo(exportedFile, true)
+
+ val jiixFile = File(cacheDir, "export.jiix").apply {
+ delete()
+ printWriter().use { out ->
+ out.print(iinkJiix.text)
+ }
+ }
+
+ val authority = "com.myscript.iink.offscreen.demo.export"
+ val iinkUri = FileProvider.getUriForFile(this@MainActivity, authority, exportedFile)
+ val jiixUri = FileProvider.getUriForFile(this@MainActivity, authority, jiixFile)
+
+ ShareCompat.IntentBuilder(this@MainActivity)
+ .setType("*/*")
+ .addStream(iinkUri)
+ .addStream(jiixUri)
+ .startChooser()
+ }
+ }
+
+ recognitionSwitch.setOnCheckedChangeListener { _, isChecked ->
+ inkViewModel.toggleRecognition(isVisible = isChecked)
+ }
+ iinkModelPreviewSwitch.setOnCheckedChangeListener { _, isChecked ->
+ iinkModelPreviewLayout.visibility = if (isChecked) View.VISIBLE else View.GONE
+ }
+ iinkJiixSwitch.setOnCheckedChangeListener { _, isChecked ->
+ iinkJiixLayout.visibility = if (isChecked) View.VISIBLE else View.GONE
+ }
+ iinkJiix.movementMethod = ScrollingMovementMethod()
+ iinkJiix.setOnLongClickListener {
+ // Copy the text to the clipboard
+ val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clip = ClipData.newPlainText("jiix", iinkJiix.text)
+ clipboard.setPrimaryClip(clip)
+ true
+ }
+ }
+ }
+
+ override fun onStop() {
+ inkViewModel.saveInk {
+ // no op
+ }
+ with(binding) {
+ inkView.strokesListener = null
+ undoBtn.setOnClickListener(null)
+ redoBtn.setOnClickListener(null)
+ clearInkBtn.setOnClickListener(null)
+ recognitionSwitch.setOnCheckedChangeListener(null)
+ }
+
+ super.onStop()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ inkViewModel.displayMetrics = null
+ }
+
+ private fun onRecognitionUpdate(recognitionFeedback: RecognitionFeedback) {
+ with(binding) {
+ recognitionContent.removeAllViews()
+
+ inkView.alpha = if (recognitionFeedback.isVisible) .25f else 1f
+
+ if (recognitionFeedback.isVisible) {
+ recognitionFeedback.items.forEach { item ->
+ val customView = RecognitionItemView(this@MainActivity, item)
+ recognitionContent.addView(customView)
+ }
+ }
+ }
+ }
+
+ private fun onIInkModelUpdate(htmlExport: String) {
+ binding.iinkModelPreview.loadData(htmlExport, "text/html", Charsets.UTF_8.toString())
+ }
+
+ private fun onUndoRedoStateUpdate(editorHistoryState: EditorHistoryState) {
+ with(binding) {
+ undoBtn.isEnabled = editorHistoryState.canUndo
+ redoBtn.isEnabled = editorHistoryState.canRedo
+ }
+ }
+
+ private fun onIInkJIIXUpdate(jiixExport: String) {
+ binding.iinkJiix.text = jiixExport
+ binding.iinkJiix.scrollTo(0, 0)
+ }
+
+ /**
+ * Listen for strokes from InkView.InputManager
+ */
+ inner class StrokesListener: InkView.StrokesListener {
+ override fun onStrokeAdded(brush: InkView.Brush) {
+ inkViewModel.addStroke(brush)
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/ui/RecognitionItemView.kt b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/ui/RecognitionItemView.kt
new file mode 100644
index 0000000..79beb20
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/ui/RecognitionItemView.kt
@@ -0,0 +1,105 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.demo.inksample.ui
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Path
+import android.graphics.Typeface
+import android.text.TextPaint
+import android.view.View
+
+class RecognitionItemView(context: Context, private val item: RecognitionItem) : View(context) {
+
+ private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
+ color = Color.BLACK
+ textSize = 48f
+ }
+
+ private val boxPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+ color = Color.GRAY
+ style = Paint.Style.STROKE
+ strokeWidth = 2f
+ }
+
+ private val typePaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
+ color = Color.GRAY
+ textSize = 32f
+ typeface = Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)
+ }
+
+ private val solvingPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+ color = Color.GRAY
+ style = Paint.Style.STROKE
+ strokeWidth = 4f
+ }
+
+ private val solvingPath = Path()
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+
+ item.boundingBox?.let { box ->
+ canvas.drawRect(
+ box.x,
+ box.y,
+ box.x + box.width,
+ box.y + box.height,
+ boxPaint.apply {
+ color = when(item.type) {
+ BlockType.TEXT -> TEXT_COLOR
+ BlockType.MATH -> MATH_COLOR
+ BlockType.SOLVING -> SOLVER_COLOR
+ }
+ }
+ )
+
+ if (item.text.isNotEmpty()) {
+ canvas.drawText(
+ item.text,
+ box.x + 5,
+ box.y + box.height - textPaint.descent(),
+ textPaint
+ )
+ }
+
+ canvas.drawText(
+ when(item.type) {
+ BlockType.TEXT -> "abc"
+ BlockType.MATH -> "Σ"
+ BlockType.SOLVING -> "="
+ },
+ box.x + 5,
+ box.y + (textPaint.descent() - textPaint.ascent()) / 2,
+ typePaint.apply {
+ color = when(item.type) {
+ BlockType.TEXT -> TEXT_COLOR
+ BlockType.MATH -> MATH_COLOR
+ BlockType.SOLVING -> SOLVER_COLOR
+ }
+ }
+ )
+
+ if (item.type == BlockType.SOLVING && item.strokes.isNotEmpty()) {
+ item.strokes.forEach { stroke ->
+ if (stroke.points.isNotEmpty()) {
+ solvingPath.reset()
+ solvingPath.moveTo(stroke.points[0].x, stroke.points[0].y)
+ for (i in 1 until stroke.points.size) {
+ solvingPath.lineTo(stroke.points[i].x, stroke.points[i].y)
+ }
+ canvas.drawPath(solvingPath, solvingPaint)
+ }
+ }
+ }
+ }
+ }
+
+ companion object {
+ private const val TEXT_COLOR = Color.RED
+ private const val MATH_COLOR = Color.BLUE
+ private const val SOLVER_COLOR = Color.GREEN
+ }
+}
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/util/DisplayMetricsConverter.kt b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/util/DisplayMetricsConverter.kt
new file mode 100644
index 0000000..01ef04a
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/util/DisplayMetricsConverter.kt
@@ -0,0 +1,20 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.demo.inksample.util
+
+import android.util.DisplayMetrics
+
+class DisplayMetricsConverter(private val displayMetrics: DisplayMetrics) {
+
+ fun x_mm2px(mm: Float) = (mm / INCH_TO_MM) * displayMetrics.xdpi
+
+ fun y_mm2px(mm: Float) = (mm / INCH_TO_MM) * displayMetrics.ydpi
+
+ fun x_px2mm(px: Float) = INCH_TO_MM * (px / displayMetrics.xdpi)
+
+ fun y_px2mm(px: Float) = INCH_TO_MM * (px / displayMetrics.ydpi)
+
+ companion object {
+ private const val INCH_TO_MM = 25.4f
+ }
+}
\ No newline at end of file
diff --git a/java/common-kotlin/src/main/java/com/myscript/iink/app/common/utils/autoCloseable.kt b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/util/autoCloseable.kt
similarity index 98%
rename from java/common-kotlin/src/main/java/com/myscript/iink/app/common/utils/autoCloseable.kt
rename to samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/util/autoCloseable.kt
index 94ca2e3..b4493f4 100644
--- a/java/common-kotlin/src/main/java/com/myscript/iink/app/common/utils/autoCloseable.kt
+++ b/samples/offscreen-interactivity/src/main/java/com/myscript/iink/demo/inksample/util/autoCloseable.kt
@@ -1,6 +1,6 @@
// Copyright @ MyScript. All rights reserved.
-package com.myscript.iink.app.common.utils
+package com.myscript.iink.demo.inksample.util
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
@@ -74,3 +74,4 @@ fun autoCloseable(initialValue: T? = null, onUpdate: ((oldValue: T?) -> Unit
where T : AutoCloseable? {
return AutoCloseableDelegate(initialValue, onUpdate)
}
+
diff --git a/samples/offscreen-interactivity/src/main/res/drawable/circle.xml b/samples/offscreen-interactivity/src/main/res/drawable/circle.xml
new file mode 100644
index 0000000..61fb9f2
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/drawable/circle.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/main/res/drawable/ic_add_background.xml b/samples/offscreen-interactivity/src/main/res/drawable/ic_add_background.xml
new file mode 100644
index 0000000..20f23bd
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/drawable/ic_add_background.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/samples/offscreen-interactivity/src/main/res/drawable/ic_export.xml b/samples/offscreen-interactivity/src/main/res/drawable/ic_export.xml
new file mode 100644
index 0000000..d5a0051
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/drawable/ic_export.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/samples/offscreen-interactivity/src/main/res/drawable/ic_launcher_foreground.xml b/samples/offscreen-interactivity/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..c7632d7
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/samples/offscreen-interactivity/src/main/res/drawable/ic_pen_outlined.xml b/samples/offscreen-interactivity/src/main/res/drawable/ic_pen_outlined.xml
new file mode 100644
index 0000000..ec8c69e
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/drawable/ic_pen_outlined.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/samples/offscreen-interactivity/src/main/res/drawable/ic_redo.xml b/samples/offscreen-interactivity/src/main/res/drawable/ic_redo.xml
new file mode 100644
index 0000000..5b03c60
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/drawable/ic_redo.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/samples/offscreen-interactivity/src/main/res/drawable/ic_undo.xml b/samples/offscreen-interactivity/src/main/res/drawable/ic_undo.xml
new file mode 100644
index 0000000..17bb2c9
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/drawable/ic_undo.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/samples/offscreen-interactivity/src/main/res/drawable/outline_delete_forever_24.xml b/samples/offscreen-interactivity/src/main/res/drawable/outline_delete_forever_24.xml
new file mode 100644
index 0000000..8e84593
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/drawable/outline_delete_forever_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/samples/offscreen-interactivity/src/main/res/drawable/rectangle.xml b/samples/offscreen-interactivity/src/main/res/drawable/rectangle.xml
new file mode 100644
index 0000000..084591e
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/drawable/rectangle.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/main/res/drawable/tool_selection_feedback_selector.xml b/samples/offscreen-interactivity/src/main/res/drawable/tool_selection_feedback_selector.xml
new file mode 100644
index 0000000..7300a4b
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/drawable/tool_selection_feedback_selector.xml
@@ -0,0 +1,13 @@
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/main/res/layout/main_activity.xml b/samples/offscreen-interactivity/src/main/res/layout/main_activity.xml
new file mode 100644
index 0000000..7279351
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/layout/main_activity.xml
@@ -0,0 +1,176 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/samples/offscreen-interactivity/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..7353dbd
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/samples/offscreen-interactivity/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..7353dbd
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/main/res/mipmap-hdpi/ic_launcher.png b/samples/offscreen-interactivity/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..14c763e
Binary files /dev/null and b/samples/offscreen-interactivity/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/samples/offscreen-interactivity/src/main/res/mipmap-hdpi/ic_launcher_round.png b/samples/offscreen-interactivity/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..a8e11f6
Binary files /dev/null and b/samples/offscreen-interactivity/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/samples/offscreen-interactivity/src/main/res/mipmap-mdpi/ic_launcher.png b/samples/offscreen-interactivity/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..5286382
Binary files /dev/null and b/samples/offscreen-interactivity/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/samples/offscreen-interactivity/src/main/res/mipmap-mdpi/ic_launcher_round.png b/samples/offscreen-interactivity/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..6daa6f7
Binary files /dev/null and b/samples/offscreen-interactivity/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/samples/offscreen-interactivity/src/main/res/mipmap-xhdpi/ic_launcher.png b/samples/offscreen-interactivity/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..abd1fd3
Binary files /dev/null and b/samples/offscreen-interactivity/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/samples/offscreen-interactivity/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/samples/offscreen-interactivity/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..02dcb4a
Binary files /dev/null and b/samples/offscreen-interactivity/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/samples/offscreen-interactivity/src/main/res/mipmap-xxhdpi/ic_launcher.png b/samples/offscreen-interactivity/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..773a53f
Binary files /dev/null and b/samples/offscreen-interactivity/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/samples/offscreen-interactivity/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/samples/offscreen-interactivity/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..075825a
Binary files /dev/null and b/samples/offscreen-interactivity/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/samples/offscreen-interactivity/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/samples/offscreen-interactivity/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..0990201
Binary files /dev/null and b/samples/offscreen-interactivity/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/samples/offscreen-interactivity/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/samples/offscreen-interactivity/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..15ca2e1
Binary files /dev/null and b/samples/offscreen-interactivity/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/samples/offscreen-interactivity/src/main/res/values-large/dimens.xml b/samples/offscreen-interactivity/src/main/res/values-large/dimens.xml
new file mode 100644
index 0000000..430bd66
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/values-large/dimens.xml
@@ -0,0 +1,6 @@
+
+
+
+ 256dp
+
+
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/main/res/values-night/colors.xml b/samples/offscreen-interactivity/src/main/res/values-night/colors.xml
new file mode 100644
index 0000000..b71c49b
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/values-night/colors.xml
@@ -0,0 +1,6 @@
+
+
+
+ #1C1C1C
+
+
diff --git a/samples/offscreen-interactivity/src/main/res/values-night/themes.xml b/samples/offscreen-interactivity/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..566cfda
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/values-night/themes.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/samples/offscreen-interactivity/src/main/res/values/attrs.xml b/samples/offscreen-interactivity/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..0643785
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/values/attrs.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/main/res/values/colors.xml b/samples/offscreen-interactivity/src/main/res/values/colors.xml
new file mode 100644
index 0000000..875bbe0
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/values/colors.xml
@@ -0,0 +1,7 @@
+
+
+
+ #009fe3
+ #F5F6F7
+
+
diff --git a/samples/offscreen-interactivity/src/main/res/values/dimens.xml b/samples/offscreen-interactivity/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..83eba2d
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/values/dimens.xml
@@ -0,0 +1,6 @@
+
+
+
+ 128dp
+
+
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/main/res/values/ic_launcher_background.xml b/samples/offscreen-interactivity/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..c5d5899
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/main/res/values/strings.xml b/samples/offscreen-interactivity/src/main/res/values/strings.xml
new file mode 100644
index 0000000..3b2f609
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ iink Offscreen Interactivity Demo
+
diff --git a/samples/offscreen-interactivity/src/main/res/values/themes.xml b/samples/offscreen-interactivity/src/main/res/values/themes.xml
new file mode 100644
index 0000000..4c960b0
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/values/themes.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/samples/offscreen-interactivity/src/main/res/xml/file_paths.xml b/samples/offscreen-interactivity/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..11867f0
--- /dev/null
+++ b/samples/offscreen-interactivity/src/main/res/xml/file_paths.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/test/java/com/myscript/iink/demo/FakeInkRepository.kt b/samples/offscreen-interactivity/src/test/java/com/myscript/iink/demo/FakeInkRepository.kt
new file mode 100644
index 0000000..cadceaf
--- /dev/null
+++ b/samples/offscreen-interactivity/src/test/java/com/myscript/iink/demo/FakeInkRepository.kt
@@ -0,0 +1,18 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.demo
+
+import com.myscript.iink.demo.inksample.data.InkRepository
+
+class FakeInkRepository: InkRepository {
+
+ private var savedJson: String? = null
+
+ override fun readInkFromFile(): String? {
+ return savedJson
+ }
+
+ override fun saveInkToFile(jsonString: String) {
+ savedJson = jsonString
+ }
+}
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/test/java/com/myscript/iink/demo/InkViewModelTests.kt b/samples/offscreen-interactivity/src/test/java/com/myscript/iink/demo/InkViewModelTests.kt
new file mode 100644
index 0000000..25d6003
--- /dev/null
+++ b/samples/offscreen-interactivity/src/test/java/com/myscript/iink/demo/InkViewModelTests.kt
@@ -0,0 +1,139 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.demo
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.microsoft.device.ink.InkView
+import com.microsoft.device.ink.InputManager
+import com.myscript.iink.Engine
+import com.myscript.iink.demo.inksample.data.InkRepository
+import com.myscript.iink.demo.inksample.ui.InkViewModel
+import com.myscript.nebo.test.utils.MainDispatcherRule
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import java.io.File
+
+@RunWith(JUnit4::class)
+class InkViewModelTests {
+
+ // Run tasks synchronously
+ @get:Rule
+ val instantExecutorRule = InstantTaskExecutorRule()
+
+ // Sets the main coroutines dispatcher to a TestCoroutineScope for unit testing.
+ @get:Rule
+ var mainDispatcherRule = MainDispatcherRule()
+
+ private val inkRepository = FakeInkRepository()
+
+ private inline fun usingViewModel(
+ repository: InkRepository = inkRepository,
+ engine: Engine? = null,
+ dataDir: File = File("data"),
+ exportConfiguration: String = "",
+ test: InkViewModel.() -> Unit
+ ) {
+ InkViewModel(
+ repository = repository,
+ engine = engine,
+ dataDir = dataDir,
+ exportConfiguration = exportConfiguration,
+ uiDispatcher = mainDispatcherRule.testDispatcher,
+ ioDispatcher = mainDispatcherRule.testDispatcher,
+ defaultDispatcher = mainDispatcherRule.testDispatcher
+ ).also { inkViewModel ->
+ try {
+ test(inkViewModel)
+ } finally {
+ inkViewModel.onCleared()
+ }
+ }
+ }
+
+ @Test
+ fun `loading ink should update live data`() = runTest {
+ inkRepository.saveInkToFile("{\"version\":\"3\",\"type\":\"Drawing\",\"id\":\"MainBlock\",\"items\":[{\"timestamp\":\"2023-06-21 11:44:00.000\",\"X\":[1.0,2.0,3.0],\"Y\":[1.0,2.0,3.0],\"F\":[0.0,0.0,0.0],\"T\":[0.0,0.0,0.0],\"type\":\"stroke\",\"id\":\"1\"}]}")
+
+ usingViewModel {
+ loadInk()
+ assertTrue(strokes.getOrAwaitValue().isNotEmpty())
+ }
+ }
+
+ @Test
+ fun `loading ink when the repository returns a null json string should result in an empty strokes list`() = runTest {
+ usingViewModel {
+ loadInk()
+ assertTrue(strokes.getOrAwaitValue().isEmpty())
+ }
+ }
+
+ @Test
+ fun `clearing ink should empty live data's content`() = runTest {
+ // start with a state where the viewModel has strokes
+ inkRepository.saveInkToFile("{\"version\":\"3\",\"type\":\"Drawing\",\"id\":\"MainBlock\",\"items\":[{\"timestamp\":\"2023-06-21 11:44:00.000\",\"X\":[1.0,2.0,3.0],\"Y\":[1.0,2.0,3.0],\"F\":[0.0,0.0,0.0],\"T\":[0.0,0.0,0.0],\"type\":\"stroke\",\"id\":\"1\"}]}")
+
+ usingViewModel {
+ loadInk()
+ clearInk()
+ assertTrue(strokes.getOrAwaitValue().isEmpty())
+ }
+ }
+
+ @Test
+ fun `adding a stroke should update live data`() = runTest {
+ val point1 = InputManager.PenInfo(
+ pointerType = InputManager.PointerType.PEN_TIP,
+ x = 1f,
+ y = 1f,
+ timestamp = -1,
+ pressure = 0f,
+ orientation = 0f,
+ tilt = 0f,
+ primaryButtonState = false,
+ secondaryButtonState = false
+ )
+ val point2 = InputManager.PenInfo(
+ pointerType = InputManager.PointerType.PEN_TIP,
+ x = 2f,
+ y = 2f,
+ timestamp = -1,
+ pressure = 0f,
+ orientation = 0f,
+ tilt = 0f,
+ primaryButtonState = false,
+ secondaryButtonState = false
+ )
+ val point3 = InputManager.PenInfo(
+ pointerType = InputManager.PointerType.PEN_TIP,
+ x = 3f,
+ y = 3f,
+ timestamp = -1,
+ pressure = 0f,
+ orientation = 0f,
+ tilt = 0f,
+ primaryButtonState = false,
+ secondaryButtonState = false
+ )
+ val stroke = InputManager.ExtendedStroke().apply {
+ addPoint(point1)
+ addPoint(point2)
+ addPoint(point3)
+ }
+ val brush = InkView.Brush(
+ id = "1",
+ stroke = stroke
+ )
+
+ usingViewModel {
+ val oldStrokes = strokes.getOrAwaitValue()
+ addStroke(brush)
+ val currentStrokes = strokes.getOrAwaitValue()
+ assertTrue(currentStrokes.size == oldStrokes.size + 1)
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/test/java/com/myscript/iink/demo/LiveDataTestExt.kt b/samples/offscreen-interactivity/src/test/java/com/myscript/iink/demo/LiveDataTestExt.kt
new file mode 100644
index 0000000..110b795
--- /dev/null
+++ b/samples/offscreen-interactivity/src/test/java/com/myscript/iink/demo/LiveDataTestExt.kt
@@ -0,0 +1,43 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.demo
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Observer
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+/**
+ * Gets the value of a [LiveData] or waits for it to have one, with a timeout.
+ *
+ * Use this extension from host-side (JVM) tests. It's recommended to use it alongside
+ * `InstantTaskExecutorRule` or a similar mechanism to execute tasks synchronously.
+ */
+fun LiveData.getOrAwaitValue(
+ time: Long = 2,
+ timeUnit: TimeUnit = TimeUnit.SECONDS,
+ afterObserve: () -> Unit = {}
+): T {
+ var data: T? = null
+ val latch = CountDownLatch(1)
+ val observer = object : Observer {
+ override fun onChanged(o: T) {
+ data = o
+ latch.countDown()
+ this@getOrAwaitValue.removeObserver(this)
+ }
+ }
+ this.observeForever(observer)
+
+ afterObserve.invoke()
+
+ // Don't wait indefinitely if the LiveData is not set.
+ if (!latch.await(time, timeUnit)) {
+ this.removeObserver(observer)
+ throw TimeoutException("LiveData value was never set.")
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ return data as T
+}
\ No newline at end of file
diff --git a/samples/offscreen-interactivity/src/test/java/com/myscript/iink/demo/MainDispatcherRule.kt b/samples/offscreen-interactivity/src/test/java/com/myscript/iink/demo/MainDispatcherRule.kt
new file mode 100644
index 0000000..29c636b
--- /dev/null
+++ b/samples/offscreen-interactivity/src/test/java/com/myscript/iink/demo/MainDispatcherRule.kt
@@ -0,0 +1,25 @@
+// Copyright MyScript. All rights reserved.
+
+package com.myscript.nebo.test.utils
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class MainDispatcherRule(
+ val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
+) : TestWatcher() {
+ override fun starting(description: Description) {
+ Dispatchers.setMain(testDispatcher)
+ }
+
+ override fun finished(description: Description) {
+ Dispatchers.resetMain()
+ }
+}
diff --git a/samples/offscreen-interactivity/src/test/java/com/myscript/iink/demo/serialization/InkParserTests.kt b/samples/offscreen-interactivity/src/test/java/com/myscript/iink/demo/serialization/InkParserTests.kt
new file mode 100644
index 0000000..c5900f1
--- /dev/null
+++ b/samples/offscreen-interactivity/src/test/java/com/myscript/iink/demo/serialization/InkParserTests.kt
@@ -0,0 +1,118 @@
+// Copyright @ MyScript. All rights reserved.
+
+package com.myscript.iink.demo.serialization
+
+import com.microsoft.device.ink.InkView
+import com.microsoft.device.ink.InputManager
+import com.myscript.iink.demo.ink.serialization.json
+import com.myscript.iink.demo.ink.serialization.parseJson
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class InkParserTests {
+
+ @Test
+ fun serializeStrokesTest() {
+ val point1 = InputManager.PenInfo(
+ pointerType = InputManager.PointerType.PEN_TIP,
+ x = 1f,
+ y = 1f,
+ timestamp = 0L,
+ pressure = 0f,
+ orientation = 0f,
+ tilt = 0f,
+ primaryButtonState = false,
+ secondaryButtonState = false
+ )
+ val point2 = InputManager.PenInfo(
+ pointerType = InputManager.PointerType.PEN_TIP,
+ x = 2f,
+ y = 2f,
+ timestamp = 1000L,
+ pressure = 0f,
+ orientation = 0f,
+ tilt = 0f,
+ primaryButtonState = false,
+ secondaryButtonState = false
+ )
+ val point3 = InputManager.PenInfo(
+ pointerType = InputManager.PointerType.PEN_TIP,
+ x = 3f,
+ y = 3f,
+ timestamp = 2000L,
+ pressure = 0f,
+ orientation = 0f,
+ tilt = 0f,
+ primaryButtonState = false,
+ secondaryButtonState = false
+ )
+ val stroke = InputManager.ExtendedStroke().apply {
+ addPoint(point1)
+ addPoint(point2)
+ addPoint(point3)
+ }
+ val brush = InkView.Brush(
+ id = "1",
+ stroke = stroke
+ )
+
+ val serialized = listOf(brush).json()
+
+ val initialTimestamp = "1970-01-01 01:00:00.000"
+ val json = "{\"version\":\"3\",\"type\":\"Drawing\",\"id\":\"MainBlock\",\"items\":[{\"timestamp\":\"$initialTimestamp\",\"X\":[1.0,2.0,3.0],\"Y\":[1.0,2.0,3.0],\"T\":[0,1000,2000],\"F\":[0.0,0.0,0.0],\"type\":\"stroke\",\"id\":\"1\"}]}"
+ assertEquals(json, serialized)
+ }
+
+ @Test
+ fun serializeEmptyStrokeListTest() {
+ val serialized = emptyList().json()
+
+ val json = "{\"version\":\"3\",\"type\":\"Drawing\",\"id\":\"MainBlock\",\"items\":[]}"
+
+ assertEquals(json, serialized)
+ }
+
+ @Test
+ fun deserializeStrokesTest() {
+ val initialTimestamp = "1970-01-01 01:00:00.000"
+ val json = "{\"version\":\"3\",\"type\":\"Drawing\",\"id\":\"MainBlock\",\"items\":[{\"timestamp\":\"$initialTimestamp\",\"X\":[1.0,2.0,3.0],\"Y\":[1.0,2.0,3.0],\"T\":[0,1000,2000],\"F\":[0.0,0.0,0.0],\"type\":\"stroke\",\"id\":\"1\"}]}"
+
+ val brushes = parseJson(json)
+
+ assertTrue(brushes.isNotEmpty())
+ assertTrue(brushes.size == 1)
+
+ val brush = brushes.first()
+ val points = brush.stroke.getPoints()
+ assertTrue(points.isNotEmpty())
+ assertTrue(points.size == 3)
+
+ val point1 = brush.stroke.getPenInfo(points.first())
+ assertTrue(point1?.x == 1f)
+ assertTrue(point1?.y == 1f)
+ assertTrue(point1?.timestamp == 0L)
+
+ val point2 = brush.stroke.getPenInfo(points[1])
+ assertTrue(point2?.x == 2f)
+ assertTrue(point2?.y == 2f)
+ assertTrue(point2?.timestamp == 1000L)
+
+ val point3 = brush.stroke.getPenInfo(points[2])
+ assertTrue(point3?.x == 3f)
+ assertTrue(point3?.y == 3f)
+ assertTrue(point3?.timestamp == 2000L)
+ }
+
+ @Test
+ fun deserializeEmptyStrokeListTest() {
+ val json = "{\"version\":\"3\",\"type\":\"Drawing\",\"id\":\"MainBlock\",\"items\":[]}"
+
+ val brushes = parseJson(json)
+
+ assertTrue(brushes.isEmpty())
+ }
+}
diff --git a/samples/search/build.gradle b/samples/search/build.gradle
new file mode 100644
index 0000000..9b96120
--- /dev/null
+++ b/samples/search/build.gradle
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+}
+
+android {
+ namespace 'com.myscript.iink.samples.search'
+
+ compileSdk Versions.compileSdk
+ defaultConfig {
+ applicationId "com.myscript.iink.samples.search"
+ minSdk Versions.minSdk
+ targetSdk Versions.targetSdk
+
+ versionCode project.ext.iinkVersionCode
+ versionName project.ext.iinkVersionName
+ }
+}
+
+dependencies {
+ implementation project(':UIReferenceImplementation')
+ implementation project(':myscript-certificate')
+
+ implementation "androidx.core:core-ktx:${Versions.androidx_core}"
+ implementation "androidx.appcompat:appcompat:${Versions.appcompat}"
+ implementation "com.google.android.material:material:${Versions.material}"
+
+ // Google Gson
+ implementation "com.google.code.gson:gson:${Versions.gson}"
+}
\ No newline at end of file
diff --git a/java/samples/search-kt/src/main/AndroidManifest.xml b/samples/search/src/main/AndroidManifest.xml
similarity index 86%
rename from java/samples/search-kt/src/main/AndroidManifest.xml
rename to samples/search/src/main/AndroidManifest.xml
index f3c553a..c8e3627 100644
--- a/java/samples/search-kt/src/main/AndroidManifest.xml
+++ b/samples/search/src/main/AndroidManifest.xml
@@ -3,8 +3,9 @@
~ Copyright (c) MyScript. All rights reserved.
-->
-
+
+
+
-
+
\ No newline at end of file
diff --git a/java/samples/search-kt/src/main/java/com/myscript/iink/samples/search/JiixRawContent.kt b/samples/search/src/main/java/com/myscript/iink/samples/search/JiixRawContent.kt
similarity index 100%
rename from java/samples/search-kt/src/main/java/com/myscript/iink/samples/search/JiixRawContent.kt
rename to samples/search/src/main/java/com/myscript/iink/samples/search/JiixRawContent.kt
diff --git a/java/samples/search-kt/src/main/java/com/myscript/iink/samples/search/MainActivity.kt b/samples/search/src/main/java/com/myscript/iink/samples/search/MainActivity.kt
similarity index 89%
rename from java/samples/search-kt/src/main/java/com/myscript/iink/samples/search/MainActivity.kt
rename to samples/search/src/main/java/com/myscript/iink/samples/search/MainActivity.kt
index 0a942c7..2a6138b 100644
--- a/java/samples/search-kt/src/main/java/com/myscript/iink/samples/search/MainActivity.kt
+++ b/samples/search/src/main/java/com/myscript/iink/samples/search/MainActivity.kt
@@ -1,7 +1,6 @@
package com.myscript.iink.samples.search
import android.content.Context
-import android.graphics.Color
import android.graphics.Typeface
import android.os.Bundle
import android.util.Log
@@ -12,11 +11,19 @@ import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.ImageButton
import android.widget.Toast
-import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
-import com.myscript.iink.*
-import com.myscript.iink.app.common.activities.ErrorActivity
-import com.myscript.iink.uireferenceimplementation.*
+import com.myscript.iink.ContentPackage
+import com.myscript.iink.ContentPart
+import com.myscript.iink.Editor
+import com.myscript.iink.EditorError
+import com.myscript.iink.IEditorListener
+import com.myscript.iink.samples.search.utils.ErrorActivity
+import com.myscript.iink.uireferenceimplementation.EditorBinding
+import com.myscript.iink.uireferenceimplementation.EditorData
+import com.myscript.iink.uireferenceimplementation.EditorView
+import com.myscript.iink.uireferenceimplementation.FontMetricsProvider
+import com.myscript.iink.uireferenceimplementation.FontUtils
+import com.myscript.iink.uireferenceimplementation.InputController
import java.io.File
@@ -75,26 +82,9 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
-
setContentView(R.layout.activity_main)
- ErrorActivity.setExceptionHandler(applicationContext)
- // Note: could be managed by domain layer and handled through observable error channel
- // but kept simple as is to avoid adding too much complexity for this special (unrecoverable) error case
- if (MyIInkApplication.getEngine() == null) {
- /* the certificate provided in `BatchModule.provideEngine` is most likely incorrect */
- AlertDialog.Builder(this)
- .setTitle( getString(R.string.app_error_invalid_certificate_title))
- .setMessage( getString(R.string.app_error_invalid_certificate_message))
- .setPositiveButton(R.string.dialog_ok
- ) { _,
- _ ->
- finishAffinity()
- finishAndRemoveTask() // be sure to end the application
- }
- .show()
- return
- }
+ ErrorActivity.setExceptionHandler(applicationContext)
// we can also intialize engine here
MyIInkApplication.getEngine()?.apply {
@@ -111,7 +101,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
}
}
- editorView = findViewById(R.id.back_frame).findViewById(R.id.editor_view)
+ editorView = findViewById(R.id.back_frame).findViewById(com.myscript.iink.uireferenceimplementation.R.id.editor_view)
searchView = findViewById(R.id.search_view)
searchView?.visibility = View.INVISIBLE
@@ -154,10 +144,10 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
title = "Type: " + contentPart.type
ed.setViewSize(editorView!!.width, editorView!!.height)
ed.configuration.let { conf ->
- conf.setBoolean("raw-content.recognition.shape", false)
- conf.setBoolean("raw-content.recognition.text", true)
+ conf.setStringArray("raw-content.recognition.types", arrayOf("text"))
+
// Allow conversion of text
- conf.setBoolean("raw-content.convert.text", true)
+ conf.setStringArray("raw-content.convert.types", arrayOf("text"))
}
// now search feature is only available for 'Raw Content' part
diff --git a/java/samples/search-kt/src/main/java/com/myscript/iink/samples/search/MyIInkApplication.kt b/samples/search/src/main/java/com/myscript/iink/samples/search/MyIInkApplication.kt
similarity index 100%
rename from java/samples/search-kt/src/main/java/com/myscript/iink/samples/search/MyIInkApplication.kt
rename to samples/search/src/main/java/com/myscript/iink/samples/search/MyIInkApplication.kt
diff --git a/java/samples/search-kt/src/main/java/com/myscript/iink/samples/search/SearchView.kt b/samples/search/src/main/java/com/myscript/iink/samples/search/SearchView.kt
similarity index 100%
rename from java/samples/search-kt/src/main/java/com/myscript/iink/samples/search/SearchView.kt
rename to samples/search/src/main/java/com/myscript/iink/samples/search/SearchView.kt
diff --git a/samples/search/src/main/java/com/myscript/iink/samples/search/utils/ErrorActivity.kt b/samples/search/src/main/java/com/myscript/iink/samples/search/utils/ErrorActivity.kt
new file mode 100644
index 0000000..9983b95
--- /dev/null
+++ b/samples/search/src/main/java/com/myscript/iink/samples/search/utils/ErrorActivity.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+package com.myscript.iink.samples.search.utils
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.Process
+import android.text.method.ScrollingMovementMethod
+import android.widget.Button
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import com.myscript.iink.samples.search.R
+import java.io.PrintWriter
+import java.io.StringWriter
+import kotlin.system.exitProcess
+
+/**
+ * This activity displays an error message when an uncaught exception is thrown within an activity
+ * that installed the associated exception handler. Since this application targets developers it's
+ * better to clearly explain what happened. The code is inspired by:
+ * https://trivedihardik.wordpress.com/2011/08/20/how-to-avoid-force-close-error-in-android/
+ */
+
+/* important do not forget to add the activity in you manifest
+
+ */
+
+class ErrorActivity : AppCompatActivity() {
+ companion object {
+ private val TAG = ErrorActivity::class.java.toString()
+ private val ERR_TITLE = "err_title@$TAG"
+ private val ERR_MESSAGE = "err_message@$TAG"
+
+ @JvmStatic
+ fun setExceptionHandler(context: Context) {
+ Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(context))
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_error)
+ val tvErrorTitle = findViewById(R.id.tv_error_title)
+ tvErrorTitle.text = intent.getStringExtra(ERR_TITLE)
+
+ val tvErrorMessage = findViewById(R.id.tv_error_message)
+ tvErrorMessage.text = intent.getStringExtra(ERR_MESSAGE)
+ tvErrorMessage.movementMethod = ScrollingMovementMethod()
+
+ findViewById(R.id.exit_button).setOnClickListener {
+ finishAffinity()
+ finishAndRemoveTask()
+ moveTaskToBack(true)
+ exitProcess(-1)
+ }
+ }
+
+ private class ExceptionHandler(private val context: Context) : Thread.UncaughtExceptionHandler {
+ override fun uncaughtException(t: Thread, e: Throwable) {
+ // get message from the root cause.
+ var root: Throwable? = e
+ while (root!!.cause != null) {
+ root = root.cause
+ }
+ val message = root.message
+
+ // print stack trace.
+ val writer = StringWriter()
+ e.printStackTrace(PrintWriter(writer))
+ val trace = writer.toString()
+
+ // launch the error activity.
+ val intent = Intent(context, ErrorActivity::class.java)
+ intent.putExtra(ERR_TITLE, message)
+ intent.putExtra(ERR_MESSAGE, trace)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK;
+ context.startActivity(intent)
+ // kill the current activity.
+ Process.killProcess(Process.myPid())
+ exitProcess(10)
+ }
+ }
+}
\ No newline at end of file
diff --git a/java/samples/search-kt/src/main/res/color/button_text_color.xml b/samples/search/src/main/res/color/button_text_color.xml
similarity index 100%
rename from java/samples/search-kt/src/main/res/color/button_text_color.xml
rename to samples/search/src/main/res/color/button_text_color.xml
diff --git a/samples/search/src/main/res/drawable-v24/ic_launcher_foreground.xml b/samples/search/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..aa654c2
--- /dev/null
+++ b/samples/search/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/java/samples/search-kt/src/main/res/drawable/button_background.xml b/samples/search/src/main/res/drawable/button_background.xml
similarity index 100%
rename from java/samples/search-kt/src/main/res/drawable/button_background.xml
rename to samples/search/src/main/res/drawable/button_background.xml
diff --git a/java/samples/search-kt/src/main/res/drawable/ic_delete.xml b/samples/search/src/main/res/drawable/ic_delete.xml
similarity index 100%
rename from java/samples/search-kt/src/main/res/drawable/ic_delete.xml
rename to samples/search/src/main/res/drawable/ic_delete.xml
diff --git a/java/samples/search-kt/src/main/res/drawable/ic_delete_disabled.xml b/samples/search/src/main/res/drawable/ic_delete_disabled.xml
similarity index 100%
rename from java/samples/search-kt/src/main/res/drawable/ic_delete_disabled.xml
rename to samples/search/src/main/res/drawable/ic_delete_disabled.xml
diff --git a/java/samples/search-kt/src/main/res/drawable/ic_delete_enabled.xml b/samples/search/src/main/res/drawable/ic_delete_enabled.xml
similarity index 100%
rename from java/samples/search-kt/src/main/res/drawable/ic_delete_enabled.xml
rename to samples/search/src/main/res/drawable/ic_delete_enabled.xml
diff --git a/samples/search/src/main/res/drawable/ic_launcher_background.xml b/samples/search/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..85efc6f
--- /dev/null
+++ b/samples/search/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/java/samples/search-kt/src/main/res/drawable/ic_redo.xml b/samples/search/src/main/res/drawable/ic_redo.xml
similarity index 100%
rename from java/samples/search-kt/src/main/res/drawable/ic_redo.xml
rename to samples/search/src/main/res/drawable/ic_redo.xml
diff --git a/java/samples/search-kt/src/main/res/drawable/ic_redo_disabled.xml b/samples/search/src/main/res/drawable/ic_redo_disabled.xml
similarity index 100%
rename from java/samples/search-kt/src/main/res/drawable/ic_redo_disabled.xml
rename to samples/search/src/main/res/drawable/ic_redo_disabled.xml
diff --git a/java/samples/search-kt/src/main/res/drawable/ic_redo_enabled.xml b/samples/search/src/main/res/drawable/ic_redo_enabled.xml
similarity index 100%
rename from java/samples/search-kt/src/main/res/drawable/ic_redo_enabled.xml
rename to samples/search/src/main/res/drawable/ic_redo_enabled.xml
diff --git a/java/samples/search-kt/src/main/res/drawable/ic_undo.xml b/samples/search/src/main/res/drawable/ic_undo.xml
similarity index 100%
rename from java/samples/search-kt/src/main/res/drawable/ic_undo.xml
rename to samples/search/src/main/res/drawable/ic_undo.xml
diff --git a/java/samples/search-kt/src/main/res/drawable/ic_undo_disabled.xml b/samples/search/src/main/res/drawable/ic_undo_disabled.xml
similarity index 100%
rename from java/samples/search-kt/src/main/res/drawable/ic_undo_disabled.xml
rename to samples/search/src/main/res/drawable/ic_undo_disabled.xml
diff --git a/java/samples/search-kt/src/main/res/drawable/ic_undo_enabled.xml b/samples/search/src/main/res/drawable/ic_undo_enabled.xml
similarity index 100%
rename from java/samples/search-kt/src/main/res/drawable/ic_undo_enabled.xml
rename to samples/search/src/main/res/drawable/ic_undo_enabled.xml
diff --git a/java/common-kotlin/src/main/res/layout/activity_error.xml b/samples/search/src/main/res/layout/activity_error.xml
similarity index 89%
rename from java/common-kotlin/src/main/res/layout/activity_error.xml
rename to samples/search/src/main/res/layout/activity_error.xml
index 94ad13a..4fca2ca 100644
--- a/java/common-kotlin/src/main/res/layout/activity_error.xml
+++ b/samples/search/src/main/res/layout/activity_error.xml
@@ -8,8 +8,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:orientation="vertical"
- tools:context=".activities.ErrorActivity">
+ android:orientation="vertical">
+ android:text="@android:string/ok" />
+
+
+
+
+
+
+
diff --git a/samples/search/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/samples/search/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..96525d0
--- /dev/null
+++ b/samples/search/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/samples/search/src/main/res/mipmap-hdpi/ic_launcher.png b/samples/search/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a98a138
Binary files /dev/null and b/samples/search/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/samples/search/src/main/res/mipmap-mdpi/ic_launcher.png b/samples/search/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..34ca9b3
Binary files /dev/null and b/samples/search/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/samples/search/src/main/res/mipmap-xhdpi/ic_launcher.png b/samples/search/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..4b3eb36
Binary files /dev/null and b/samples/search/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/samples/search/src/main/res/mipmap-xxhdpi/ic_launcher.png b/samples/search/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..bbea1d0
Binary files /dev/null and b/samples/search/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/samples/search/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/samples/search/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..36639e3
Binary files /dev/null and b/samples/search/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/java/samples/search-kt/src/main/res/values-night/themes.xml b/samples/search/src/main/res/values-night/themes.xml
similarity index 100%
rename from java/samples/search-kt/src/main/res/values-night/themes.xml
rename to samples/search/src/main/res/values-night/themes.xml
diff --git a/java/samples/search-kt/src/main/res/values/colors.xml b/samples/search/src/main/res/values/colors.xml
similarity index 79%
rename from java/samples/search-kt/src/main/res/values/colors.xml
rename to samples/search/src/main/res/values/colors.xml
index a03f43c..bfb4da6 100644
--- a/java/samples/search-kt/src/main/res/values/colors.xml
+++ b/samples/search/src/main/res/values/colors.xml
@@ -6,6 +6,10 @@
+ #448aff
+ #3f51b5
+ @android:color/white
+
@android:color/darker_gray
@color/colorPrimary
diff --git a/java/samples/search-kt/src/main/res/values/dimens.xml b/samples/search/src/main/res/values/dimens.xml
similarity index 100%
rename from java/samples/search-kt/src/main/res/values/dimens.xml
rename to samples/search/src/main/res/values/dimens.xml
diff --git a/java/samples/search-kt/src/main/res/values/integers.xml b/samples/search/src/main/res/values/integers.xml
similarity index 100%
rename from java/samples/search-kt/src/main/res/values/integers.xml
rename to samples/search/src/main/res/values/integers.xml
diff --git a/java/samples/search-kt/src/main/res/values/strings.xml b/samples/search/src/main/res/values/strings.xml
similarity index 100%
rename from java/samples/search-kt/src/main/res/values/strings.xml
rename to samples/search/src/main/res/values/strings.xml
diff --git a/java/samples/search-kt/src/main/res/values/themes.xml b/samples/search/src/main/res/values/themes.xml
similarity index 100%
rename from java/samples/search-kt/src/main/res/values/themes.xml
rename to samples/search/src/main/res/values/themes.xml
diff --git a/samples/settings.gradle b/samples/settings.gradle
new file mode 100644
index 0000000..3776737
--- /dev/null
+++ b/samples/settings.gradle
@@ -0,0 +1,35 @@
+
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+rootProject.name = 'myscript.iink.samples.android.java'
+
+gradle.ext.iinkVersionCode = 4130
+gradle.ext.iinkVersionName = "4.1.3"
+gradle.ext.iinkResourcesURL = "https://download.myscript.com/iink/recognitionAssets_iink_4.1"
+
+def projects = [
+ // samples
+ ':batch-mode-kotlin' : file("$settingsDir/batch-mode"),
+ ':exercise-assessment-kotlin' : file("$settingsDir/exercise-assessment"),
+ ':search-sample-kotlin' : file("$settingsDir/search"),
+ ':write-to-type' : file("$settingsDir/write-to-type"),
+ ':offscreen-interactivity' : file("$settingsDir/offscreen-interactivity"),
+ ':keyboard-input' : file("$settingsDir/keyboard-input"),
+ ':hwgeneration' : file("$settingsDir/hwgeneration"),
+ // MyScript certificate -> this should be empty
+ ':myscript-certificate' : file("$settingsDir/certificate"),
+ // MyScript iink UI reference implementation.
+ // Copy from: https://github.com/MyScript/interactive-ink-examples-android/tree/master/UIReferenceImplementation
+ ':UIReferenceImplementation' : file("$settingsDir/UIReferenceImplementation"),
+]
+
+projects.each { proj, dir ->
+ logger.info("Including $proj")
+ include proj
+ if (dir != null)
+ project(proj).projectDir = dir
+ if(proj==':myscript-certificate')
+ project(proj).buildFileName = 'build_android.gradle'
+}
diff --git a/samples/write-to-type/ReadMe.pdf b/samples/write-to-type/ReadMe.pdf
new file mode 100644
index 0000000..c705efa
Binary files /dev/null and b/samples/write-to-type/ReadMe.pdf differ
diff --git a/samples/write-to-type/build.gradle b/samples/write-to-type/build.gradle
new file mode 100644
index 0000000..9ebfddf
--- /dev/null
+++ b/samples/write-to-type/build.gradle
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+}
+
+android {
+ namespace 'com.myscript.iink.samples.writetotype'
+ compileSdk Versions.compileSdk
+ defaultConfig {
+ applicationId "com.myscript.iink.samples.writetotype"
+ minSdk Versions.minSdk
+ targetSdk Versions.targetSdk
+ versionCode project.ext.iinkVersionCode
+ versionName project.ext.iinkVersionName
+ }
+}
+
+dependencies {
+ implementation "androidx.core:core-ktx:${Versions.androidx_core}"
+
+ implementation project(':UIReferenceImplementation')
+ implementation project(':myscript-certificate')
+
+ implementation "androidx.appcompat:appcompat:${Versions.appcompat}"
+ implementation "com.google.code.gson:gson:${Versions.gson}"
+}
\ No newline at end of file
diff --git a/samples/write-to-type/src/main/AndroidManifest.xml b/samples/write-to-type/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..efa12b8
--- /dev/null
+++ b/samples/write-to-type/src/main/AndroidManifest.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/CustomViewGroup.java b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/CustomViewGroup.java
new file mode 100644
index 0000000..b87c8cc
--- /dev/null
+++ b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/CustomViewGroup.java
@@ -0,0 +1,437 @@
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+package com.myscript.iink.samples.writetotype;
+
+import android.content.Context;
+import android.graphics.RectF;
+import android.os.Build;
+import android.text.Editable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+
+import androidx.annotation.NonNull;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class CustomViewGroup extends LinearLayout
+{
+ private static final String PUNCTUATIONS = ".,?!'\"(){}-:;«»„¡¿”•_;·჻՛՜՞՝՚。、~〈〉《》「」〖〗・·…๏๚๛ฯๆ";
+ private static final String SPACE = " ";
+ private static final String NEW_LINE = "\n";
+
+ /** Interface definition for callbacks invoked when EditText status changed. */
+ public interface OnChangedListener
+ {
+ void onFocusChanged(final EditText editText);
+ void onSelectionChanged(final EditText editText, final int selectionStart, final int selectionEnd);
+ }
+
+ private OnChangedListener mOnChangedListener = null;
+
+ private EditText mFocusedEditText = null;
+ private final Map mIndexMap = new HashMap<>();
+
+ /** Event Listener for selection changed and focus changed of EditText. */
+ final private View.AccessibilityDelegate mViewDelegate = new View.AccessibilityDelegate() {
+ @Override
+ public void sendAccessibilityEvent(@NonNull View host, int eventType)
+ {
+ super.sendAccessibilityEvent(host, eventType);
+
+ if (eventType == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED)
+ {
+ if (mOnChangedListener != null)
+ {
+ EditText editText = (EditText) host;
+ mOnChangedListener.onSelectionChanged(editText, editText.getSelectionStart(), editText.getSelectionEnd());
+ }
+ }
+
+ if (eventType == AccessibilityEvent.TYPE_VIEW_FOCUSED)
+ {
+ if ((mOnChangedListener != null) && (host == mFocusedEditText))
+ {
+ mOnChangedListener.onFocusChanged((EditText) host);
+ }
+ }
+ }
+ };
+
+ // --------------------------------------------------------------------------
+ // Constructor
+
+ /** Constructor */
+ public CustomViewGroup(Context context)
+ {
+ this(context, null);
+ }
+
+ /** Constructor */
+ public CustomViewGroup(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ }
+
+ /** Constructor */
+ public CustomViewGroup(Context context, AttributeSet attrs, int defStyle)
+ {
+ super(context, attrs, defStyle);
+ }
+
+ // --------------------------------------------------------------------------
+ // Public methods
+
+ public void setOnChangedListener(OnChangedListener onChangedListener)
+ {
+ mOnChangedListener = onChangedListener;
+ }
+
+ public void initIndexForEditText()
+ {
+ int childCount = getChildCount();
+ int edittextIndex = 0;
+
+ mIndexMap.clear();
+ for (int i = 0; i < childCount; i++)
+ {
+ if (getChildAt(i) instanceof EditText)
+ {
+ mIndexMap.put(edittextIndex, i);
+ edittextIndex++;
+ }
+ }
+ }
+
+ public void resetTextForEditText()
+ {
+ int edittextCount = mIndexMap.size();
+ String [] defaultText = getResources().getStringArray(R.array.default_edittext);
+
+ for (int i = 0; i < edittextCount; i++)
+ {
+ Integer indexChild = mIndexMap.get(i);
+ if (indexChild != null)
+ {
+ EditText editText = (EditText) getChildAt(indexChild);
+ editText.setText(defaultText[i]);
+ }
+ }
+ }
+
+ public EditText findViewByPosition(final float x, final float y)
+ {
+ EditText editText = null;
+
+ int edittextCount = mIndexMap.size();
+ for (int i = 0; i < edittextCount; i++)
+ {
+ Integer childIndex = mIndexMap.get(i);
+ if (childIndex != null)
+ {
+ EditText childView = (EditText) getChildAt(childIndex);
+ float childX = childView.getX();
+ float childY = childView.getY();
+ float childWidth = childView.getWidth();
+ float childHeight = childView.getHeight();
+ RectF childViewRect = new RectF(childX, childY, childX + childWidth, childY + childHeight);
+
+ if (childViewRect.contains(x, y))
+ {
+ editText = childView;
+ break;
+ }
+ }
+ }
+
+ return editText;
+ }
+
+ public int findCursorByPosition(@NonNull EditText editText, final float x, final float y)
+ {
+ float relativeX = x - editText.getX();
+ float relativeY = y - editText.getY();
+ return editText.getOffsetForPosition(relativeX, relativeY);
+ }
+
+ public EditText setFocus(final int index)
+ {
+ Integer childIndex = mIndexMap.get(index);
+ if (childIndex != null)
+ {
+ EditText childView = (EditText) getChildAt(childIndex);
+ setFocus(childView);
+
+ return childView;
+ }
+
+ return null;
+ }
+
+ public void setFocus(EditText editText)
+ {
+ if (editText != mFocusedEditText)
+ {
+ mFocusedEditText = editText;
+ editText.setAccessibilityDelegate(mViewDelegate);
+ editText.requestFocus();
+ }
+ }
+
+ public void setSelection(EditText editText, final float x, final float y, final boolean range, final int color)
+ {
+ if (editText == mFocusedEditText)
+ {
+ int position = findCursorByPosition(editText, x, y);
+ String text = editText.getText().toString();
+
+ if (range && text.length() > position)
+ {
+ String charAtPosition = String.valueOf(text.charAt(position));
+
+ editText.setHighlightColor(color);
+
+ if (charAtPosition.equals(SPACE))
+ {
+ editText.setSelection(position);
+ }
+ else if (PUNCTUATIONS.contains(charAtPosition))
+ {
+ editText.setSelection(position, position + 1);
+ }
+ else
+ {
+ for (int i = 0; i < PUNCTUATIONS.length(); i++)
+ {
+ text = text.replace(PUNCTUATIONS.substring(i, i + 1), SPACE);
+ }
+
+ int start = text.lastIndexOf(SPACE, position) + 1;
+ int end = ((end = text.indexOf(SPACE, position)) == -1) ? text.length() : end;
+
+ if (isMultiLine(editText))
+ {
+ int lineStart = text.lastIndexOf(NEW_LINE, position) + 1;
+ int lineEnd = ((lineEnd = text.indexOf(NEW_LINE, position)) == -1) ? text.length() : lineEnd;
+
+ start = Math.max(start, lineStart);
+ end = Math.min(end, lineEnd);
+ }
+
+ editText.setSelection(start, end);
+ }
+ }
+ else
+ {
+ editText.setSelection(position);
+ }
+ }
+ }
+
+ public void setSelection(EditText editText, @NonNull final RectF selectionRect, final int color)
+ {
+ if (editText == mFocusedEditText)
+ {
+ editText.setHighlightColor(color);
+
+ int start = findCursorByPosition(editText, selectionRect.left, selectionRect.centerY());
+ int end = findCursorByPosition(editText, selectionRect.right, selectionRect.centerY());
+
+ editText.setSelection(start, end);
+ }
+ }
+
+ public void setText(EditText editText, @NonNull final String label)
+ {
+ if (editText == mFocusedEditText)
+ {
+ int start = editText.getSelectionStart();
+ int end = editText.getSelectionEnd();
+
+ StringBuilder builder = new StringBuilder();
+
+ Editable editable = editText.getEditableText();
+ if (start != 0 && !PUNCTUATIONS.contains(label))
+ {
+ if ((editable.length() == end && !editable.toString().substring(end - 1).equals(SPACE)) ||
+ (isMultiLine(editText) && editable.toString().startsWith(NEW_LINE, end)))
+ {
+ char endChar = editable.charAt(start - 1);
+ char startChar = label.charAt(0);
+
+ if (!isCJ(endChar) || !isCJ(startChar))
+ {
+ builder.append(SPACE);
+ }
+ }
+ }
+ builder.append(label);
+ editable.replace(start, end, builder.toString());
+
+ editText.setSelection(start + builder.toString().length());
+ }
+ }
+
+ public void setSpace(EditText editText, final float x, final float y)
+ {
+ if (editText == mFocusedEditText)
+ {
+ int position = findCursorByPosition(editText, x, y);
+
+ editText.setSelection(position);
+ Editable editable = editText.getEditableText();
+
+ int spacePosition = findSpaceNear(editText, SPACE, position);
+ if (spacePosition == -1)
+ {
+ if (findSpaceNear(editText, NEW_LINE, position) == -1 && (editable.length() != position))
+ {
+ editable.insert(position, SPACE);
+ }
+ else if (isMultiLine(editText))
+ {
+ editable.insert(position, NEW_LINE);
+ }
+ }
+ else if (isMultiLine(editText))
+ {
+ editable.replace(spacePosition, spacePosition + 1, NEW_LINE);
+ }
+ }
+ }
+
+ public void eraseSpace(EditText editText, final float x, final float y)
+ {
+ if (editText == mFocusedEditText)
+ {
+ int position = findCursorByPosition(editText, x, y);
+
+ editText.setSelection(position);
+ Editable editable = editText.getEditableText();
+
+ int spacePosition = findSpaceNear(editText, SPACE, position);
+ if (spacePosition != -1)
+ {
+ editable.delete(spacePosition, spacePosition + 1);
+ }
+ else if (isMultiLine(editText))
+ {
+ int newlinePosition = findSpaceNear(editText, NEW_LINE, position);
+ if (newlinePosition != -1)
+ {
+ editable.replace(newlinePosition, newlinePosition + 1, SPACE);
+ }
+ }
+ }
+ }
+
+ public void forwardCursor(EditText editText)
+ {
+ if (editText == mFocusedEditText)
+ {
+ int start = editText.getSelectionStart();
+ int end = editText.getSelectionEnd();
+
+ String text = editText.getText().toString();
+
+ if (start != end)
+ {
+ editText.setSelection(end);
+ }
+ else if (end < text.length())
+ {
+ editText.setSelection(end + 1);
+ }
+ }
+ }
+
+ public void backwardDelete(EditText editText)
+ {
+ if (editText == mFocusedEditText)
+ {
+ int start = editText.getSelectionStart();
+ int end = editText.getSelectionEnd();
+
+ Editable editable = editText.getEditableText();
+
+ if (start != end)
+ {
+ editable.delete(start, end);
+ editText.setSelection(start);
+ }
+ else if (start > 0)
+ {
+ editText.setSelection(start);
+
+ if (isLowSurrogate(editable.charAt(start - 1)))
+ {
+ editable.delete(start - 2, start);
+ }
+ else
+ {
+ editable.delete(start - 1, start);
+ }
+ }
+ }
+ }
+
+ public boolean isMultiLine(EditText editText)
+ {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
+ {
+ return !editText.isSingleLine();
+ }
+ else
+ {
+ return ((editText.getMinLines() > 1) || (editText.getMaxLines() > 1));
+ }
+ }
+
+ private int findSpaceNear(@NonNull EditText editText, final String target, final int position)
+ {
+ int newPosition = -1;
+
+ String text = editText.getText().toString();
+ if ((0 < position) && (position < text.length()))
+ {
+ if (String.valueOf(text.charAt(position)).equals(target))
+ {
+ newPosition = position;
+ }
+ else if (String.valueOf(text.charAt(position - 1)).equals(target))
+ {
+ newPosition = (position - 1);
+ }
+ }
+
+ return newPosition;
+ }
+
+ private boolean isCJ(final char c)
+ {
+ return Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_RADICALS_SUPPLEMENT // 2E80..2EFF
+ || Character.UnicodeBlock.of(c) == Character.UnicodeBlock.KANGXI_RADICALS // 2F00..2FDF
+ || Character.UnicodeBlock.of(c) == Character.UnicodeBlock.HIRAGANA // 3040..309F
+ || Character.UnicodeBlock.of(c) == Character.UnicodeBlock.KATAKANA // 30A0..30FF
+ || Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_STROKES // 31C0..31EF
+ || Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A // 3400..4DBF
+ || Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS // 4E00..9FFF
+ || Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS // F900..FAFF
+ || Character.UnicodeBlock.of(c) == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS // FF00..FFEF
+ || Character.UnicodeBlock.of(c) == Character.UnicodeBlock.KANA_SUPPLEMENT // 1B000..1B0FF
+ || Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B // 20000..2A6DF
+ || Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_C // 2A700..2B73F
+ || Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_D // 2B740..2B81F
+ || Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT; // 2F800..2FA1F
+ }
+
+ private boolean isLowSurrogate(final char c)
+ {
+ return Character.UnicodeBlock.of(c) == Character.UnicodeBlock.LOW_SURROGATES;
+ }
+}
diff --git a/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/DebugView.java b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/DebugView.java
new file mode 100644
index 0000000..4d53870
--- /dev/null
+++ b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/DebugView.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+package com.myscript.iink.samples.writetotype;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Only for debug purpose.
+ * It displaying bounding boxes of view, text in side of view, and touch point.
+ */
+public class DebugView extends View
+{
+ private boolean mDebug = false;
+
+ private Paint mPaint;
+ private RectF mViewBounds = null;
+ private RectF mTextBounds = null;
+ private RectF mRecoBounds = null;
+ private RectF mTouchPoint = null;
+
+ public DebugView(Context context)
+ {
+ this(context, null);
+ }
+
+ public DebugView(Context context, @Nullable AttributeSet attrs)
+ {
+ super(context, attrs);
+ init();
+ }
+
+ public DebugView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)
+ {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ private void init()
+ {
+ mPaint = new Paint();
+ mPaint.setAntiAlias(true);
+ mPaint.setStrokeWidth(1);
+ mPaint.setStrokeJoin(Paint.Join.ROUND);
+ mPaint.setStrokeCap(Paint.Cap.ROUND);
+ }
+
+ public void setDebug(final boolean debug)
+ {
+ mDebug = debug;
+ invalidate();
+ }
+
+ public void setViewBounds(final RectF viewBounds)
+ {
+ mViewBounds = (viewBounds == null) ? null : new RectF(viewBounds);
+ invalidate();
+ }
+
+ public void setViewBounds(final float left, final float top, final float right, final float bottom)
+ {
+ mViewBounds = (left == top && top == right && right == bottom && left == -1) ? null : new RectF(left, top, right, bottom);
+ invalidate();
+ }
+
+ public void setTextBounds(final RectF textBounds)
+ {
+ mTextBounds = (textBounds == null) ? null : new RectF(textBounds);
+ invalidate();
+ }
+
+ public void setTextBounds(final float left, final float top, final float right, final float bottom)
+ {
+ mTextBounds = (left == top && top == right && right == bottom && left == -1) ? null : new RectF(left, top, right, bottom);
+ invalidate();
+ }
+
+ public void setRecoBounds(final RectF recoBounds)
+ {
+ mRecoBounds = (recoBounds == null) ? null : new RectF(recoBounds);
+ invalidate();
+ }
+
+ public void setRecoBounds(final float left, final float top, final float right, final float bottom)
+ {
+ mRecoBounds = (left == top && top == right && right == bottom && left == -1) ? null : new RectF(left, top, right, bottom);
+ invalidate();
+ }
+
+ public void setTouchPoint(final float x, final float y)
+ {
+ mTouchPoint = (x == y && x == -1) ? null : new RectF(x - 5, y - 5, x + 5, y + 5);
+ invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas)
+ {
+ super.onDraw(canvas);
+
+ if (!mDebug)
+ {
+ return;
+ }
+
+ canvas.save();
+ if (mViewBounds != null)
+ {
+ mPaint.setColor(Color.RED);
+ mPaint.setStyle(Paint.Style.STROKE);
+ canvas.drawRect(mViewBounds, mPaint);
+ }
+ if (mTextBounds != null)
+ {
+ mPaint.setColor(Color.BLUE);
+ mPaint.setStyle(Paint.Style.STROKE);
+ canvas.drawRect(mTextBounds, mPaint);
+ }
+ if (mRecoBounds != null)
+ {
+ mPaint.setColor(Color.MAGENTA);
+ mPaint.setStyle(Paint.Style.STROKE);
+ canvas.drawRect(mRecoBounds, mPaint);
+ }
+ if (mTouchPoint != null)
+ {
+ mPaint.setColor(Color.RED);
+ mPaint.setStyle(Paint.Style.FILL);
+ canvas.drawRect(mTouchPoint, mPaint);
+ }
+ canvas.restore();
+ }
+}
diff --git a/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/IInkApplication.java b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/IInkApplication.java
new file mode 100644
index 0000000..9d1680d
--- /dev/null
+++ b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/IInkApplication.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+package com.myscript.iink.samples.writetotype;
+
+import android.app.Application;
+
+import com.myscript.certificate.MyCertificate;
+import com.myscript.iink.Engine;
+
+public class IInkApplication extends Application
+{
+ private static Engine engine;
+
+ public static synchronized Engine getEngine()
+ {
+ if (engine == null)
+ {
+ engine = Engine.create(MyCertificate.getBytes());
+ }
+
+ return engine;
+ }
+}
diff --git a/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/MainActivity.java b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/MainActivity.java
new file mode 100644
index 0000000..39f2715
--- /dev/null
+++ b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/MainActivity.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+package com.myscript.iink.samples.writetotype;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Vibrator;
+import android.text.method.ScrollingMovementMethod;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.myscript.iink.Configuration;
+import com.myscript.iink.Engine;
+import com.myscript.iink.samples.writetotype.core.inkcapture.InkCaptureView;
+import com.myscript.iink.samples.writetotype.im.InputMethodEmulator;
+import com.myscript.iink.samples.writetotype.utils.ErrorActivity;
+
+public class MainActivity extends AppCompatActivity implements WriteToTypeManager.OnDebugListener
+{
+ private static final float INCH_IN_MILLIMETER = 25.4f;
+
+ private CustomViewGroup mViewGroup;
+ private WriteToTypeManager mWriteToTypeManager;
+ private InputMethodEmulator mInputMethod;
+ private TextView mLogView;
+
+ protected Engine mEngine;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ ErrorActivity.setExceptionHandler(getApplicationContext());
+
+ // To disable popping-up soft-keyboard
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
+
+ mEngine = IInkApplication.getEngine();
+
+ // configure recognition
+ Configuration conf = mEngine.getConfiguration();
+ String confDir = "zip://" + getPackageCodePath() + "!/assets/conf";
+ conf.setStringArray("recognizer.configuration-manager.search-path", new String[]{ confDir });
+ setSuperimposed(true);
+
+ mLogView = findViewById(R.id.text_view_log);
+ mLogView.setMovementMethod(new ScrollingMovementMethod());
+
+ mViewGroup = findViewById(R.id.custom_view_group);
+ InkCaptureView inkCaptureView = findViewById(R.id.ink_capture_view);
+ mWriteToTypeManager = new WriteToTypeManager(inkCaptureView);
+ mWriteToTypeManager.setOnDebugListener(this);
+ mWriteToTypeManager.setActiveStylusOnly(false);
+
+ // Configuring iink in post runnable may not need.
+ // It will be replaced by just calling of 'setIInkEngine()' and 'setLanguage' later on.
+ inkCaptureView.post(() -> {
+ mWriteToTypeManager.setIInkEngine(mEngine);
+ mWriteToTypeManager.setLanguage("en_US");
+ mWriteToTypeManager.setCommitTimeout(500);
+
+ final float scaleY = INCH_IN_MILLIMETER / getResources().getDisplayMetrics().ydpi;
+ Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
+ mInputMethod = new InputMethodEmulator(mWriteToTypeManager, mViewGroup, scaleY, vibrator);
+
+ mInputMethod.setDebugView((DebugView) findViewById(R.id.debug_view)); // DEBUG ONLY
+ mInputMethod.setDefaultEditText(0);
+ });
+ }
+
+ private void setSuperimposed(boolean enable)
+ {
+ mEngine.getConfiguration().setString("recognizer.text.configuration.name", enable ? "text-superimposed" : "text");
+ if (mWriteToTypeManager != null)
+ {
+ mWriteToTypeManager.resetTextRecognizer();
+ }
+ }
+
+ private boolean isSuperimposed()
+ {
+ return "text-superimposed".equals(mEngine.getConfiguration().getString("recognizer.text.configuration.name"));
+ }
+
+ @Override
+ protected void onDestroy()
+ {
+ mWriteToTypeManager.destroy();
+
+ // IInkApplication has the ownership, do not close here
+ mEngine = null;
+ super.onDestroy();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu)
+ {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.activity_main, menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item)
+ {
+ if (item.getItemId() == R.id.action_reset)
+ {
+ mInputMethod.resetTextForEditText();
+ mInputMethod.setDefaultEditText(0);
+ mLogView.setText("");
+ }
+
+ if (item.getItemId() == R.id.action_debug_view)
+ {
+ boolean isDebug = !mInputMethod.isDebug();
+ item.setTitle(isDebug ? R.string.menu_debug_on : R.string.menu_debug_off);
+ mInputMethod.setDebug(isDebug);
+ }
+ else if (item.getItemId() == R.id.action_toggle_recognizer)
+ {
+ boolean isSuperimposed = !isSuperimposed();
+ item.setTitle(isSuperimposed ? R.string.menu_recognizer_superimposed_on : R.string.menu_recognizer_superimposed_off);
+ setSuperimposed(isSuperimposed);
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ // --------------------------------------------------------------------------
+ // Implementation of WriteToTypeWidget.OnDebugListener
+
+ @Override
+ public void onDebug(@NonNull final String message)
+ {
+ mLogView.setText(message);
+ }
+
+ @Override
+ public void onError(@NonNull final String message)
+ {
+ mLogView.setText(message);
+ }
+}
\ No newline at end of file
diff --git a/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/WriteToTypeManager.java b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/WriteToTypeManager.java
new file mode 100644
index 0000000..fb7ad5c
--- /dev/null
+++ b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/WriteToTypeManager.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+package com.myscript.iink.samples.writetotype;
+
+import android.graphics.PointF;
+import android.graphics.RectF;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.myscript.iink.Engine;
+import com.myscript.iink.samples.writetotype.core.inkcapture.InkCaptureView;
+import com.myscript.iink.samples.writetotype.core.inkcapture.StrokePoint;
+import com.myscript.iink.samples.writetotype.core.recognition.RecognitionHandler;
+import com.myscript.iink.samples.writetotype.core.recognition.jiixmodel.JiixGesture;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class WriteToTypeManager implements InkCaptureView.OnStrokeListener, RecognitionHandler.OnRecognizedListener
+{
+ private static final int COMMIT_TIMEOUT = 500;
+
+ /**
+ * To store text recognition result by Text Recognizer.
+ */
+ public static class TextResult
+ {
+ public String label;
+ public List candidates;
+ public RectF boundingBox;
+
+ public TextResult()
+ {
+ label = "";
+ candidates = new ArrayList<>();
+ }
+ }
+
+ /**
+ * To store recognition results of both, Text and Gesture Recognizer, and other necessary information.
+ */
+ public static class RecognitionResult
+ {
+ /** State of touch event, ture
if POINTER_UP, otherwise false
. */
+ public boolean isPointerUp;
+ /** State of recognizer, true
if IDLE, otherwise false
. */
+ public boolean isRecognizerIdle;
+
+ /** Stroke bounding rect from raw path information, not from recognizer. */
+ public RectF strokeRect;
+ /** Last stroke points of raw path information, it will use to find exact location of vertical stroke. */
+ public List points;
+
+ /** Result of Gesture Recognizer. */
+ public String gestureType;
+ /** Result of Text Recognizer. */
+ public TextResult textResult;
+
+ public RecognitionResult(final boolean isRecognizerIdle)
+ {
+ this.isRecognizerIdle = isRecognizerIdle;
+ }
+ }
+
+ /** Interface definition for callback of recognition results */
+ public interface OnWriteToTypeListener
+ {
+ boolean onText(@NonNull RecognitionResult recognitionResult, boolean isCommitted);
+ boolean onTopBottom(@NonNull RecognitionResult recognitionResult, boolean isCommitted);
+ boolean onBottomTop(@NonNull RecognitionResult recognitionResult, boolean isCommitted);
+ boolean onLeftRight(@NonNull RecognitionResult recognitionResult, boolean isCommitted);
+ boolean onRightLeft(@NonNull RecognitionResult recognitionResult, boolean isCommitted);
+ boolean onScratch(@NonNull RecognitionResult recognitionResult, boolean isCommitted);
+ boolean onSurround(@NonNull RecognitionResult recognitionResult, boolean isCommitted);
+ boolean onSingleTap(@NonNull RecognitionResult recognitionResult, boolean isCommitted);
+ boolean onDoubleTap(@NonNull RecognitionResult recognitionResult, boolean isCommitted);
+ boolean onLongPress(@NonNull RecognitionResult recognitionResult, boolean isCommitted);
+ }
+
+ /** Interface definition for callbacks invoked when there is an error, or a debug message */
+ public interface OnDebugListener
+ {
+ void onDebug(@NonNull String message);
+ void onError(@NonNull String message);
+ }
+
+ private OnWriteToTypeListener mOnWriteToTypeListener = null;
+ private OnDebugListener mOnDebugListener = null;
+
+ @NonNull
+ private final InkCaptureView mInkCaptureView;
+ private RecognitionHandler mRecognitionHandler = null;
+
+ private Timer mCommitTimer = null;
+ private int mCommitTimeout = COMMIT_TIMEOUT;
+
+ // --------------------------------------------------------------------------
+ // Constructor
+
+ /** Constructor */
+ public WriteToTypeManager(@NonNull InkCaptureView inkCaptureView) {
+ mInkCaptureView = inkCaptureView;
+ mInkCaptureView.setOnStrokeListener(this);
+ }
+
+ // --------------------------------------------------------------------------
+ // Public methods
+
+ /** Register callbacks to be invoked when . */
+ public void setOnWriteToTypeListener(@Nullable OnWriteToTypeListener onWriteToTypeListener)
+ {
+ mOnWriteToTypeListener = onWriteToTypeListener;
+ }
+
+ /** Register callbacks to be invoked when . */
+ public void setOnDebugListener(@Nullable OnDebugListener onDebugListener)
+ {
+ mOnDebugListener = onDebugListener;
+ }
+
+ public void setIInkEngine(@NonNull final Engine engine)
+ {
+ mRecognitionHandler = new RecognitionHandler(engine, mInkCaptureView.getResources().getDisplayMetrics());
+ mRecognitionHandler.setOnRecognizedListener(this);
+ }
+
+ public void resetTextRecognizer()
+ {
+ mRecognitionHandler.resetTextRecognizer();
+ }
+
+ public void setLanguage(@NonNull final String language)
+ {
+ if (mRecognitionHandler != null)
+ {
+ mRecognitionHandler.setLanguage(language);
+ }
+ }
+
+ public void setCommitTimeout(final int commitTimeout)
+ {
+ mCommitTimeout = commitTimeout;
+ }
+
+ /**
+ * A method to set pen input only mode (Pen means an active stylus pen).
+ * You are able to use it when you don't want to allow finger input.
+ * It is 'false' by default which means both of pen and finger are available.
+ * Note: a passive stylus acts as finger input.
+ *
+ * @param activeStylusOnly only pen input is available if it's true.
+ */
+ public void setActiveStylusOnly(final boolean activeStylusOnly)
+ {
+ mInkCaptureView.setActiveStylusOnly(activeStylusOnly);
+ }
+
+ public void clearSession()
+ {
+ if (mRecognitionHandler != null)
+ {
+ mRecognitionHandler.clearSession();
+ }
+ }
+
+ public void destroy()
+ {
+ if (mRecognitionHandler != null)
+ {
+ mRecognitionHandler.destroy();
+ }
+ }
+
+ // --------------------------------------------------------------------------
+ // Implementation of InkCaptureView.OnInkCapturedListener
+
+ @Override
+ public void onStrokeBegin(@NonNull InkCaptureView inkCaptureView, @NonNull StrokePoint point, int pointerId)
+ {
+ if (mRecognitionHandler != null)
+ {
+ mRecognitionHandler.pointerDown(point, pointerId);
+ }
+
+ killCommitTimer();
+ }
+
+ @Override
+ public void onStrokeMove(@NonNull InkCaptureView inkCaptureView, @NonNull StrokePoint point, int pointerId)
+ {
+ if (mRecognitionHandler != null)
+ {
+ mRecognitionHandler.pointerMove(point, pointerId);
+ }
+ }
+
+ @Override
+ public void onStrokeEnd(@NonNull InkCaptureView inkCaptureView, @NonNull StrokePoint point, int pointerId)
+ {
+ if (mRecognitionHandler != null)
+ {
+ mRecognitionHandler.pointerUp(point, pointerId);
+ }
+
+ startCommitTimer();
+ }
+
+ @Override
+ public void onStrokeCancel(@NonNull InkCaptureView inkCaptureView)
+ {
+ mRecognitionHandler.pointerCancel();
+ cancelRecognition();
+ }
+
+ // --------------------------------------------------------------------------
+ // Implementation of RecognitionHandler.OnRecognizedListener
+
+ /**
+ * onRecognitionResult is recognition result callback from {@link RecognitionHandler}.
+ *
+ * @param recognitionResult A structure of recognition result, see {@link RecognitionResult}.
+ * @param isCommitted true
when a commit has been taken into account to current recognition result.
+ * @param debug JIIX string of Gesture result for debug purpose.
+ */
+ @Override
+ public void onRecognitionResult(@NonNull RecognitionResult recognitionResult, final boolean isCommitted, @NonNull String debug)
+ {
+ if (mOnWriteToTypeListener != null)
+ {
+ mInkCaptureView.post(() -> {
+ boolean overlapped = false;
+ boolean isError = false;
+
+ switch (recognitionResult.gestureType)
+ {
+ case JiixGesture.GESTURE_TYPE_EMPTY:
+ case JiixGesture.GESTURE_TYPE_NONE:
+ overlapped = mOnWriteToTypeListener.onText(recognitionResult, isCommitted);
+ break;
+ case JiixGesture.GESTURE_TYPE_TOP_BOTTOM:
+ overlapped = mOnWriteToTypeListener.onTopBottom(recognitionResult, isCommitted);
+ break;
+ case JiixGesture.GESTURE_TYPE_BOTTOM_TOP:
+ overlapped = mOnWriteToTypeListener.onBottomTop(recognitionResult, isCommitted);
+ break;
+ case JiixGesture.GESTURE_TYPE_LEFT_RIGHT:
+ overlapped = mOnWriteToTypeListener.onLeftRight(recognitionResult, isCommitted);
+ break;
+ case JiixGesture.GESTURE_TYPE_RIGHT_LEFT:
+ overlapped = mOnWriteToTypeListener.onRightLeft(recognitionResult, isCommitted);
+ break;
+ case JiixGesture.GESTURE_TYPE_SCRATCH:
+ overlapped = mOnWriteToTypeListener.onScratch(recognitionResult, isCommitted);
+ break;
+ case JiixGesture.GESTURE_TYPE_SURROUND:
+ overlapped = mOnWriteToTypeListener.onSurround(recognitionResult, isCommitted);
+ break;
+ case JiixGesture.GESTURE_TYPE_TAP:
+ overlapped = mOnWriteToTypeListener.onSingleTap(recognitionResult, isCommitted);
+ break;
+ case JiixGesture.GESTURE_TYPE_DOUBLE_TAP:
+ overlapped = mOnWriteToTypeListener.onDoubleTap(recognitionResult, isCommitted);
+ break;
+ case JiixGesture.GESTURE_TYPE_LONG_PRESS:
+ overlapped = mOnWriteToTypeListener.onLongPress(recognitionResult, isCommitted);
+ break;
+ default:
+ isError = true;
+ if (mOnDebugListener != null)
+ {
+ mOnDebugListener.onError("Invalid gesture type\n\n" + debug);
+ }
+ break;
+ }
+
+ if (!isError)
+ {
+ debugMessage(("Event State: " + (isCommitted ? "COMMITTED" : (recognitionResult.isRecognizerIdle ? "IDLE" : "BUSY"))) + "\n" +
+ ("Overlapped: " + (overlapped ? "YES" : "NO")) + "\n" +
+ ("Gesture Type: " + recognitionResult.gestureType) + "\n" +
+ ("Text result: " + recognitionResult.textResult.label + ((recognitionResult.textResult.candidates == null || recognitionResult.textResult.candidates.isEmpty()) ? "" : " " + recognitionResult.textResult.candidates)) + "\n" +
+ ("Stroke rect: " + recognitionResult.strokeRect.toShortString()) + "\n\n" +
+ debug);
+ }
+ });
+ }
+ }
+
+ /**
+ * onError is invoked when an error happens during processing of recognition at {@link RecognitionHandler}.
+ *
+ * @param message error message.
+ */
+ @Override
+ public void onError(@NonNull final String message)
+ {
+ if (mOnDebugListener != null)
+ {
+ mInkCaptureView.post(() -> mOnDebugListener.onError(message));
+ }
+ }
+
+ public void cancelRecognition()
+ {
+ if (mRecognitionHandler != null)
+ {
+ mRecognitionHandler.clear();
+ }
+
+ clearInkCaptureView(false);
+ killCommitTimer();
+ }
+
+ // --------------------------------------------------------------------------
+ // Internal methods of WriteToTypeWidget class
+
+ private void commitRecognition()
+ {
+ if (mRecognitionHandler != null)
+ {
+ mRecognitionHandler.commitRecognition();
+ }
+
+ clearInkCaptureView(true);
+ }
+
+ private void killCommitTimer()
+ {
+ try
+ {
+ if (mCommitTimer != null)
+ {
+ mCommitTimer.cancel();
+ }
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ private void startCommitTimer()
+ {
+ TimerTask task = new TimerTask()
+ {
+ @Override
+ public void run()
+ {
+ commitRecognition();
+ if (mCommitTimer != null)
+ {
+ mCommitTimer.purge();
+ }
+ }
+ };
+
+ mCommitTimer = new Timer();
+ mCommitTimer.schedule(task, mCommitTimeout);
+ }
+
+ private void clearInkCaptureView(final boolean animate)
+ {
+ mInkCaptureView.post(() -> mInkCaptureView.clearStrokes(animate));
+ }
+
+ private void debugMessage(@NonNull final String message)
+ {
+ if (mOnDebugListener != null)
+ {
+ mOnDebugListener.onDebug(message);
+ }
+ }
+}
diff --git a/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/inkcapture/FadeoutView.java b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/inkcapture/FadeoutView.java
new file mode 100644
index 0000000..def59f4
--- /dev/null
+++ b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/inkcapture/FadeoutView.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+package com.myscript.iink.samples.writetotype.core.inkcapture;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class FadeoutView extends View implements Animation.AnimationListener
+{
+ private static final float START_ALPHA = 1.f;
+ private static final float END_ALPHA = 0.f;
+ private static final int DURATION = 500;
+
+ /** Interface definition for callbacks invoked when fadeout animation ended. */
+ public interface OnFadeoutViewListener
+ {
+ void onStrokeViewFadeoutAnimationEnd(@NonNull FadeoutView v);
+ }
+
+ private Path mPath = null;
+ private Paint mPaint = null;
+ private OnFadeoutViewListener mOnFadeoutViewListener = null;
+
+ // --------------------------------------------------------------------------
+ // Constructor
+
+ /** Constructor */
+ public FadeoutView(@NonNull Context context)
+ {
+ this(context, null);
+ }
+
+ /** Constructor */
+ public FadeoutView(@NonNull Context context, @Nullable AttributeSet attrs)
+ {
+ super(context, attrs);
+ }
+
+ /** Constructor */
+ public FadeoutView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)
+ {
+ super(context, attrs, defStyleAttr);
+ }
+
+ // --------------------------------------------------------------------------
+ // Public methods
+
+ /** Register callbacks invoked when fadeout animation ended. */
+ public void setOnFadeoutViewListener(@Nullable OnFadeoutViewListener onFadeoutViewListener)
+ {
+ mOnFadeoutViewListener = onFadeoutViewListener;
+ }
+
+ public void setPath(@NonNull final Path path, @NonNull final Paint paint)
+ {
+ mPath = path;
+ mPaint = paint;
+ }
+
+ public void clearPath()
+ {
+ mPath.reset();
+ }
+
+ public Path getPath()
+ {
+ return mPath;
+ }
+
+ public void fadeout()
+ {
+ final AlphaAnimation animation = new AlphaAnimation(START_ALPHA, END_ALPHA);
+ animation.setAnimationListener(this);
+ animation.setDuration(DURATION);
+ postDelayed(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ startAnimation(animation);
+ }
+ }, 0);
+ }
+
+ // --------------------------------------------------------------------------
+ // Implementation of View.onDraw to draw a stroke with fadeout
+
+ @Override
+ protected void onDraw(Canvas canvas)
+ {
+ if (mPath != null && !mPath.isEmpty() && mPaint != null)
+ {
+ canvas.save();
+ canvas.drawPath(mPath, mPaint);
+ canvas.restore();
+ }
+ }
+
+ // --------------------------------------------------------------------------
+ // Implementation of Animation.AnimationListener
+
+ @Override
+ public void onAnimationStart(Animation animation)
+ {
+ // no operation
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation)
+ {
+ // no operation
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation)
+ {
+ final FadeoutView strokeView = this;
+ post(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ if (mOnFadeoutViewListener != null)
+ {
+ mOnFadeoutViewListener.onStrokeViewFadeoutAnimationEnd(strokeView);
+ }
+ }
+ });
+ }
+}
diff --git a/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/inkcapture/InkCaptureView.java b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/inkcapture/InkCaptureView.java
new file mode 100644
index 0000000..4bc2f3b
--- /dev/null
+++ b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/inkcapture/InkCaptureView.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+package com.myscript.iink.samples.writetotype.core.inkcapture;
+
+import android.content.Context;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+import java.util.HashMap;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class InkCaptureView extends FrameLayout implements InkView.OnStrokeDrawListener, FadeoutView.OnFadeoutViewListener
+{
+ /** Interface definition for callbacks invoked when capturing a stroke. */
+ public interface OnStrokeListener
+ {
+ void onStrokeBegin(@NonNull InkCaptureView inkCaptureView, @NonNull StrokePoint point, int pointerId);
+ void onStrokeMove(@NonNull InkCaptureView inkCaptureView, @NonNull StrokePoint point, int pointerId);
+ void onStrokeEnd(@NonNull InkCaptureView inkCaptureView, @NonNull StrokePoint point, int pointerId);
+ void onStrokeCancel(@NonNull InkCaptureView inkCaptureView);
+ }
+
+ private static final int DEFAULT_INK_WIDTH = 5;
+ private static final int DEFAULT_INK_COLOR = 0xFF33B5E5;
+
+ private OnStrokeListener mOnStrokeListener = null;
+
+ private Paint mPaint;
+ private InkView mInkView;
+ private HashMap mFadeoutViews;
+ private int mPointerId;
+
+ /** Flag to enable 'active stylus input' only mode. If it's set by true, finger input is disabled. */
+ private boolean mActiveStylusOnly = false;
+
+ // --------------------------------------------------------------------------
+ // Constructor
+
+ /** Constructor */
+ public InkCaptureView(@NonNull Context context)
+ {
+ this(context, null);
+ }
+
+ /** Constructor */
+ public InkCaptureView(@NonNull Context context, @Nullable AttributeSet attrs)
+ {
+ super(context, attrs);
+ init();
+ }
+
+ /** Constructor */
+ public InkCaptureView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)
+ {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ /** Initialize this view */
+ private void init()
+ {
+ mPaint = new Paint();
+ mPaint.setAntiAlias(true);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setColor(DEFAULT_INK_COLOR);
+ mPaint.setStrokeWidth(DEFAULT_INK_WIDTH);
+ mPaint.setStrokeJoin(Paint.Join.ROUND);
+ mPaint.setStrokeCap(Paint.Cap.ROUND);
+
+ mFadeoutViews = new HashMap<>();
+
+ mInkView = new InkView(getContext());
+ mInkView.setPaint(mPaint);
+ mInkView.setOnStrokeDrawListener(this);
+
+ addView(mInkView);
+ }
+
+ // --------------------------------------------------------------------------
+ // Public methods
+
+ /** Register callbacks to be invoked when capturing a stroke. */
+ public void setOnStrokeListener(@Nullable OnStrokeListener onStrokeListener)
+ {
+ mOnStrokeListener = onStrokeListener;
+ }
+
+ public void setActiveStylusOnly(final boolean activeStylusOnly)
+ {
+ mActiveStylusOnly = activeStylusOnly;
+ }
+
+ public void clearStrokes(boolean animated)
+ {
+ if (!mFadeoutViews.isEmpty())
+ {
+ if (animated)
+ {
+ for (FadeoutView fadeoutView : mFadeoutViews.values())
+ {
+ fadeoutView.fadeout();
+ }
+ }
+ else
+ {
+ for (FadeoutView fadeoutView : mFadeoutViews.values())
+ {
+ fadeoutView.setOnFadeoutViewListener(null);
+ fadeoutView.clearPath();
+ removeView(fadeoutView);
+ }
+ mFadeoutViews.clear();
+ }
+ }
+ }
+
+ // --------------------------------------------------------------------------
+ // Implementation of InkView.OnInkEventListener
+
+ @Override
+ public boolean shouldBeginDraw(@NonNull InkView inkView, @NonNull StrokePoint point, int pointerId, boolean isStylus)
+ {
+ mPointerId = pointerId;
+
+ // the return value MUST be true to draw stroke on canvas
+ return !mActiveStylusOnly || isStylus;
+ }
+
+ @Override
+ public void onStrokeDrawBegin(@NonNull InkView inkView, @NonNull StrokePoint point)
+ {
+ if (mOnStrokeListener != null)
+ {
+ mOnStrokeListener.onStrokeBegin(this, point, mPointerId);
+ }
+ }
+
+ @Override
+ public void onStrokeDrawMove(@NonNull InkView inkView, @NonNull StrokePoint point)
+ {
+ if (mOnStrokeListener != null)
+ {
+ mOnStrokeListener.onStrokeMove(this, point, mPointerId);
+ }
+ }
+
+ @Override
+ public void onStrokeDrawEnd(@NonNull InkView inkView, @NonNull StrokePoint point, @NonNull Path path)
+ {
+ if (!mFadeoutViews.containsKey(path))
+ {
+ FadeoutView fadeoutView = new FadeoutView(getContext());
+ fadeoutView.setPath(path, mPaint);
+ fadeoutView.setOnFadeoutViewListener(this);
+ // When you want to fadeout a stroke when stroke draw end,
+ // you can call "fadeoutView.fadeout()" here.
+ mFadeoutViews.put(path, fadeoutView);
+ addView(fadeoutView);
+ }
+
+ if (mOnStrokeListener != null)
+ {
+ mOnStrokeListener.onStrokeEnd(this, point, mPointerId);
+ }
+
+ mInkView.clear();
+ }
+
+ @Override
+ public void onStrokeDrawCancel(@NonNull InkView inkView)
+ {
+ if (mOnStrokeListener != null)
+ {
+ mOnStrokeListener.onStrokeCancel(this);
+ }
+
+ mInkView.clear();
+ }
+
+ // --------------------------------------------------------------------------
+ // Implementation of FadeoutView.OnFadeoutViewListener
+
+ @Override
+ public void onStrokeViewFadeoutAnimationEnd(@NonNull final FadeoutView fadeoutView)
+ {
+ removeStroke(fadeoutView.getPath());
+ }
+
+ private void removeStroke(@Nullable final Path path)
+ {
+ if (path != null && mFadeoutViews.containsKey(path))
+ {
+ FadeoutView fadeoutView = mFadeoutViews.get(path);
+ mFadeoutViews.remove(path);
+ if (fadeoutView != null)
+ {
+ fadeoutView.setOnFadeoutViewListener(null);
+ fadeoutView.clearPath();
+ removeView(fadeoutView);
+ }
+ }
+ }
+}
diff --git a/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/inkcapture/InkView.java b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/inkcapture/InkView.java
new file mode 100644
index 0000000..9299030
--- /dev/null
+++ b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/inkcapture/InkView.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+package com.myscript.iink.samples.writetotype.core.inkcapture;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class InkView extends View
+{
+ /** Interface definition for callbacks invoked when drawing a stroke. */
+ public interface OnStrokeDrawListener
+ {
+ boolean shouldBeginDraw(@NonNull InkView inkView, @NonNull StrokePoint point, int pointerId, boolean isStylus);
+ void onStrokeDrawBegin(@NonNull InkView inkView, @NonNull StrokePoint point);
+ void onStrokeDrawMove(@NonNull InkView inkView, @NonNull StrokePoint point);
+ void onStrokeDrawEnd(@NonNull InkView inkView, @NonNull StrokePoint point, @NonNull Path path);
+ void onStrokeDrawCancel(@NonNull InkView inkView);
+ }
+
+ private OnStrokeDrawListener mOnStrokeDrawListener = null;
+
+ private Paint mPaint = null;
+ private Path mPath;
+
+ private int mActivePointerId = -1;
+ private float mLastPointX;
+ private float mLastPointY;
+
+ // --------------------------------------------------------------------------
+ // Constructor
+
+ /** Constructor */
+ public InkView(@NonNull Context context)
+ {
+ this(context, null);
+ }
+
+ /** Constructor */
+ public InkView(@NonNull Context context, @Nullable AttributeSet attrs)
+ {
+ super(context, attrs);
+ init();
+ }
+
+ /** Constructor */
+ public InkView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)
+ {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ /** Initialize this view */
+ private void init()
+ {
+ mPath = new Path();
+ }
+
+ // --------------------------------------------------------------------------
+ // Public methods to configure this view
+
+ /** Register callbacks invoked when drawing a stroke. */
+ public void setOnStrokeDrawListener(@Nullable OnStrokeDrawListener onStrokeDrawListener)
+ {
+ mOnStrokeDrawListener = onStrokeDrawListener;
+ }
+
+ public void setPaint(final Paint paint)
+ {
+ mPaint = paint;
+ }
+
+ public void clear()
+ {
+ mPath.reset();
+ invalidate();
+ }
+
+ // --------------------------------------------------------------------------
+ // Implementation of View.onDraw to draw a stroke
+
+ @Override
+ protected void onDraw(Canvas canvas)
+ {
+ super.onDraw(canvas);
+
+ if (!mPath.isEmpty() && mPaint != null)
+ {
+ canvas.save();
+ canvas.drawPath(mPath, mPaint);
+ canvas.restore();
+ }
+ }
+
+ // --------------------------------------------------------------------------
+ // Mouse event handling to draw a stroke and detect necessary gestures
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event)
+ {
+ final int actionMasked = event.getActionMasked();
+ switch (actionMasked)
+ {
+ case MotionEvent.ACTION_POINTER_DOWN:
+ case MotionEvent.ACTION_DOWN:
+ processActionDownEvent(event);
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ processActionMoveEvent(event);
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ if (isStylusButtonPressed(event))
+ {
+ processActionUpEvent();
+ }
+ else
+ {
+ processActionCancelEvent(event);
+ }
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP:
+ case MotionEvent.ACTION_UP:
+ processActionUpEvent(event);
+ break;
+
+ default:
+ // ignore unhandled motion event
+ break;
+ }
+
+ /*
+ * Must return "true" here to not pass MotionEvents to bottom layers.
+ */
+ return true;
+ }
+
+ private void processActionDownEvent(@NonNull final MotionEvent event)
+ {
+ if (mActivePointerId == -1)
+ {
+ final boolean isStylus = isStylusMotionEvent(event);
+
+ final int pointerIndex = event.getActionIndex();
+ final int pointerId = event.getPointerId(pointerIndex);
+
+ final float x = event.getX(pointerIndex);
+ final float y = event.getY(pointerIndex);
+ final float p = event.getPressure(pointerIndex);
+ final long t = event.getEventTime();
+
+ StrokePoint point = new StrokePoint(x, y, p, t, StrokePoint.EventType.EVENT_TYPE_DOWN);
+ if (mOnStrokeDrawListener != null && mOnStrokeDrawListener.shouldBeginDraw(this, point, pointerId, isStylus))
+ {
+ mActivePointerId = pointerId;
+
+ mPath.moveTo(x, y);
+ mLastPointX = x;
+ mLastPointY = y;
+
+ mOnStrokeDrawListener.onStrokeDrawBegin(this, point);
+ invalidate();
+ }
+ }
+ }
+
+ private void processActionMoveEvent(@NonNull final MotionEvent event)
+ {
+ final int pointerIndex = event.findPointerIndex(mActivePointerId);
+ if (pointerIndex != -1)
+ {
+ final int size = event.getHistorySize();
+ for (int i = 0; i < size; i++)
+ {
+ final float hx = event.getHistoricalX(pointerIndex, i);
+ final float hy = event.getHistoricalY(pointerIndex, i);
+ final float hp = event.getHistoricalPressure(pointerIndex, i);
+ final long ht = event.getHistoricalEventTime(i);
+ strokeDrawMove(hx, hy, hp, ht);
+ }
+
+ final float x = event.getX(pointerIndex);
+ final float y = event.getY(pointerIndex);
+ final float p = event.getPressure(pointerIndex);
+ final long t = event.getEventTime();
+ strokeDrawMove(x, y, p, t);
+
+ invalidate();
+ }
+ }
+
+ private void strokeDrawMove(final float x, final float y, final float p, final long t)
+ {
+ if (mOnStrokeDrawListener != null)
+ {
+ mPath.quadTo((mLastPointX + x) / 2.f, (mLastPointY + y) / 2.f, x, y);
+ mLastPointX = x;
+ mLastPointY = y;
+
+ StrokePoint point = new StrokePoint(x, y, p, t, StrokePoint.EventType.EVENT_TYPE_MOVE);
+ mOnStrokeDrawListener.onStrokeDrawMove(this, point);
+ }
+ }
+
+ private void processActionUpEvent(@NonNull final MotionEvent event)
+ {
+ final int pointerIndex = event.getActionIndex();
+ final int pointerId = event.getPointerId(pointerIndex);
+
+ if (pointerId == mActivePointerId)
+ {
+ final float x = event.getX(pointerIndex);
+ final float y = event.getY(pointerIndex);
+ final float p = event.getPressure(pointerIndex);
+ final long t = event.getEventTime();
+
+ if (mOnStrokeDrawListener != null)
+ {
+ mPath.quadTo((mLastPointX + x) / 2.f, (mLastPointY + y) / 2.f, x, y);
+ mLastPointX = x;
+ mLastPointY = y;
+
+ Path path = new Path(mPath);
+ StrokePoint point = new StrokePoint(x, y, p, t, StrokePoint.EventType.EVENT_TYPE_UP);
+ mOnStrokeDrawListener.onStrokeDrawEnd(this, point, path);
+ invalidate();
+ }
+
+ mActivePointerId = -1;
+ }
+ }
+
+ private void processActionUpEvent()
+ {
+ if (mOnStrokeDrawListener != null)
+ {
+ mOnStrokeDrawListener.onStrokeDrawCancel(this);
+ }
+
+ invalidate();
+ mActivePointerId = -1;
+ }
+
+ private void processActionCancelEvent(@NonNull final MotionEvent event)
+ {
+ final int pointerIndex = event.getActionIndex();
+ final int pointerId = event.getPointerId(pointerIndex);
+
+ if (pointerId == mActivePointerId)
+ {
+ if (mOnStrokeDrawListener != null)
+ {
+ mOnStrokeDrawListener.onStrokeDrawCancel(this);
+ }
+
+ clear();
+ mActivePointerId = -1;
+ }
+ }
+
+ // --------------------------------------------------------------------------
+ // Stylus detection
+
+ private boolean isStylusMotionEvent(@NonNull final MotionEvent event)
+ {
+ final int pointerIndex = event.getActionIndex();
+ final int toolType = event.getToolType(pointerIndex);
+ return toolType == MotionEvent.TOOL_TYPE_STYLUS;
+ }
+
+ private boolean isStylusButtonPressed(@NonNull final MotionEvent event)
+ {
+ final int buttonState = event.getButtonState();
+ return ((buttonState & (MotionEvent.BUTTON_SECONDARY | MotionEvent.BUTTON_TERTIARY)) != 0);
+ }
+}
diff --git a/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/inkcapture/StrokePoint.java b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/inkcapture/StrokePoint.java
new file mode 100644
index 0000000..b8a7529
--- /dev/null
+++ b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/inkcapture/StrokePoint.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+package com.myscript.iink.samples.writetotype.core.inkcapture;
+
+import androidx.annotation.NonNull;
+
+public class StrokePoint
+{
+ public enum EventType
+ {
+ EVENT_TYPE_DOWN,
+ EVENT_TYPE_MOVE,
+ EVENT_TYPE_UP
+ }
+
+ public final float x;
+ public final float y;
+ public final float p;
+ public final long t;
+ public final EventType eventType;
+
+ public StrokePoint(final float x, final float y, final float p, final long t, @NonNull final EventType eventType)
+ {
+ this.x = x;
+ this.y = y;
+ this.p = p;
+ this.t = t;
+ this.eventType = eventType;
+ }
+}
diff --git a/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/recognition/RecognitionHandler.java b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/recognition/RecognitionHandler.java
new file mode 100644
index 0000000..d7f83d8
--- /dev/null
+++ b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/recognition/RecognitionHandler.java
@@ -0,0 +1,548 @@
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+package com.myscript.iink.samples.writetotype.core.recognition;
+
+import android.graphics.RectF;
+import android.util.DisplayMetrics;
+
+import com.google.gson.Gson;
+import com.myscript.iink.Configuration;
+import com.myscript.iink.Engine;
+import com.myscript.iink.MimeType;
+import com.myscript.iink.Recognizer;
+import com.myscript.iink.graphics.Point;
+import com.myscript.iink.samples.writetotype.WriteToTypeManager.RecognitionResult;
+import com.myscript.iink.samples.writetotype.WriteToTypeManager.TextResult;
+import com.myscript.iink.samples.writetotype.core.inkcapture.StrokePoint;
+import com.myscript.iink.samples.writetotype.core.recognition.jiixmodel.JiixGesture;
+import com.myscript.iink.samples.writetotype.core.recognition.jiixmodel.item.JiixBoundingBox;
+import com.myscript.iink.samples.writetotype.core.recognition.jiixmodel.JiixText;
+import com.myscript.iink.samples.writetotype.core.recognition.jiixmodel.item.JiixWord;
+import com.myscript.iink.samples.writetotype.core.recognition.strokemodel.CustomPath;
+
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class RecognitionHandler
+{
+ // Constants for the recognizer type
+ private static final String RECOGNIZER_TYPE_TEXT = "Text";
+ private static final String RECOGNIZER_TYPE_GESTURE = "Gesture";
+
+ private static final float INCH_IN_MILLIMETER = 25.4f;
+
+ private static final float COMMA_HEIGHT_RATIO = 0.5f;
+ private static final String CHARACTER_DOT = ".";
+ private static final String CHARACTER_COMMA = ",";
+
+ /** Interface definition for callbacks invoked when recognizing input strokes. */
+ public interface OnRecognizedListener
+ {
+ void onRecognitionResult(@NonNull RecognitionResult recognitionResult, boolean isCommitted, @NonNull String debug);
+
+ void onError(@NonNull String message);
+ }
+
+ private OnRecognizedListener mOnRecognizedListener = null;
+
+ private final Engine mEngine;
+ private Recognizer mTextRecognizer;
+ private Recognizer mGestureRecognizer;
+
+ private final CustomPath mPath = new CustomPath();
+
+ private JiixBoundingBox mWordBoundingBox = new JiixBoundingBox();
+
+ private final float mScaleX;
+ private final float mScaleY;
+
+ private Thread mRecognitionThread;
+
+ // Flags for control recognition thread
+ private boolean mIsRecognitionThreadAlive;
+ private boolean mIsResultAvailable;
+ private boolean mShouldCommit;
+ private boolean mShouldClear;
+
+ private int mCurrentPointerId = -1;
+ private StrokePoint.EventType mCurrentPointerEvent;
+
+ // Variables to control feeding strokes to Gesture/Text recognizer
+ private int mStrokeCount = 0;
+ private String mLastGestureType = JiixGesture.GESTURE_TYPE_EMPTY;
+
+ // --------------------------------------------------------------------------
+ // Constructor
+
+ /** Constructor */
+ public RecognitionHandler(@NonNull final Engine engine, @NonNull DisplayMetrics displayMetrics)
+ {
+ mEngine = engine;
+
+ mScaleX = INCH_IN_MILLIMETER / displayMetrics.xdpi;
+ mScaleY = INCH_IN_MILLIMETER / displayMetrics.ydpi;
+
+ mTextRecognizer = mEngine.createRecognizer(mScaleX, mScaleY, RECOGNIZER_TYPE_TEXT);
+ mGestureRecognizer = mEngine.createRecognizer(mScaleX, mScaleY, RECOGNIZER_TYPE_GESTURE);
+
+ startRecognitionThread();
+ }
+
+ public void resetTextRecognizer()
+ {
+ if (mTextRecognizer != null)
+ {
+ mTextRecognizer.close();
+ mTextRecognizer = null;
+ }
+
+ mTextRecognizer = mEngine.createRecognizer(mScaleX, mScaleY, RECOGNIZER_TYPE_TEXT);
+ }
+
+ // --------------------------------------------------------------------------
+ // Public methods
+
+ /** Register callbacks to be invoked when recognizing input strokes. */
+ public void setOnRecognizedListener(@Nullable OnRecognizedListener onRecognizedListener)
+ {
+ mOnRecognizedListener = onRecognizedListener;
+ }
+
+ /** Configure recognition with this method, otherwise, 'en_US' will be set by default. */
+ public void setLanguage(@NonNull final String language)
+ {
+ if (mEngine != null)
+ {
+ Configuration conf = mEngine.getConfiguration();
+ conf.setString("recognizer.lang", language);
+
+ destroy();
+ mTextRecognizer = mEngine.createRecognizer(mScaleX, mScaleY, RECOGNIZER_TYPE_TEXT);
+ mGestureRecognizer = mEngine.createRecognizer(mScaleX, mScaleY, RECOGNIZER_TYPE_GESTURE);
+
+ startRecognitionThread();
+ }
+ }
+
+ //
+ // For following event registering methods - pointerDown, pointerMove and pointerUp.
+ //
+ // Variables mStrokeCount
and mLastGestureType
are used for stroke injection management.
+ // - Text Recognizer:
+ // All pointers will be injected to the Text Recognizer.
+ // - Gesture Recognizer:
+ // As soon as the first pointer down, mStrokeCount became 1, and it is increased for every pointer down.
+ // Pointers will be injected to Gesture Recognizer when mStrokeCount < 2 (which mean only for the first stroke).
+ // There is an exception, double-tap
gesture requires 2 strokes, and it is always preceded by tap
gesture.
+ // So, as an exception, if the preceded gesture is tap
(even if mStrokeCount >= 2), pointers are injected to Gesture Recognizer.
+ //
+
+ /** Register pointer down event to iink recognizer. */
+ public void pointerDown(@NonNull StrokePoint point, int pointerId)
+ {
+ try
+ {
+ if (mCurrentPointerId == -1 && !mShouldClear)
+ {
+ mStrokeCount++;
+ mPath.moveTo(point.x, point.y);
+
+ mTextRecognizer.pointerDown(point.x, point.y, point.t, point.p);
+ if (mStrokeCount < 2 || mLastGestureType.equals(JiixGesture.GESTURE_TYPE_TAP))
+ {
+ mGestureRecognizer.pointerDown(point.x, point.y, point.t, point.p);
+ }
+
+ mIsResultAvailable = true;
+
+ mCurrentPointerEvent = point.eventType;
+
+ mCurrentPointerId = pointerId;
+ }
+ }
+ catch (Exception e)
+ {
+ // ignore spurious invalid touch events
+ }
+ }
+
+ /** Register pointer move event to iink recognizer. */
+ public void pointerMove(@NonNull StrokePoint point, int pointerId)
+ {
+ try
+ {
+ if (mCurrentPointerId == pointerId)
+ {
+ mPath.lineTo(point.x, point.y);
+
+ mTextRecognizer.pointerMove(point.x, point.y, point.t, point.p);
+ if (mStrokeCount < 2 || mLastGestureType.equals(JiixGesture.GESTURE_TYPE_TAP))
+ {
+ mGestureRecognizer.pointerMove(point.x, point.y, point.t, point.p);
+ }
+
+ mCurrentPointerEvent = point.eventType;
+ }
+ }
+ catch (Exception e)
+ {
+ // ignore spurious invalid touch events
+ }
+ }
+
+ /** Register pointer up event to iink recognizer. */
+ public void pointerUp(@NonNull StrokePoint point, int pointerId)
+ {
+ try
+ {
+ if (mCurrentPointerId == pointerId)
+ {
+ mPath.lineTo(point.x, point.y);
+
+ mTextRecognizer.pointerUp(point.x, point.y, point.t, point.p);
+ if (mStrokeCount < 2 || mLastGestureType.equals(JiixGesture.GESTURE_TYPE_TAP))
+ {
+ mGestureRecognizer.pointerUp(point.x, point.y, point.t, point.p);
+ }
+
+ mCurrentPointerEvent = point.eventType;
+
+ mCurrentPointerId = -1;
+ }
+ }
+ catch (Exception e)
+ {
+ // ignore spurious invalid touch events
+ }
+ }
+
+ public void pointerCancel()
+ {
+ mTextRecognizer.pointerCancel();
+ mGestureRecognizer.pointerCancel();
+ }
+
+ /** Commit timeout expired, so that, make recognition result to take it into account. */
+ public void commitRecognition()
+ {
+ mShouldCommit = true;
+ mIsResultAvailable = false;
+ }
+
+ /**
+ * Clear current session of recognition.
+ * It's usually called when Input focus changed or to reset
+ * previous text bounding box to determine dot recognition.
+ **/
+ public void clearSession()
+ {
+ mWordBoundingBox = new JiixBoundingBox();
+ }
+
+ /** Clear iink recognizers. */
+ public void clear()
+ {
+ mShouldClear = true;
+ mIsResultAvailable = false;
+ }
+
+ /** Destroy iink recognizers. */
+ public void destroy()
+ {
+ stopRecognitionThread();
+
+ if (mTextRecognizer != null)
+ {
+ mTextRecognizer.close();
+ mTextRecognizer = null;
+ }
+
+ if (mGestureRecognizer != null)
+ {
+ mGestureRecognizer.close();
+ mGestureRecognizer = null;
+ }
+ }
+
+ // --------------------------------------------------------------------------
+ // Private methods used in this class
+
+ private void startRecognitionThread()
+ {
+ mIsRecognitionThreadAlive = true;
+
+ mIsResultAvailable = false;
+ mShouldCommit = false;
+ mShouldClear = false;
+
+ mRecognitionThread = new Thread(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ while (mIsRecognitionThreadAlive)
+ {
+ try
+ {
+ Thread.sleep(10);
+
+ if (mShouldCommit && !mShouldClear)
+ {
+ mTextRecognizer.waitForIdle();
+ mGestureRecognizer.waitForIdle();
+ doRecognition(true);
+
+ clearRecognizers();
+ mShouldCommit = false;
+ }
+
+ if (mIsResultAvailable && !mShouldClear)
+ {
+ doRecognition(false);
+ mIsResultAvailable = !(mTextRecognizer.isIdle() && mGestureRecognizer.isIdle());
+ }
+
+ if (mShouldClear)
+ {
+ clearRecognizers();
+ mShouldClear = false;
+ }
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+ });
+
+ mRecognitionThread.setPriority(Thread.MAX_PRIORITY);
+ mRecognitionThread.start();
+ }
+
+ private void stopRecognitionThread()
+ {
+ try
+ {
+ clearRecognizers();
+
+ mIsRecognitionThreadAlive = false;
+ mRecognitionThread.join();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ private void doRecognition(final boolean isCommitted)
+ {
+ if (mOnRecognizedListener != null)
+ {
+ boolean isRecognizerIdle = mTextRecognizer.isIdle() && mGestureRecognizer.isIdle();
+
+ String gestureJiix = "";
+ String gestureType = JiixGesture.GESTURE_TYPE_EMPTY;
+
+ if (mStrokeCount < 2 || mLastGestureType.equals(JiixGesture.GESTURE_TYPE_TAP))
+ {
+ gestureJiix = mGestureRecognizer.getResult(MimeType.JIIX);
+ gestureType = parseGestureJiix(gestureJiix);
+ }
+
+ TextResult textResult = parseTextJiix(mTextRecognizer.getResult(MimeType.JIIX));
+
+ if (gestureType != null)
+ {
+ RecognitionResult result = combineTextAndGestureResult(isRecognizerIdle, textResult, gestureType);
+ mOnRecognizedListener.onRecognitionResult(result, isCommitted, gestureJiix);
+ mLastGestureType = gestureType;
+ }
+ }
+ }
+
+ /**
+ * Following codes will parse the exported JIIX of Text Recognizer
+ **/
+ @Nullable
+ private TextResult parseTextJiix(@Nullable final String jiixString)
+ {
+ TextResult result = new TextResult();
+
+ if (notifyError(jiixString == null, "ERROR: No exported result from TEXT."))
+ {
+ clear();
+ return null;
+ }
+
+ try
+ {
+ JiixText textResult = new Gson().fromJson(jiixString, JiixText.class);
+
+ if (textResult != null && textResult.type.equals(RECOGNIZER_TYPE_TEXT) && textResult.words.size() > 0)
+ {
+ final String label = textResult.label;
+
+ if (label.length() == 1)
+ {
+ JiixWord word = textResult.words.get(0);
+ List candidates = word.candidates;
+ result.candidates = candidates;
+
+ if (candidates.get(0).equals(CHARACTER_DOT))
+ {
+ result.label = candidates.get(0);
+ }
+ else
+ {
+ JiixBoundingBox boundingBox = word.boundingBox;
+ if (candidates.contains(CHARACTER_COMMA) &&
+ (boundingBox.height <= (mWordBoundingBox.height * COMMA_HEIGHT_RATIO)))
+ {
+ result.label = CHARACTER_COMMA;
+ }
+ else
+ {
+ result.label = candidates.get(0);
+ }
+ }
+
+ result.boundingBox = getRectFromBoundingBox(word.boundingBox);
+ }
+ else
+ {
+ result.label = label;
+ JiixWord word = getLongestWord(textResult.words);
+
+ if ((word != null) && (word.boundingBox != null))
+ {
+ JiixBoundingBox boundingBox = word.boundingBox;
+ mWordBoundingBox.x = boundingBox.x;
+ mWordBoundingBox.y = boundingBox.y;
+ mWordBoundingBox.width = boundingBox.width;
+ mWordBoundingBox.height = boundingBox.height;
+
+ result.boundingBox = getRectFromBoundingBox(boundingBox);
+ }
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+
+ return result;
+ }
+
+ /**
+ * Following codes will parse the exported JIIX of Gesture Recognizer.
+ **/
+ @Nullable
+ private String parseGestureJiix(@Nullable final String jiixString)
+ {
+ if (notifyError(jiixString == null, "ERROR: No exported result from GESTURE."))
+ {
+ return null;
+ }
+
+ String gestureType = null;
+
+ try
+ {
+ JiixGesture gestureResult = new Gson().fromJson(jiixString, JiixGesture.class);
+
+ if (gestureResult != null && gestureResult.type.equals(RECOGNIZER_TYPE_GESTURE) && gestureResult.gestures.size() > 0)
+ {
+ JiixGesture.Gesture gesture = gestureResult.gestures.get(0);
+ gestureType = gesture.type;
+ }
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+
+ return gestureType;
+ }
+
+ @NonNull
+ private RecognitionResult combineTextAndGestureResult(final boolean isRecognizerIdle, TextResult textResult, String gestureType)
+ {
+ RecognitionResult recognitionResult = new RecognitionResult(isRecognizerIdle);
+ recognitionResult.isPointerUp = (mCurrentPointerEvent == StrokePoint.EventType.EVENT_TYPE_UP);
+ recognitionResult.strokeRect = mPath.getBoundingRect();
+
+ recognitionResult.gestureType = gestureType;
+ recognitionResult.points = mPath.getPoints();
+
+ recognitionResult.textResult = textResult;
+
+ return recognitionResult;
+ }
+
+ @NonNull
+ private RectF getRectFromBoundingBox(@NonNull JiixBoundingBox boundingBox)
+ {
+ final float x = boundingBox.x / mScaleX;
+ final float y = boundingBox.y / mScaleY;
+ final float width = boundingBox.width / mScaleX;
+ final float height = boundingBox.height / mScaleY;
+
+ final Point topLeft = new Point(x, y);
+ final Point bottomRight = new Point(x + width, y + height);
+
+ return new RectF(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y);
+ }
+
+ @Nullable
+ private JiixWord getLongestWord(@NonNull List words)
+ {
+ JiixWord longestWord = null;
+
+ for (JiixWord word : words)
+ {
+ if (longestWord == null || longestWord.label.length() < word.label.length())
+ {
+ longestWord = word;
+ }
+ }
+
+ return longestWord;
+ }
+
+ private boolean notifyError(final boolean error, @NonNull final String message)
+ {
+ if (error && mOnRecognizedListener != null)
+ {
+ mOnRecognizedListener.onError(message);
+ }
+
+ return error;
+ }
+
+ private void clearRecognizers()
+ {
+ mPath.reset();
+ mStrokeCount = 0;
+ mLastGestureType = JiixGesture.GESTURE_TYPE_EMPTY;
+ mCurrentPointerId = -1;
+
+ try
+ {
+ mTextRecognizer.clear();
+ mGestureRecognizer.clear();
+
+ mTextRecognizer.waitForIdle();
+ mGestureRecognizer.waitForIdle();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/recognition/jiixmodel/JiixGesture.java b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/recognition/jiixmodel/JiixGesture.java
new file mode 100644
index 0000000..be000ed
--- /dev/null
+++ b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/recognition/jiixmodel/JiixGesture.java
@@ -0,0 +1,35 @@
+package com.myscript.iink.samples.writetotype.core.recognition.jiixmodel;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.List;
+
+public class JiixGesture
+{
+ public static final String GESTURE_TYPE_EMPTY = "";
+
+ // Gestures what are available by Gesture Recognizer
+ // (Gesture Recognizer means Recognizer with `RECOGNIZER_TYPE_GESTURE` parameter)
+ public static final String GESTURE_TYPE_NONE = "none";
+ public static final String GESTURE_TYPE_TOP_BOTTOM = "top-bottom";
+ public static final String GESTURE_TYPE_BOTTOM_TOP = "bottom-top";
+ public static final String GESTURE_TYPE_LEFT_RIGHT = "left-right";
+ public static final String GESTURE_TYPE_RIGHT_LEFT = "right-left";
+ public static final String GESTURE_TYPE_SCRATCH = "scratch";
+ public static final String GESTURE_TYPE_SURROUND = "surround";
+ public static final String GESTURE_TYPE_TAP = "tap";
+ public static final String GESTURE_TYPE_DOUBLE_TAP = "double-tap";
+ public static final String GESTURE_TYPE_LONG_PRESS = "long-press";
+
+ @SerializedName("type")
+ public String type;
+
+ @SerializedName("gestures")
+ public List gestures;
+
+ public static class Gesture
+ {
+ @SerializedName("type")
+ public String type;
+ }
+}
diff --git a/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/recognition/jiixmodel/JiixText.java b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/recognition/jiixmodel/JiixText.java
new file mode 100644
index 0000000..90cd7a0
--- /dev/null
+++ b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/recognition/jiixmodel/JiixText.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+package com.myscript.iink.samples.writetotype.core.recognition.jiixmodel;
+
+import com.google.gson.annotations.SerializedName;
+import com.myscript.iink.samples.writetotype.core.recognition.jiixmodel.item.JiixChar;
+import com.myscript.iink.samples.writetotype.core.recognition.jiixmodel.item.JiixWord;
+
+import java.util.List;
+
+public class JiixText
+{
+ @SerializedName("type")
+ public String type;
+
+ @SerializedName("label")
+ public String label;
+
+ @SerializedName("words")
+ public List words;
+
+ @SerializedName("chars")
+ public List chars;
+}
diff --git a/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/recognition/jiixmodel/item/JiixBoundingBox.java b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/recognition/jiixmodel/item/JiixBoundingBox.java
new file mode 100644
index 0000000..7f279f5
--- /dev/null
+++ b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/recognition/jiixmodel/item/JiixBoundingBox.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+package com.myscript.iink.samples.writetotype.core.recognition.jiixmodel.item;
+
+import com.google.gson.annotations.SerializedName;
+
+public class JiixBoundingBox
+{
+ @SerializedName("x")
+ public float x;
+
+ @SerializedName("y")
+ public float y;
+
+ @SerializedName("width")
+ public float width;
+
+ @SerializedName("height")
+ public float height;
+}
\ No newline at end of file
diff --git a/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/recognition/jiixmodel/item/JiixChar.java b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/recognition/jiixmodel/item/JiixChar.java
new file mode 100644
index 0000000..20630fa
--- /dev/null
+++ b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/recognition/jiixmodel/item/JiixChar.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+package com.myscript.iink.samples.writetotype.core.recognition.jiixmodel.item;
+
+import com.google.gson.annotations.SerializedName;
+
+public class JiixChar
+{
+ @SerializedName("label")
+ public String label;
+
+ @SerializedName("word")
+ public int word;
+
+ @SerializedName("bounding-box")
+ public JiixBoundingBox boundingBox;
+}
diff --git a/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/recognition/jiixmodel/item/JiixWord.java b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/recognition/jiixmodel/item/JiixWord.java
new file mode 100644
index 0000000..b4b6eaa
--- /dev/null
+++ b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/recognition/jiixmodel/item/JiixWord.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+package com.myscript.iink.samples.writetotype.core.recognition.jiixmodel.item;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.List;
+
+public class JiixWord
+{
+ @SerializedName("label")
+ public String label;
+
+ @SerializedName("candidates")
+ public List candidates;
+
+ @SerializedName("first-char")
+ public int firstChar;
+
+ @SerializedName("last-char")
+ public int lastChar;
+
+ @SerializedName("bounding-box")
+ public JiixBoundingBox boundingBox;
+}
diff --git a/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/recognition/strokemodel/CustomPath.java b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/recognition/strokemodel/CustomPath.java
new file mode 100644
index 0000000..53a0307
--- /dev/null
+++ b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/core/recognition/strokemodel/CustomPath.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+package com.myscript.iink.samples.writetotype.core.recognition.strokemodel;
+
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.graphics.RectF;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.annotation.NonNull;
+
+public class CustomPath extends Path
+{
+ final private List mPoints = new ArrayList<>();
+
+ @Override
+ public void moveTo(float x, float y)
+ {
+ super.moveTo(x, y);
+ mPoints.add(new PointF(x, y));
+ }
+
+ @Override
+ public void lineTo(float x, float y)
+ {
+ super.lineTo(x, y);
+ mPoints.add(new PointF(x, y));
+ }
+
+ @Override
+ public void reset()
+ {
+ super.reset();
+ mPoints.clear();
+ }
+
+ public RectF getBoundingRect()
+ {
+ RectF rect = new RectF();
+ this.computeBounds(rect, true);
+
+ return rect;
+ }
+
+ @NonNull
+ public List getPoints()
+ {
+ return new ArrayList<>(this.mPoints);
+ }
+}
diff --git a/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/im/InputMethodEmulator.java b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/im/InputMethodEmulator.java
new file mode 100644
index 0000000..bdc8421
--- /dev/null
+++ b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/im/InputMethodEmulator.java
@@ -0,0 +1,835 @@
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+package com.myscript.iink.samples.writetotype.im;
+
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.widget.EditText;
+
+import com.myscript.iink.samples.writetotype.CustomViewGroup;
+import com.myscript.iink.samples.writetotype.DebugView;
+import com.myscript.iink.samples.writetotype.WriteToTypeManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class InputMethodEmulator implements WriteToTypeManager.OnWriteToTypeListener, CustomViewGroup.OnChangedListener
+{
+ private static final float OVERLAP_RATIO = 0.4f;
+ private static final float BIGGER_HEIGHT_FACTOR = 3.0f;
+ private static final float UNDERLINE_DISTANCE_IN_MILLIMETER = 5;
+ private static final int SCRATCH_COLOR = 0xffcccccc; // it's light gray.
+
+ private enum LineType {
+ LINE_TYPE_VERTICAL,
+ LINE_TYPE_HORIZONTAL
+ }
+
+ private final WriteToTypeManager mWriteToTypeManager;
+ private final CustomViewGroup mViewGroup;
+ private DebugView mDebugView = null;
+
+ private EditText mEditText = null;
+ private int mSelectionStart = -1;
+ private int mSelectionEnd = -1;
+ private int mDefaultHighlightColor;
+
+ private final float mExtraDistanceInDpi;
+
+ private final Vibrator mVibrator;
+ private boolean mIsVibrating = false;
+
+ private boolean mDebug;
+
+ // --------------------------------------------------------------------------
+ // Constructor
+
+ /** Constructor. */
+ public InputMethodEmulator(@NonNull final WriteToTypeManager writeToTypeManager, @NonNull final CustomViewGroup viewGroup, final float scaleY, @NonNull final Vibrator vibrator)
+ {
+ mWriteToTypeManager = writeToTypeManager;
+ mWriteToTypeManager.setOnWriteToTypeListener(this);
+
+ mViewGroup = viewGroup;
+ mViewGroup.initIndexForEditText();
+ mViewGroup.setOnChangedListener(this);
+
+ mExtraDistanceInDpi = UNDERLINE_DISTANCE_IN_MILLIMETER / scaleY;
+ mVibrator = vibrator;
+
+ mDebug = false;
+ }
+
+ public void setDebugView(DebugView debugView)
+ {
+ mDebugView = debugView;
+ }
+
+ public void setDebug(final boolean debug)
+ {
+ mDebug = debug;
+ mDebugView.setDebug(mDebug);
+ }
+
+ public boolean isDebug()
+ {
+ return mDebug;
+ }
+
+ public void setDefaultEditText(final int index)
+ {
+ EditText editText = mViewGroup.setFocus(index);
+ if (editText != null)
+ {
+ mEditText = editText;
+ mDefaultHighlightColor = mEditText.getHighlightColor();
+ debugMethod(-1, -1, null, editText, null);
+ }
+ }
+
+ public void resetTextForEditText()
+ {
+ mViewGroup.resetTextForEditText();
+ }
+
+ // --------------------------------------------------------------------------
+ // Implementation of WriteToTypeWidget.OnWriteToTypeListener
+
+ /**
+ * onText is invoked when "NONE" gesture is recognized by Gesture Recognizer.
+ * IMPLEMENTATION CHOICE:
+ * Gesture result "NONE" means no gesture detected.
+ * Currently, a behavior of onText is the exactly same as onScratch
,
+ * but the action is postponed to pointer-up as "NONE" most probably mean Text input.
+ *
+ * @param recognitionResult A structure of recognition result, see {@link WriteToTypeManager.RecognitionResult}
+ * @param isCommitted true
when a commit has been taken into account to current recognition result.
+ * @return true
if Stroke bounding box is overlapped on existing text of EditText, false
otherwise.
+ */
+ @Override
+ public boolean onText(@NonNull final WriteToTypeManager.RecognitionResult recognitionResult, final boolean isCommitted)
+ {
+ if (recognitionResult.isPointerUp)
+ {
+ return onScratch(recognitionResult, isCommitted);
+ }
+
+ return false;
+ }
+
+ /**
+ * onTopBottom is invoked when "TOP-BOTTOM" gesture is recognized by Gesture Recognizer.
+ * IMPLEMENTATION CHOICE:
+ * Gesture result "TOP-BOTTOM" means a vertical line to bottom direction is detected.
+ * - if "top to bottom" stroke pass through existing text of EditText, it adds a space in stroke location
+ * (multiple spaces are not available),
+ * - otherwise, it adds recognized text in current cursor location.
+ *
+ * @param recognitionResult A structure of recognition result, see {@link WriteToTypeManager.RecognitionResult}
+ * @param isCommitted true
when a commit has been taken into account to current recognition result.
+ * @return true
if Stroke bounding box is overlapped on existing text of EditText, false
otherwise.
+ */
+ public boolean onTopBottom(@NonNull final WriteToTypeManager.RecognitionResult recognitionResult, final boolean isCommitted)
+ {
+ boolean overlapped = false;
+
+ if (recognitionResult.isPointerUp)
+ {
+ final RectF strokeRect = recognitionResult.strokeRect;
+ checkFocusChangedWithExtraDistance(strokeRect, LineType.LINE_TYPE_VERTICAL);
+
+ if (mEditText != null)
+ {
+ final float x = strokeRect.centerX();
+ final float y = strokeRect.centerY();
+
+ RectF intersect = new RectF(strokeRect);
+ RectF textBounds = getTextBounds(x, y, mEditText);
+
+ if (textBounds != null && mEditText.getText().length() != 0 &&
+ ((intersect.intersect(textBounds) && isPassedThrough(strokeRect, textBounds)) || (isInDistanceOf(strokeRect, textBounds, LineType.LINE_TYPE_VERTICAL) && isCommitted)))
+ {
+ mWriteToTypeManager.cancelRecognition();
+ final PointF center = getCenterOfLine(recognitionResult.points, textBounds);
+ if (center.x != -1 && center.y != -1)
+ {
+ mViewGroup.setSpace(mEditText, center.x, center.y);
+ }
+ else
+ {
+ final float centerX = (strokeRect.left < textBounds.left) ? textBounds.left : textBounds.right;
+ final float centerY = textBounds.centerY();
+ mViewGroup.setSpace(mEditText, centerX, centerY);
+ }
+
+ overlapped = true;
+ }
+ else
+ {
+ setText(recognitionResult, isCommitted);
+ }
+
+ debugMethod(-1, -1, textBounds, mEditText, strokeRect);
+ }
+ }
+
+ return overlapped;
+ }
+
+ /**
+ * onBottomTop is invoked when "BOTTOM-TOP" gesture is recognized by Gesture Recognizer.
+ * IMPLEMENTATION CHOICE:
+ * Gesture result "BOTTOM-TOP" means a vertical line to top direction is detected.
+ * - if "bottom to top" stroke pass through a space over text of EditText, it deletes the space of the stroke location,
+ * - otherwise, nothing happens.
+ *
+ * @param recognitionResult A structure of recognition result, see {@link WriteToTypeManager.RecognitionResult}
+ * @param isCommitted true
when a commit has been taken into account to current recognition result.
+ * @return true
if Stroke bounding box is overlapped on existing text of EditText, false
otherwise.
+ */
+ @Override
+ public boolean onBottomTop(@NonNull final WriteToTypeManager.RecognitionResult recognitionResult, final boolean isCommitted)
+ {
+ boolean overlapped = false;
+
+ if (recognitionResult.isPointerUp)
+ {
+ final RectF strokeRect = recognitionResult.strokeRect;
+ checkFocusChangedWithExtraDistance(strokeRect, LineType.LINE_TYPE_VERTICAL);
+
+ if (mEditText != null)
+ {
+ final float x = strokeRect.centerX();
+ final float y = strokeRect.centerY();
+
+ RectF intersect = new RectF(strokeRect);
+ RectF textBounds = getTextBounds(x, y, mEditText);
+
+ if (textBounds != null && ((intersect.intersect(textBounds) && isPassedThrough(strokeRect, textBounds)) || isInDistanceOf(strokeRect, textBounds, LineType.LINE_TYPE_VERTICAL)))
+ {
+ mWriteToTypeManager.cancelRecognition();
+ final PointF center = getCenterOfLine(recognitionResult.points, textBounds);
+ if (center.x != -1 && center.y != -1)
+ {
+ mViewGroup.eraseSpace(mEditText, center.x, center.y);
+ }
+ else
+ {
+ final float centerX = (strokeRect.left < textBounds.left) ? textBounds.left : textBounds.right;
+ final float centerY = textBounds.centerY();
+ mViewGroup.eraseSpace(mEditText, centerX, centerY);
+ }
+
+ overlapped = true;
+ }
+
+ debugMethod(-1, -1, textBounds, mEditText, strokeRect);
+ }
+ }
+
+ return overlapped;
+ }
+
+ /**
+ * onLeftRight is invoked when "LEFT-RIGHT" gesture is recognized by Gesture Recognizer.
+ * IMPLEMENTATION CHOICE:
+ * Gesture result "LEFT-RIGHT" means a horizontal line to right direction is detected.
+ * - if "left to right" stroke over an existing text of EditText, it selects characters of stroke area,
+ * - otherwise, it moves one character forward cursor from current cursor location.
+ *
+ * @param recognitionResult A structure of recognition result, see {@link WriteToTypeManager.RecognitionResult}
+ * @param isCommitted true
when a commit has been taken into account to current recognition result.
+ * @return true
if Stroke bounding box is overlapped on existing text of EditText, false
otherwise.
+ */
+ @Override
+ public boolean onLeftRight(@NonNull final WriteToTypeManager.RecognitionResult recognitionResult, final boolean isCommitted)
+ {
+ final RectF strokeRect = recognitionResult.strokeRect;
+ checkFocusChangedWithExtraDistance(strokeRect, LineType.LINE_TYPE_HORIZONTAL);
+
+ boolean overlapped = false;
+
+ if (mEditText != null)
+ {
+ final float x = strokeRect.centerX();
+ final float y = strokeRect.centerY();
+
+ RectF intersect = new RectF(strokeRect);
+ RectF textBounds = getTextBounds(x, y, mEditText);
+
+ if (textBounds != null && ((intersect.intersect(textBounds) && isOverlappedEnough(strokeRect, intersect)) || isInDistanceOf(strokeRect, textBounds, LineType.LINE_TYPE_HORIZONTAL)))
+ {
+ if (recognitionResult.isPointerUp)
+ {
+ mWriteToTypeManager.cancelRecognition();
+ }
+ mViewGroup.setSelection(mEditText, intersect, mDefaultHighlightColor);
+
+ overlapped = true;
+ }
+ else
+ {
+ if (recognitionResult.isRecognizerIdle)
+ {
+ mWriteToTypeManager.cancelRecognition();
+ mViewGroup.forwardCursor(mEditText);
+ }
+ }
+
+ debugMethod(-1, -1, textBounds, mEditText, strokeRect);
+ }
+
+ return overlapped;
+ }
+
+ /**
+ * onRightLeft is invoked when "RIGHT-LEFT" gesture is recognized by Gesture Recognizer.
+ * IMPLEMENTATION CHOICE:
+ * Gesture result "RIGHT-LEFT" means a horizontal line to left direction is detected.
+ * - if "right to left" stroke over an existing text of EditText, it selects characters of stroke area,
+ * - otherwise, it deletes one character backward from current cursor location.
+ *
+ * @param recognitionResult A structure of recognition result, see {@link WriteToTypeManager.RecognitionResult}
+ * @param isCommitted true
when a commit has been taken into account to current recognition result.
+ * @return true
if Stroke bounding box is overlapped on existing text of EditText, false
otherwise.
+ */
+ @Override
+ public boolean onRightLeft(@NonNull final WriteToTypeManager.RecognitionResult recognitionResult, final boolean isCommitted)
+ {
+ final RectF strokeRect = recognitionResult.strokeRect;
+ checkFocusChanged(strokeRect);
+
+ boolean overlapped = false;
+
+ if (mEditText != null)
+ {
+ final float x = strokeRect.centerX();
+ final float y = strokeRect.centerY();
+
+ RectF intersect = new RectF(strokeRect);
+ RectF textBounds = getTextBounds(x, y, mEditText);
+
+ if (textBounds != null && intersect.intersect(textBounds) && isOverlappedEnough(strokeRect, intersect))
+ {
+ if (recognitionResult.isPointerUp)
+ {
+ mWriteToTypeManager.cancelRecognition();
+ }
+ mViewGroup.setSelection(mEditText, intersect, mDefaultHighlightColor);
+
+ overlapped = true;
+ }
+ else
+ {
+ if (recognitionResult.isRecognizerIdle)
+ {
+ mWriteToTypeManager.cancelRecognition();
+ mViewGroup.backwardDelete(mEditText);
+ }
+ }
+
+ debugMethod(-1, -1, textBounds, mEditText, strokeRect);
+ }
+
+ return overlapped;
+ }
+
+ /**
+ * onScratch is invoked when "SCRATCH" gesture is recognized by Gesture Recognizer.
+ * IMPLEMENTATION CHOICE:
+ * - if "scratch" are drawn over an existing text of EditText, it deletes characters of scratched area,
+ * - otherwise, it adds recognized text in current cursor location.
+ * Note that, real text writing could be recognized as "SCRATCH" by Gesture Recognizer.
+ *
+ * @param recognitionResult A structure of recognition result, see {@link WriteToTypeManager.RecognitionResult}
+ * @param isCommitted true
when a commit has been taken into account to current recognition result.
+ * @return true
if Stroke bounding box is overlapped on existing text of EditText, false
otherwise.
+ */
+ @Override
+ public boolean onScratch(@NonNull WriteToTypeManager.RecognitionResult recognitionResult, boolean isCommitted)
+ {
+ final RectF strokeRect = recognitionResult.strokeRect;
+ checkFocusChanged(strokeRect);
+
+ boolean overlapped = false;
+
+ if (mEditText != null)
+ {
+ final float x = strokeRect.centerX();
+ final float y = strokeRect.centerY();
+
+ RectF intersect = new RectF(strokeRect);
+ RectF textBounds = getTextBounds(x, y, mEditText);
+
+ if (textBounds != null && intersect.intersect(textBounds) && isOverlappedEnough(strokeRect, intersect))
+ {
+ if (recognitionResult.isPointerUp)
+ {
+ mViewGroup.backwardDelete(mEditText);
+ mWriteToTypeManager.cancelRecognition();
+ }
+ else
+ {
+ mViewGroup.setSelection(mEditText, strokeRect, SCRATCH_COLOR);
+ }
+
+ overlapped = true;
+ }
+ else
+ {
+ setText(recognitionResult, isCommitted);
+ }
+
+ debugMethod(-1, -1, textBounds, mEditText, strokeRect);
+ }
+
+ return overlapped;
+ }
+
+ /**
+ * onSurround is invoked when "SURROUND" gesture is recognized by Gesture Recognizer.
+ * IMPLEMENTATION CHOICE:
+ * Gesture result "SURROUND" means a circle, an oval or a rectangle is detected.
+ * - if "surround" are drawn over an existing text of EditText, it selects surrounded area,
+ * - otherwise, it adds recognized text in current cursor location.
+ *
+ * @param recognitionResult A structure of recognition result, see {@link WriteToTypeManager.RecognitionResult}
+ * @param isCommitted true
when a commit has been taken into account to current recognition result.
+ * @return true
if Stroke bounding box is overlapped on existing text of EditText, false
otherwise.
+ */
+ @Override
+ public boolean onSurround(@NonNull final WriteToTypeManager.RecognitionResult recognitionResult, final boolean isCommitted)
+ {
+ final RectF strokeRect = recognitionResult.strokeRect;
+ checkFocusChanged(strokeRect);
+
+ boolean overlapped = false;
+
+ if (mEditText != null)
+ {
+ final float x = strokeRect.centerX();
+ final float y = strokeRect.centerY();
+
+ RectF intersect = new RectF(strokeRect);
+ RectF textBounds = getTextBounds(x, y, mEditText);
+
+ if (textBounds != null && intersect.intersect(textBounds) && textBounds.contains(x, y))
+ {
+ if (recognitionResult.isPointerUp)
+ {
+ mWriteToTypeManager.cancelRecognition();
+ }
+ mViewGroup.setSelection(mEditText, strokeRect, mDefaultHighlightColor);
+
+ overlapped = true;
+ }
+ else
+ {
+ setText(recognitionResult, isCommitted);
+ }
+
+ debugMethod(-1, -1, textBounds, mEditText, strokeRect);
+ }
+
+ return overlapped;
+ }
+
+ /**
+ * onSingleTap is invoked when "TAP" gesture is recognized by Gesture Recognizer.
+ * IMPLEMENTATION CHOICE:
+ * - if "single tap" over an existing text of EditText, it changes a cursor position to the tapped location
+ * (nothing happens if single tap on a selection area),
+ * - otherwise:
+ * - if "single tap" over empty area of other EditText than current focus, it changes a focus of EditText,
+ * - if "single tap" over empty area of current focus EditText, it adds recognized text in current cursor location.
+ *
+ * @param recognitionResult A structure of recognition result, see {@link WriteToTypeManager.RecognitionResult}
+ * @param isCommitted true
when a commit has been taken into account to current recognition result.
+ * @return true
if Stroke bounding box is overlapped on existing text of EditText, false
otherwise.
+ */
+ @Override
+ public boolean onSingleTap(@NonNull final WriteToTypeManager.RecognitionResult recognitionResult, final boolean isCommitted)
+ {
+ boolean overlapped = false;
+
+ if (recognitionResult.isRecognizerIdle)
+ {
+ final RectF strokeRect = recognitionResult.strokeRect;
+ final float x = strokeRect.centerX();
+ final float y = strokeRect.centerY();
+
+ EditText editText = mViewGroup.findViewByPosition(x, y);
+ if (editText != null)
+ {
+ RectF textBounds = getTextBounds(x, y, editText);
+
+ if (editText != mEditText)
+ {
+ mWriteToTypeManager.cancelRecognition();
+
+ mEditText = editText;
+
+ mViewGroup.setFocus(editText);
+ mViewGroup.setSelection(editText, x, y, false, mDefaultHighlightColor);
+
+ overlapped = true;
+
+ debugMethod(x, y, textBounds, mEditText, strokeRect);
+ }
+ else
+ {
+ if (textBounds != null && textBounds.contains(x, y))
+ {
+ int cursor = mViewGroup.findCursorByPosition(editText, x, y);
+ if ((mSelectionStart != mSelectionEnd) || (mSelectionStart != cursor))
+ {
+ mWriteToTypeManager.cancelRecognition();
+
+ mViewGroup.setSelection(editText, x, y, false, mDefaultHighlightColor);
+ }
+
+ overlapped = true;
+
+ debugMethod(x, y, textBounds, mEditText, strokeRect);
+ }
+ else
+ {
+ setText(recognitionResult, isCommitted);
+ debugMethod(-1, -1, textBounds, mEditText, strokeRect);
+ }
+ }
+ }
+ else
+ {
+ setText(recognitionResult, isCommitted);
+ debugMethod(-1, -1, null, mEditText, strokeRect);
+ }
+ }
+
+ return overlapped;
+ }
+
+ /**
+ * onDoubleTap is invoked when "DOUBLE-TAP" gesture is recognized by Gesture Recognizer.
+ * IMPLEMENTATION CHOICE:
+ * - if "double tap" over an existing text of EditText, it selects a word of the double tapped location,
+ * - otherwise, nothing happens.
+ *
+ * @param recognitionResult A structure of recognition result, see {@link WriteToTypeManager.RecognitionResult}
+ * @param isCommitted true
when a commit has been taken into account to current recognition result.
+ * @return true
if Stroke bounding box is overlapped on existing text of EditText, false
otherwise.
+ */
+ @Override
+ public boolean onDoubleTap(@NonNull final WriteToTypeManager.RecognitionResult recognitionResult, final boolean isCommitted)
+ {
+ final RectF strokeRect = recognitionResult.strokeRect;
+ checkFocusChanged(strokeRect);
+
+ boolean overlapped = false;
+
+ if (mEditText != null)
+ {
+ final float x = strokeRect.centerX();
+ final float y = strokeRect.centerY();
+
+ RectF intersect = new RectF(strokeRect);
+ RectF textBounds = getTextBounds(x, y, mEditText);
+
+ if (textBounds != null && intersect.intersect(textBounds) && textBounds.contains(x, y))
+ {
+ mViewGroup.setSelection(mEditText, x, y, true, mDefaultHighlightColor);
+ overlapped = true;
+ }
+
+ debugMethod(x, y, textBounds, mEditText, null);
+ }
+
+ mWriteToTypeManager.cancelRecognition();
+
+ return overlapped;
+ }
+
+ /**
+ * onLongPress is invoked when "LONG-PRESS" gesture is recognized by Gesture Recognizer.
+ * IMPLEMENTATION CHOICE:
+ * - if "long press" over an existing text of EditText, it selects a word of the long pressed location,
+ * - otherwise, nothing happens.
+ *
+ * @param recognitionResult A structure of recognition result, see {@link WriteToTypeManager.RecognitionResult}
+ * @param isCommitted true
when a commit has been taken into account to current recognition result.
+ * @return true
if Stroke bounding box is overlapped on existing text of EditText, false
otherwise.
+ */
+ @Override
+ public boolean onLongPress(@NonNull final WriteToTypeManager.RecognitionResult recognitionResult, final boolean isCommitted)
+ {
+ final RectF strokeRect = recognitionResult.strokeRect;
+ checkFocusChanged(strokeRect);
+
+ boolean overlapped = false;
+
+ if (mEditText != null)
+ {
+ final float x = strokeRect.centerX();
+ final float y = strokeRect.centerY();
+
+ RectF intersect = new RectF(strokeRect);
+ RectF textBounds = getTextBounds(x, y, mEditText);
+
+ if (recognitionResult.isPointerUp)
+ {
+ mWriteToTypeManager.cancelRecognition();
+ mIsVibrating = false;
+
+ if (textBounds != null && intersect.intersect(textBounds) && textBounds.contains(x, y))
+ {
+ overlapped = true;
+ }
+ }
+ else
+ {
+ if (textBounds != null && intersect.intersect(textBounds) && textBounds.contains(x, y))
+ {
+ if (!mIsVibrating)
+ {
+ mIsVibrating = true;
+ vibrate();
+ }
+ mViewGroup.setSelection(mEditText, x, y, true, mDefaultHighlightColor);
+ overlapped = true;
+ }
+ }
+
+ debugMethod(x, y, textBounds, mEditText, null);
+ }
+
+ return overlapped;
+ }
+
+ // --------------------------------------------------------------------------
+ // Implementation of CustomViewGroup.OnChangedListener
+
+ @Override
+ public void onFocusChanged(EditText editText)
+ {
+ if (editText != mEditText)
+ {
+ mEditText = editText;
+ mSelectionStart = -1;
+ mSelectionEnd = -1;
+
+ mWriteToTypeManager.clearSession();
+ }
+ }
+
+ @Override
+ public void onSelectionChanged(EditText editText, int selectionStart, int selectionEnd)
+ {
+ mEditText = editText;
+ mSelectionStart = selectionStart;
+ mSelectionEnd = selectionEnd;
+ }
+
+ // --------------------------------------------------------------------------
+ // Internal methods of InputMethodEmulator class
+
+ private void setText(@NonNull WriteToTypeManager.RecognitionResult recognitionResult, boolean isCommitted)
+ {
+ if (isCommitted && !recognitionResult.textResult.label.isEmpty())
+ {
+ mViewGroup.setText(mEditText, recognitionResult.textResult.label);
+ }
+ }
+
+ private void debugMethod(float x, float y, RectF textBounds, @NonNull EditText editText, RectF recoBounds)
+ {
+ mDebugView.setTouchPoint(x, y);
+
+ mDebugView.setTextBounds(textBounds);
+ mDebugView.setViewBounds(editText.getX(), editText.getY(), editText.getX() + editText.getWidth(), editText.getY() + editText.getHeight());
+
+ mDebugView.setRecoBounds(recoBounds);
+ }
+
+ /** Compute text bounding box for the first text line inside of EditText. */
+ @Nullable
+ private RectF getTextBounds(final float x, final float y, @NonNull final EditText editText)
+ {
+ RectF textBounds = null;
+
+ final int count = editText.getLineCount();
+ for (int i = 0; i < count; i++)
+ {
+ Rect bounds = new Rect();
+ // Get the absolute positions of text bounding box.
+ // Note that it will retrieve a full space of given line what you can input text.
+ editText.getLineBounds(i, bounds);
+
+ // Compute the relative positions inside of EditText.
+ bounds.offset((int) editText.getX(), (int) editText.getY());
+
+ if (bounds.contains((int) x, (int) y) || (i == (count - 1)))
+ {
+ textBounds = new RectF(bounds);
+
+ // Layout.getLineRight(int line) gets the rightmost position
+ // that should be exposed for horizontal scrolling on the specified line.
+ // It is used for computing actual text end position.
+ textBounds.right = textBounds.left + editText.getLayout().getLineRight(i) + (textBounds.height() / 4);
+
+ break;
+ }
+ }
+
+ return textBounds;
+ }
+
+ /** Check if recognition area is really overlapped with existing text or it was just happen by chance. */
+ private boolean isOverlappedEnough(@NonNull final RectF original, @NonNull final RectF intersect)
+ {
+ final float originalArea = original.width() * original.height();
+ final float intersectArea = intersect.width() * intersect.height();
+
+ return ((originalArea * OVERLAP_RATIO) < intersectArea);
+ }
+
+ /** Check if the given line is in the extra distance or not. */
+ private boolean isInDistanceOf(@NonNull final RectF lineRect, @NonNull final RectF textRect, final LineType lineType)
+ {
+ if (lineType == LineType.LINE_TYPE_HORIZONTAL)
+ {
+ final float lineCenter = lineRect.left + (lineRect.width() / 2);
+
+ return (((textRect.left <= lineCenter) && (lineCenter <= textRect.right)) &&
+ ((lineRect.top > textRect.top) && (lineRect.top < (textRect.bottom + mExtraDistanceInDpi))));
+ }
+ else
+ {
+ return (((lineRect.top < textRect.top) && (textRect.bottom < lineRect.bottom)) &&
+ ((lineRect.left > textRect.left) && (lineRect.left < (textRect.right + mExtraDistanceInDpi))));
+ }
+ }
+
+ /** Check if the given line is passed through up/down the text or not. */
+ private boolean isPassedThrough(@NonNull final RectF lineRect, @NonNull final RectF textRect)
+ {
+ return (((textRect.left <= lineRect.left) && (lineRect.right <= textRect.right)) &&
+ ((lineRect.top < textRect.top) && (textRect.bottom < lineRect.bottom)));
+ }
+
+ /** Get center point of the line in the text bounds. */
+ @NonNull
+ private PointF getCenterOfLine(@NonNull final List points, @NonNull final RectF textBounds)
+ {
+ assert (points.size() > 0) : "The size of points must be bigger than 0.";
+
+ List intersectPoints = new ArrayList<>();
+ for (PointF point : points)
+ {
+ if (textBounds.contains(point.x, point.y))
+ {
+ intersectPoints.add(point);
+ }
+ }
+
+ float centerX = -1;
+ float centerY = -1;
+
+ if (intersectPoints.size() > 2)
+ {
+ final int lastIndex = intersectPoints.size() - 1;
+ centerX = (intersectPoints.get(0).x + intersectPoints.get(lastIndex).x) / 2;
+ centerY = (intersectPoints.get(0).y + intersectPoints.get(lastIndex).y) / 2;
+ }
+
+ return new PointF(centerX, centerY);
+ }
+
+ private void changeFocusTo(EditText editText, final float centerX, final float centerY)
+ {
+ if (editText == null)
+ {
+ editText = mViewGroup.findViewByPosition(centerX, centerY);
+ }
+
+ if (editText != null && editText != mEditText)
+ {
+ mEditText = editText;
+
+ mViewGroup.setFocus(editText);
+ mViewGroup.setSelection(editText, centerX, centerY, false, mDefaultHighlightColor);
+ }
+ }
+
+ /**
+ * Check whether the written area (boundingRect) is on the other EditText field or not.
+ * If yes, the focus is changed to that field.
+ * */
+ private void checkFocusChanged(@NonNull final RectF boundingRect)
+ {
+ if (mEditText != null && mEditText.getHeight() * BIGGER_HEIGHT_FACTOR < boundingRect.height())
+ {
+ return;
+ }
+
+ changeFocusTo(null, boundingRect.centerX(), boundingRect.centerY());
+ }
+
+ /**
+ * Check whether the written area (boundingRect) with extra distance is belong to other EditText field or not.
+ * If yes, the focus is changed to that field.
+ */
+ private void checkFocusChangedWithExtraDistance(@NonNull final RectF boundingRect, final LineType lineType)
+ {
+ if (mEditText != null && mEditText.getHeight() * BIGGER_HEIGHT_FACTOR < boundingRect.height())
+ {
+ return;
+ }
+
+ float centerX = boundingRect.centerX();
+ float centerY = boundingRect.centerY();
+
+ EditText editText = mViewGroup.findViewByPosition(centerX, centerY);
+ if (editText != null)
+ {
+ changeFocusTo(editText, centerX, centerY);
+ }
+ else
+ {
+ if (lineType == LineType.LINE_TYPE_HORIZONTAL)
+ {
+ centerY = boundingRect.top - mExtraDistanceInDpi;
+ changeFocusTo(null, centerX, centerY);
+ }
+ else if (lineType == LineType.LINE_TYPE_VERTICAL)
+ {
+ centerX = boundingRect.left - mExtraDistanceInDpi;
+ editText = mViewGroup.findViewByPosition(centerX, centerY);
+ if (editText != null && mViewGroup.isMultiLine(editText))
+ {
+ changeFocusTo(editText, centerX, centerY);
+ }
+ }
+ }
+ }
+
+ private void vibrate()
+ {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
+ {
+ mVibrator.vibrate(VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE));
+ }
+ else
+ {
+ mVibrator.vibrate(50);
+ }
+ }
+}
diff --git a/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/utils/ErrorActivity.kt b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/utils/ErrorActivity.kt
new file mode 100644
index 0000000..b7e8a76
--- /dev/null
+++ b/samples/write-to-type/src/main/java/com/myscript/iink/samples/writetotype/utils/ErrorActivity.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) MyScript. All rights reserved.
+ */
+
+package com.myscript.iink.samples.writetotype.utils
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.Process
+import android.text.method.ScrollingMovementMethod
+import android.widget.Button
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import com.myscript.iink.samples.writetotype.R
+import java.io.PrintWriter
+import java.io.StringWriter
+import kotlin.system.exitProcess
+
+/**
+ * This activity displays an error message when an uncaught exception is thrown within an activity
+ * that installed the associated exception handler. Since this application targets developers it's
+ * better to clearly explain what happened. The code is inspired by:
+ * https://trivedihardik.wordpress.com/2011/08/20/how-to-avoid-force-close-error-in-android/
+ */
+
+/* important do not forget to add the activity in you manifest
+
+ */
+
+class ErrorActivity : AppCompatActivity() {
+ companion object {
+ private val TAG = ErrorActivity::class.java.toString()
+ private val ERR_TITLE = "err_title@$TAG"
+ private val ERR_MESSAGE = "err_message@$TAG"
+
+ @JvmStatic
+ fun setExceptionHandler(context: Context) {
+ Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(context))
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_error)
+ val tvErrorTitle = findViewById(R.id.tv_error_title)
+ tvErrorTitle.text = intent.getStringExtra(ERR_TITLE)
+
+ val tvErrorMessage = findViewById(R.id.tv_error_message)
+ tvErrorMessage.text = intent.getStringExtra(ERR_MESSAGE)
+ tvErrorMessage.movementMethod = ScrollingMovementMethod()
+
+ findViewById(R.id.exit_button).setOnClickListener {
+ finishAffinity()
+ finishAndRemoveTask()
+ moveTaskToBack(true)
+ exitProcess(-1)
+ }
+ }
+
+ private class ExceptionHandler(private val context: Context) : Thread.UncaughtExceptionHandler {
+ override fun uncaughtException(t: Thread, e: Throwable) {
+ // get message from the root cause.
+ var root: Throwable? = e
+ while (root!!.cause != null) {
+ root = root.cause
+ }
+ val message = root.message
+
+ // print stack trace.
+ val writer = StringWriter()
+ e.printStackTrace(PrintWriter(writer))
+ val trace = writer.toString()
+
+ // launch the error activity.
+ val intent = Intent(context, ErrorActivity::class.java)
+ intent.putExtra(ERR_TITLE, message)
+ intent.putExtra(ERR_MESSAGE, trace)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK;
+ context.startActivity(intent)
+ // kill the current activity.
+ Process.killProcess(Process.myPid())
+ exitProcess(10)
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/write-to-type/src/main/res/drawable-v24/ic_launcher_foreground.xml b/samples/write-to-type/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..aa654c2
--- /dev/null
+++ b/samples/write-to-type/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/write-to-type/src/main/res/drawable/border.xml b/samples/write-to-type/src/main/res/drawable/border.xml
new file mode 100644
index 0000000..9ff82ab
--- /dev/null
+++ b/samples/write-to-type/src/main/res/drawable/border.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/samples/write-to-type/src/main/res/drawable/edittext_rectangle.xml b/samples/write-to-type/src/main/res/drawable/edittext_rectangle.xml
new file mode 100644
index 0000000..1534652
--- /dev/null
+++ b/samples/write-to-type/src/main/res/drawable/edittext_rectangle.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/write-to-type/src/main/res/drawable/ic_launcher_background.xml b/samples/write-to-type/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..85efc6f
--- /dev/null
+++ b/samples/write-to-type/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/samples/write-to-type/src/main/res/layout/activity_error.xml b/samples/write-to-type/src/main/res/layout/activity_error.xml
new file mode 100644
index 0000000..37bdb21
--- /dev/null
+++ b/samples/write-to-type/src/main/res/layout/activity_error.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/write-to-type/src/main/res/layout/activity_main.xml b/samples/write-to-type/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..1746a1f
--- /dev/null
+++ b/samples/write-to-type/src/main/res/layout/activity_main.xml
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/write-to-type/src/main/res/menu/activity_main.xml b/samples/write-to-type/src/main/res/menu/activity_main.xml
new file mode 100644
index 0000000..2955b06
--- /dev/null
+++ b/samples/write-to-type/src/main/res/menu/activity_main.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/write-to-type/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/samples/write-to-type/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..96525d0
--- /dev/null
+++ b/samples/write-to-type/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/samples/write-to-type/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/samples/write-to-type/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..96525d0
--- /dev/null
+++ b/samples/write-to-type/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/samples/write-to-type/src/main/res/mipmap-hdpi/ic_launcher.png b/samples/write-to-type/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a98a138
Binary files /dev/null and b/samples/write-to-type/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/samples/write-to-type/src/main/res/mipmap-mdpi/ic_launcher.png b/samples/write-to-type/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..34ca9b3
Binary files /dev/null and b/samples/write-to-type/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/samples/write-to-type/src/main/res/mipmap-xhdpi/ic_launcher.png b/samples/write-to-type/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..4b3eb36
Binary files /dev/null and b/samples/write-to-type/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/samples/write-to-type/src/main/res/mipmap-xxhdpi/ic_launcher.png b/samples/write-to-type/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..bbea1d0
Binary files /dev/null and b/samples/write-to-type/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/samples/write-to-type/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/samples/write-to-type/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..36639e3
Binary files /dev/null and b/samples/write-to-type/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/java/common-kotlin/src/main/res/values/colors.xml b/samples/write-to-type/src/main/res/values/colors.xml
similarity index 95%
rename from java/common-kotlin/src/main/res/values/colors.xml
rename to samples/write-to-type/src/main/res/values/colors.xml
index 7e51850..aca041f 100644
--- a/java/common-kotlin/src/main/res/values/colors.xml
+++ b/samples/write-to-type/src/main/res/values/colors.xml
@@ -8,4 +8,4 @@
#448aff
#3f51b5
@android:color/white
-
+
\ No newline at end of file
diff --git a/samples/write-to-type/src/main/res/values/strings.xml b/samples/write-to-type/src/main/res/values/strings.xml
new file mode 100644
index 0000000..3b8c964
--- /dev/null
+++ b/samples/write-to-type/src/main/res/values/strings.xml
@@ -0,0 +1,25 @@
+
+
+
+
+ Write To Type sample
+ [LOG VIEW]
+
+ Reset
+ Enable Debug
+ Disable Debug
+ Enable superimposed
+ Disable superimposed
+
+
+ Hello, how are you!
+ Sticky Memo\n- It\'s good for a short memo.\n- Great!!
+
+
+ - @string/sample_empty
+ - @string/sample_single_line
+ - @string/sample_multi_line
+
+
\ No newline at end of file
diff --git a/java/common-kotlin/src/main/res/values/styles.xml b/samples/write-to-type/src/main/res/values/styles.xml
similarity index 55%
rename from java/common-kotlin/src/main/res/values/styles.xml
rename to samples/write-to-type/src/main/res/values/styles.xml
index f91524d..be797ff 100644
--- a/java/common-kotlin/src/main/res/values/styles.xml
+++ b/samples/write-to-type/src/main/res/values/styles.xml
@@ -1,9 +1,9 @@
+
-
-
+
+
\ No newline at end of file
diff --git a/write-to-type.gif b/write-to-type.gif
new file mode 100644
index 0000000..32ca011
Binary files /dev/null and b/write-to-type.gif differ
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy