This commit is contained in:
polwex 2024-11-24 19:19:17 +07:00
parent de9592764a
commit c06aff7133
6 changed files with 286 additions and 140 deletions

View File

@ -3,7 +3,20 @@
<component name="deploymentTargetDropDown">
<value>
<entry key="app">
<State />
<State>
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="SERIAL_NUMBER" />
<value value="8cb784d4" />
</Key>
</deviceKey>
</Target>
</runningDeviceTargetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2024-11-24T11:06:07.206200396Z" />
</State>
</entry>
</value>
</component>

View File

@ -1,8 +1,14 @@
package com.sortug.thaiinput
import android.util.Log
import com.google.gson.annotations.SerializedName
typealias InputToolsResponse = List<Any>
sealed interface InputToolsResponse{
data class Success(
val results: List<TransliterationResult>
): InputToolsResponse
data object FailedToParse: InputToolsResponse
}
data class ParsedResponse(
val status: String,
@ -13,4 +19,23 @@ data class TransliterationResult(
val wholeString: String,
val options: List<String>,
val metadata: Map<String, Any>?
)
)
public fun parseResponse(response: List<Any>): InputToolsResponse {
Log.d("THAI_IME", "parsing API response: $response")
return when (response[0] as String) {
"SUCCESS" -> {
val dataList = response[1] as List<*>
val results = dataList.map { item ->
val itemList = item as List<*>
TransliterationResult(
wholeString = itemList[0] as String,
options = (itemList[1] as List<*>).filterIsInstance<String>(),
metadata = if (itemList.size > 3) itemList[3] as? Map<String, Any> else null
)
}
InputToolsResponse.Success(results)
}
"FAILED_TO_PARSE_REQUEST_BODY" -> InputToolsResponse.FailedToParse
else -> throw IllegalStateException("Unknown response status: ${response[0]}")
}
}

View File

