diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..e8eecbf --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,93 @@ +name: Knowre Android Library CI + +on: + push: + branches: [ "main" ] + pull_request: + types: + - opened + - synchronize + branches: [ "main" ] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Gradle Caching + uses: actions/cache@v4.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ github.ref_name }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Run unit test + run: ./gradlew testDebugUnitTest + + - name: Notify to slack + run: | + STATUS="Build Succeeded!" + COLOR="good" + if [[ ${{ job.status }} == "failure" ]]; then + STATUS="Build Failed!" + COLOR="danger" + elif [[ ${{ job.status }} == "cancelled" ]]; then + STATUS="Build Cancelled!" + COLOR="warning" + fi + + BUILD_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + + curl -X POST -H 'Content-type: application/json' --data \ + '{ + "attachments": [ + { + "color": "'"$COLOR"'", + "pretext": "'"$STATUS"'", + "fields": [ + { + "title": "User", + "value": "${{ github.actor }}", + "short": true + }, + { + "title": "Commit Message", + "value": "${{ github.event.head_commit.message }}", + "short": false + }, + { + "title": "Branch", + "value": "${{ github.ref_name }}", + "short": true + } + ], + "actions": [ + { + "type": "button", + "text": "View Build", + "url": "'"${BUILD_URL}"'" + } + ] + } + ] + }' ${{ secrets.SLACK_WEB_HOOK_URL }} + + + diff --git a/app/build.gradle b/app/build.gradle index 947662a..26e41cf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -42,6 +42,8 @@ android { dependencies { implementation project("${getImplementationPrefix(project.parent.name)}:extension-standard") implementation project("${getImplementationPrefix(project.parent.name)}:extension-android") + implementation project("${getImplementationPrefix(project.parent.name)}:myscript-iink") + implementation project("${getImplementationPrefix(project.parent.name)}:myscript-iink:UIReferenceImplementation") implementation 'androidx.core:core-ktx:1.8.0' implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'com.google.android.material:material:1.9.0' diff --git a/app/release/app-myscript-test-release.apk b/app/release/app-myscript-test-release.apk new file mode 100644 index 0000000..192e25f Binary files /dev/null and b/app/release/app-myscript-test-release.apk differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json new file mode 100644 index 0000000..cdd942b --- /dev/null +++ b/app/release/output-metadata.json @@ -0,0 +1,20 @@ +{ + "version": 3, + "artifactType": { + "type": "APK", + "kind": "Directory" + }, + "applicationId": "com.knowre.android.kal", + "variantName": "release", + "elements": [ + { + "type": "SINGLE", + "filters": [], + "attributes": [], + "versionCode": 1, + "versionName": "1.0", + "outputFile": "app-release.apk" + } + ], + "elementType": "File" +} \ No newline at end of file diff --git a/app/src/main/assets/n_digit_exp.res b/app/src/main/assets/n_digit_exp.res new file mode 100644 index 0000000..b2e13d8 Binary files /dev/null and b/app/src/main/assets/n_digit_exp.res differ diff --git a/app/src/main/java/com/knowre/android/kal/FirstFragment.kt b/app/src/main/java/com/knowre/android/kal/FirstFragment.kt index c1087a0..3f19325 100644 --- a/app/src/main/java/com/knowre/android/kal/FirstFragment.kt +++ b/app/src/main/java/com/knowre/android/kal/FirstFragment.kt @@ -1,10 +1,10 @@ package com.knowre.android.kal import android.os.Bundle -import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import com.knowre.android.kal.databinding.FragmentFirstBinding diff --git a/app/src/main/java/com/knowre/android/kal/MainActivity.kt b/app/src/main/java/com/knowre/android/kal/MainActivity.kt index eefb646..d5a0cd6 100644 --- a/app/src/main/java/com/knowre/android/kal/MainActivity.kt +++ b/app/src/main/java/com/knowre/android/kal/MainActivity.kt @@ -8,7 +8,6 @@ import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.navigateUp import androidx.navigation.ui.setupActionBarWithNavController -import com.google.android.material.snackbar.Snackbar import com.knowre.android.kal.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { @@ -27,10 +26,6 @@ class MainActivity : AppCompatActivity() { val navController = findNavController(R.id.nav_host_fragment_content_main) appBarConfiguration = AppBarConfiguration(navController.graph) setupActionBarWithNavController(navController, appBarConfiguration) - - binding.fab.setOnClickListener { view -> - Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG).setAction("Action", null).show() - } } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -50,4 +45,8 @@ class MainActivity : AppCompatActivity() { return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() } + override fun onBackPressed() { + + } + } \ No newline at end of file diff --git a/app/src/main/java/com/knowre/android/kal/myscript/Candidate.kt b/app/src/main/java/com/knowre/android/kal/myscript/Candidate.kt new file mode 100644 index 0000000..b3c8343 --- /dev/null +++ b/app/src/main/java/com/knowre/android/kal/myscript/Candidate.kt @@ -0,0 +1,9 @@ +package com.knowre.android.kal.myscript + + +internal sealed class Candidate { + + class Data(val itemId: String, val label: String) : Candidate() + + class Exit() : Candidate() +} \ No newline at end of file diff --git a/app/src/main/java/com/knowre/android/kal/myscript/CandidateAdapter.kt b/app/src/main/java/com/knowre/android/kal/myscript/CandidateAdapter.kt new file mode 100644 index 0000000..44fbd33 --- /dev/null +++ b/app/src/main/java/com/knowre/android/kal/myscript/CandidateAdapter.kt @@ -0,0 +1,81 @@ +package com.knowre.android.kal.myscript + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.knowre.android.kal.databinding.ViewCandidateItemBinding + + +internal class CandidateAdapter( + private val onCandidateClicked: (Candidate.Data) -> Unit, + private val onExitClicked: () -> Unit +) : RecyclerView.Adapter() { + + private var candidates = listOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CandidateViewHolder { + return CandidateViewHolder.newInstance(parent, + onCandidateClicked = { + clear() + onCandidateClicked(it) + }, + onExitClicked = { + clear() + onExitClicked() + } + ) + } + + override fun onBindViewHolder(holder: CandidateViewHolder, position: Int) { + holder.bind(candidates[position]) + } + + override fun getItemCount() = candidates.size + + fun setCandidates(candidates: List) { + this.candidates = candidates + .toMutableList() + .apply { if (isNotEmpty()) add(Candidate.Exit()) } + .also { notifyDataSetChanged() } + } + + fun clear() { + this.candidates = listOf() + notifyDataSetChanged() + } +} + +internal class CandidateViewHolder( + private val binding: ViewCandidateItemBinding +) : RecyclerView.ViewHolder(binding.root) { + + private lateinit var candidate: Candidate + + fun bind(candidate: Candidate) { + this.candidate = candidate + when (candidate) { + is Candidate.Data -> binding.candidateText.text = candidate.label + is Candidate.Exit -> { + binding.candidateText.text = "X" + } + } + } + + companion object { + fun newInstance( + parent: ViewGroup, + onCandidateClicked: (Candidate.Data) -> Unit, + onExitClicked: () -> Unit + ): CandidateViewHolder { + return CandidateViewHolder(ViewCandidateItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)).apply { + binding.root.setOnClickListener { + if (candidate is Candidate.Data) { + onCandidateClicked(candidate as Candidate.Data) + } else { + onExitClicked() + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/knowre/android/kal/myscript/MyScriptPadView.kt b/app/src/main/java/com/knowre/android/kal/myscript/MyScriptPadView.kt new file mode 100644 index 0000000..07f816d --- /dev/null +++ b/app/src/main/java/com/knowre/android/kal/myscript/MyScriptPadView.kt @@ -0,0 +1,188 @@ +package com.knowre.android.kal.myscript + +import android.content.Context +import android.content.res.AssetManager +import android.content.res.ColorStateList +import android.content.res.Resources +import android.graphics.Color +import android.util.AttributeSet +import android.util.Log +import android.util.TypedValue +import android.view.LayoutInflater +import android.widget.FrameLayout +import android.widget.Toast +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.shape.MaterialShapeDrawable +import com.google.android.material.shape.ShapeAppearanceModel +import com.knowre.android.kal.databinding.ViewMyscriptPadBinding +import com.knowre.android.myscript.iink.MyScriptApi +import com.knowre.android.myscript.iink.MyScriptInitializer +import com.knowre.android.myscript.iink.MyScriptInterpretListener +import com.knowre.android.myscript.iink.ToolFunction +import com.knowre.android.myscript.iink.ToolType +import com.myscript.iink.Editor +import com.myscript.iink.EditorError +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch + + +internal class MyScriptPadView constructor( + context: Context, + attrs: AttributeSet? = null + +) : FrameLayout(context, attrs) { + + private val binding = ViewMyscriptPadBinding.inflate(LayoutInflater.from(context), this, true) + + private val mainScope = MainScope() + + private lateinit var myScript: MyScriptApi + + private val candidateAdapter = CandidateAdapter( + onCandidateClicked = { candidate -> }, + onExitClicked = {} + ) + + init { + initializeMyScript() + initializeRecyclerView() + initializeToolsListener() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + mainScope.cancel() + } + + private fun initializeMyScript() { + mainScope.launch { + myScript = MyScriptInitializer( + certificate = byteArrayOf(), + myScriptView = binding.myScriptView, + context = context, + scope = mainScope + ) + .initialize() + .apply { addListener(interpretListener) } + .apply { isAutoConvertEnabled = false } + } + } + + private fun initializeRecyclerView() { + with(binding.candidate) { + adapter = candidateAdapter + layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) + background = MaterialShapeDrawable( + ShapeAppearanceModel.Builder() + .setAllCornerSizes(8F.dp) + .build() + ).apply { + tintList = ColorStateList.valueOf(Color.parseColor("#FFFFFF")) + strokeWidth = 1F.dp + strokeColor = ColorStateList.valueOf(Color.parseColor("#CFD8DC")) + elevation = 4F.dp + } + } + } + + private fun initializeToolsListener() { + binding.redo.isEnabled = false + binding.undo.isEnabled = false + + binding.deleteAll.setOnClickListener { myScript.eraseAll() } + binding.digitOnlyGrammar.setOnClickListener { + myScript.loadMathGrammar("n_digit_exp", context.assets.toByteArray("n_digit_exp.res")) + } + + binding.defaultGrammar.setOnClickListener { + //TODO + } + + binding.red.setOnClickListener { + myScript.penColor = 0xFF0000 + } + + binding.blue.setOnClickListener { + myScript.penColor = 0x0000FF + } + + binding.black.setOnClickListener { + myScript.penColor = 0x000000 + } + + binding.convert.setOnClickListener { myScript.convert() } + + binding.penSwitch.setOnCheckedChangeListener { _, isChecked -> + binding.eraserSwitch.isChecked = false + myScript.tool = if (isChecked) { + MyScriptApi.Tool( + toolType = ToolType.PEN, + toolFunction = ToolFunction.DRAWING + ) + } else { + MyScriptApi.Tool( + toolType = ToolType.HAND, + toolFunction = ToolFunction.DRAWING + ) + } + } + + binding.convertSwitch.setOnCheckedChangeListener { _, isChecked -> + myScript.isAutoConvertEnabled = isChecked + } + + binding.eraserSwitch.setOnCheckedChangeListener { _, isChecked -> + val toolType = if (binding.penSwitch.isChecked) ToolType.PEN else ToolType.HAND + if (isChecked) { + myScript.tool = MyScriptApi.Tool(toolType, ToolFunction.ERASING) + } else { + myScript.tool = MyScriptApi.Tool(toolType, ToolFunction.DRAWING) + } + } + + binding.redo.setOnClickListener { + myScript.redo() + } + + binding.undo.setOnClickListener { + myScript.undo() + } + + binding.candidateSwitch.setOnCheckedChangeListener { _, isChecked -> } + } + + private val interpretListener: MyScriptInterpretListener + get() = object : MyScriptInterpretListener { + override fun onInterpreted(interpreted: String) { + binding.latex.text = interpreted + binding.redo.isEnabled = myScript.canRedo + binding.undo.isEnabled = myScript.canUndo + } + + override fun onInterpretError(editor: Editor, blockId: String, error: EditorError, message: String) { + Log.d("MY_SCRIPT_ERROR", "$error with message $message") + } + + override fun onImportError() { + Toast + .makeText(context, "해당 문자로는 변경이 불가능합니다.", Toast.LENGTH_SHORT) + .show() + } + } + + private fun showNoCandidateAvailable() { + Toast + .makeText(context, "No candidates available.", Toast.LENGTH_SHORT) + .show() + } + + private fun AssetManager.toByteArray(fileName: String) = open(fileName).use { it.readBytes() } + + private val Number.dp: Float + get() = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + toFloat(), + Resources.getSystem().displayMetrics + ) +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 9fec30d..3c4722d 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -20,15 +20,6 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_first.xml b/app/src/main/res/layout/fragment_first.xml index 83bb21c..531b331 100644 --- a/app/src/main/res/layout/fragment_first.xml +++ b/app/src/main/res/layout/fragment_first.xml @@ -10,20 +10,17 @@ android:id="@+id/button_first" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginTop="50dp" android:text="@string/next" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/textview_first" /> + app:layout_constraintStart_toStartOf="parent" /> - + app:layout_constraintBottom_toBottomOf="parent"/> \ No newline at end of file diff --git a/app/src/main/res/layout/view_candidate_item.xml b/app/src/main/res/layout/view_candidate_item.xml new file mode 100644 index 0000000..a17adf8 --- /dev/null +++ b/app/src/main/res/layout/view_candidate_item.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_myscript_pad.xml b/app/src/main/res/layout/view_myscript_pad.xml new file mode 100644 index 0000000..d6ac65c --- /dev/null +++ b/app/src/main/res/layout/view_myscript_pad.xml @@ -0,0 +1,166 @@ + + + + + + + +