@ -19,7 +19,7 @@ interface GoogleInputToolsApiService {
@Query("ie") ie: String = "utf-8",
@Query("oe") oe: String = "utf-8",
@Query("app") app: String = "demopage"
): InputToolsResponse
): List<Any>
@GET("request")
suspend fun getRawInputSuggestions(
@Query("text") text: String,

View File

@ -1,8 +1,9 @@
package com.sortug.thaiinput
import android.graphics.Color
import android.graphics.Typeface
import android.inputmethodservice.InputMethodService
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputConnection
import kotlinx.coroutines.*
import android.util.Log
@ -21,6 +22,7 @@ class MyKeyboardService : InputMethodService() {
updateCurrentInputDisplay(newValue)
}
private val gson = Gson()
private lateinit var keyboardContainer: LinearLayout
private lateinit var suggestionsContainer: LinearLayout
private lateinit var currentInputDisplay: TextView
@ -28,87 +30,97 @@ class MyKeyboardService : InputMethodService() {
private fun updateCurrentInputDisplay(text: String) {
currentInputDisplay.text = text
}
private val syms = listOf(
"1234567890".toList(),
"!@#$%^&*()".toList(),
"\".:?/-=+".toList()
)
private val letters = listOf(
"QWERTYUIOP".toList(),
"ASDFGHJKL".toList(),
"ZXCVBNM".toList()
)
private val fonts = listOf(
Typeface.NORMAL,
// Typeface.createFromAsset(assets, "fonts/round.ttf")
)
private var currentFontIndex = 0
override fun onCreateInputView(): View {
val inputView = layoutInflater.inflate(R.layout.keyboard_layout, null) as LinearLayout
// options box
val scrollView = HorizontalScrollView(this).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
}
Log.d("THAI_IME", "onCreateInputView called") // Add this debug log
val inputView = layoutInflater.inflate(R.layout.keyboard_layout, null)
// Create suggestions container
suggestionsContainer = LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
}
currentInputDisplay = TextView(this).apply{
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
setMargins(10, 10, 10, 10)
}
setTextColor(Color.BLACK)
setBackgroundColor(Color.LTGRAY)
textSize = 18f
setPadding(10, 10, 10, 10)
}
// Initialize views using findViewById
currentInputDisplay = inputView.findViewById(R.id.current_input_display)
suggestionsContainer = inputView.findViewById(R.id.suggestions_container)
keyboardContainer = inputView.findViewById(R.id.keyboard_container)
inputView.addView(currentInputDisplay)
scrollView.addView(suggestionsContainer)
inputView.addView(scrollView)
setupKeyboardKeys(inputView)
// Set up the keyboard
setKeyboard(letters)
Log.d("THAI_IME", "Keyboard container child count: ${keyboardContainer.childCount}")
return inputView
}
private fun setupKeyboardKeys(keyboardView: LinearLayout){
val keys = listOf(
"qwertyuiop".toList(),
"asdfghjkl".toList(),
"zkcvbnm".toList()
)
var rowLayout: LinearLayout? = null
keys.forEachIndexed { rowIndex, row ->
val rowLayout = LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
}
if (rowIndex == 2) { // Third row
// Add Shift key
val shiftButton = createSpecialButton("") { onShiftPressed() }
rowLayout.addView(shiftButton)
}
private fun setSpecialKey(row: LinearLayout, txt: String, onClick: (String) -> Unit, width: Float = 1.5f){
val button = Button(this).apply{
text = txt
layoutParams = LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
width
).apply {
setMargins(4, 4, 4, 4)
}
setTextColor(Color.LTGRAY)
setBackgroundColor(Color.BLACK)
setOnClickListener {onClick(txt)}
}
row.addView(button)
}
private fun setKeyboard(rows: List<List<Char>>) {
rows.forEachIndexed { rowIndex, row ->
Log.d("THAI_IME", "setting row :$row $rowIndex")
val rowLayout = LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
}
if (rowIndex == 2){setSpecialKey(rowLayout, "1!", ::handleShift)}
// set keys proper
row.forEach { char ->
Log.d("THAI_IME", "setting char :$char")
val button = Button(this).apply {
text = char.toString()
layoutParams = LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f
)
setOnClickListener { onKeyPressed(if (isShiftActive) char.toString() else char.lowercaseChar().toString()) }
).apply{
setMargins(4, 4, 4, 4)
}
setTextColor(Color.LTGRAY)
textSize = 18f
setBackgroundColor(Color.BLACK)
setOnClickListener {
if (isShiftActive) {
noApiCall(char.toString())
}else{
onKeyPressed(char.lowercaseChar().toString())
}
}
}
rowLayout.addView(button)
}
if (rowIndex == 2) { // Third row
// Add Backspace key
val backspaceButton = createSpecialButton("") { onBackspace() }
rowLayout.addView(backspaceButton)
}
keyboardView.addView(rowLayout)
if (rowIndex == 2) {setSpecialKey(rowLayout,"", ::onBackspace)}
keyboardContainer.addView(rowLayout)
}
// Add a row for space
val spaceRow = LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL
layoutParams = LinearLayout.LayoutParams(
@ -116,21 +128,10 @@ class MyKeyboardService : InputMethodService() {
LinearLayout.LayoutParams.WRAP_CONTENT
)
}
val spaceButton = Button(this).apply {
text = "Space"
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
setOnClickListener { onSpace() }
}
spaceRow.addView(spaceButton)
keyboardView.addView(spaceRow)
Log.d("MyKeyboardService", "Keyboard keys set up")
setSpecialKey(spaceRow, "F", ::changeFont, 0.4f)
setSpecialKey(spaceRow, "", {_ -> noApiCall(" ")}, 1f)
setSpecialKey(spaceRow, "", {_ -> noApiCall("\n")}, 0.4f)
keyboardContainer.addView(spaceRow)
}
private fun createSpecialButton(text: String, onClickListener: () -> Unit): Button {
@ -145,35 +146,15 @@ class MyKeyboardService : InputMethodService() {
}
}
private fun onShiftPressed() {
isShiftActive = !isShiftActive
Log.d("MyKeyboardService", "Shift pressed. Active: $isShiftActive")
// You might want to update the appearance of the Shift key here
}
private fun onBackspace() {
private fun onBackspace(_thing: String) {
Log.d("MyKeyboardService", "onBackspace called")
val inputConnection = currentInputConnection ?: return
// Only make API request if we still have text
if (currentInput.isNotEmpty()) {
currentInput = currentInput.substring(0, currentInput.length - 1)
coroutineScope.launch {
try {
val response = withContext(Dispatchers.IO) {
RetrofitClient.googleInputToolsApiService.getInputSuggestions(currentInput)
}
val parsedResponse = parseResponse(response)
val options = parsedResponse.results.flatMap { it.options }
Log.d("MyKeyboardService", "Parsed options after backspace: $options")
displaySuggestions(options)
} catch (e: Exception) {
Log.e("MyKeyboardService", "Error in API call after backspace", e)
displaySuggestions(listOf("Error: ${e.message}"))
}
}
apiCall()
} else {
inputConnection.deleteSurroundingText(1, 0)
// Clear suggestions if we've deleted all text
@ -181,64 +162,56 @@ class MyKeyboardService : InputMethodService() {
}
Log.d("MyKeyboardService", "Current input after backspace: $currentInput")
}
private fun onSpace(){
val inputConnection = currentInputConnection ?: return
inputConnection.commitText(" ", 1)
}
override fun onDestroy() {
super.onDestroy()
coroutineScope.cancel()
}
private fun onKeyPressed(text: String) {
Log.d("MyKeyboardService", "onKeyPressed called with text: $text")
val inputConnection: InputConnection = currentInputConnection ?: return
currentInput += text
Log.d("MyKeyboardService", "Current input: $currentInput")
Log.d("MyKeyboardService", "Key pressed: $text")
apiCall()
}
private fun noApiCall(text: String){
val inputConnection: InputConnection = currentInputConnection ?: return
inputConnection.commitText(text, 1)
}
private fun apiCall(){
if (currentInput.isNotEmpty()){
coroutineScope.launch {
try {
// val rawResponse = withContext(Dispatchers.IO) {
// RetrofitClient.googleInputToolsApiService.getRawInputSuggestions(currentInput.toString())
// }
// val rawJson = rawResponse.body()?.string() ?: "Empty response"
// Log.d("MyKeyboardService", "Raw API Response: $rawJson")
val result = withContext(Dispatchers.IO) {
RetrofitClient.googleInputToolsApiService.getInputSuggestions(currentInput.toString())
Log.d("THAI_IME", "API request input: $currentInput")
val response = withContext(Dispatchers.IO) {
RetrofitClient.googleInputToolsApiService.getInputSuggestions(currentInput)
}
Log.d("THAI_IME", "raw API response: $response")
when (val apiResponse = parseResponse(response)){
is InputToolsResponse.Success -> {
val options = apiResponse.results.flatMap{it.options}
Log.d("MyKeyboardService", "Parsed options after backspace: $options")
displaySuggestions((options))
}
is InputToolsResponse.FailedToParse -> {
Log.e("THAI_IME", "Failed to parse request body: $currentInput")
}
}
val parsedResponse = parseResponse(result)
val options = parsedResponse.results.flatMap { it.options }
displaySuggestions(options)
} catch (e: Exception) {
Log.e("MyKeyboardService", "Unhandled error in HTTP request", e)
// Handle exceptions
Log.e("MyKeyboardService", "Error in API call after backspace", e)
displaySuggestions(listOf("Error: ${e.message}"))
}
}
}
private fun parseResponse(response: InputToolsResponse): ParsedResponse {
val status = response[0] as String
val dataList = response[1] as List<*>
val results = dataList.map { item ->
val itemList = item as List<*>
TransliterationResult(
wholeString = itemList[0] as String,
options = (itemList[1] as List<*>).filterIsInstance<String>(),
metadata = if (itemList.size > 3) itemList[3] as? Map<String, Any> else null
)
}
return ParsedResponse(status, results)
}
private fun displaySuggestions(options: List<String>){
suggestionsContainer.removeAllViews()
options.forEach{ option ->
val button = Button(this).apply{
text = option
textSize = 20f
// typeface = fonts[currentFontIndex] // Set the current font
setOnClickListener { onSuggestionClicked(option) }
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
@ -261,7 +234,88 @@ class MyKeyboardService : InputMethodService() {
suggestionsContainer.removeAllViews()
}
private fun handleShift(_thing: String){
Log.d("THAI_IME", "shift pressed")
isShiftActive = !isShiftActive
val keys = if (isShiftActive) syms else letters
keyboardContainer.removeAllViews()
setKeyboard(keys)
}
private fun changeFont(_thing: String){
currentFontIndex = (currentFontIndex + 1) % fonts.size
Log.d("THAI_IME", "🔤 Changed font to index: $currentFontIndex")
// Refresh suggestions with new font
redrawCurrentSuggestions()
}
private fun redrawCurrentSuggestions() {
val currentButtons = mutableListOf<String>()
for (i in 0 until suggestionsContainer.childCount) {
val button = suggestionsContainer.getChildAt(i) as? Button
button?.text?.toString()?.let { currentButtons.add(it) }
}
displaySuggestions(currentButtons)
}
// TODO: Add methods for handling shift and number toggle
}
object ViewDebugUtils {
fun getViewHierarchy(view: View?, level: Int = 0): String {
if (view == null) return "null"
val indent = " ".repeat(level)
val builder = StringBuilder()
// Basic view information
builder.append("$indent${view.javaClass.simpleName} {")
builder.append("\n${indent} id: ${getViewId(view)}")
builder.append("\n${indent} width: ${view.width}, height: ${view.height}")
builder.append("\n${indent} visibility: ${getVisibilityString(view.visibility)}")
// Layout parameters
val params = view.layoutParams
if (params != null) {
builder.append("\n${indent} layout_width: ${getLayoutParamSize(params.width)}")
builder.append("\n${indent} layout_height: ${getLayoutParamSize(params.height)}")
}
// For ViewGroups, recursively print children
if (view is ViewGroup) {
builder.append("\n${indent} children: [")
for (i in 0 until view.childCount) {
builder.append("\n${getViewHierarchy(view.getChildAt(i), level + 2)}")
}
if (view.childCount > 0) builder.append("\n$indent ]")
else builder.append("]")
}
builder.append("\n$indent}")
return builder.toString()
}
private fun getViewId(view: View): String {
val id = view.id
return when {
id == View.NO_ID -> "NO_ID"
else -> try {
view.resources.getResourceEntryName(id)
} catch (e: Exception) {
id.toString()
}
}
}
private fun getVisibilityString(visibility: Int): String = when (visibility) {
View.VISIBLE -> "VISIBLE"
View.INVISIBLE -> "INVISIBLE"
View.GONE -> "GONE"
else -> visibility.toString()
}
private fun getLayoutParamSize(size: Int): String = when (size) {
ViewGroup.LayoutParams.MATCH_PARENT -> "MATCH_PARENT"
ViewGroup.LayoutParams.WRAP_CONTENT -> "WRAP_CONTENT"
else -> size.toString()
}
}

View File

@ -1,8 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/keyboard_layout"
android:id="@+id/keyboard_main_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
android:background="@android:color/darker_gray">
<!-- Top Bar -->
<TextView
android:id="@+id/keyboard_title"
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="SORKB"
android:textSize="18sp"
android:textStyle="bold"
android:gravity="center"
android:textColor="@android:color/white"
android:background="@android:color/black"
android:elevation="4dp" />
<!-- Current Input Display -->
<TextView
android:id="@+id/current_input_display"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_margin="4dp"
android:padding="4dp"
android:textSize="18sp"
android:textColor="@android:color/darker_gray"
android:background="@android:color/black" />
<!-- Suggestions Scroll View -->
<HorizontalScrollView
android:id="@+id/suggestions_scroll_view"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/suggestions_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal" />
</HorizontalScrollView>
<!-- Keyboard Container -->
<LinearLayout
android:id="@+id/keyboard_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</LinearLayout>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/keyboard_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
</LinearLayout>