From a491151a3b5d60717ed8143e4f754777921ce10e Mon Sep 17 00:00:00 2001 From: goofyz Date: Tue, 14 Jan 2025 08:57:49 +0800 Subject: [PATCH] feat: request and handle inline suggestion in `TrimeInputMethodService` Add `InlineSuggestionHandler` to create request for inline suggestion and handle the responded suggestion. --- .../suggestion/InlineSuggestionHandler.kt | 121 ++++++++++++++++++ .../trime/ime/core/TrimeInputMethodService.kt | 20 +++ .../res/drawable/bg_inline_suggestion.xml | 22 ++++ 3 files changed, 163 insertions(+) create mode 100644 app/src/main/java/com/osfans/trime/ime/candidates/suggestion/InlineSuggestionHandler.kt create mode 100644 app/src/main/res/drawable/bg_inline_suggestion.xml diff --git a/app/src/main/java/com/osfans/trime/ime/candidates/suggestion/InlineSuggestionHandler.kt b/app/src/main/java/com/osfans/trime/ime/candidates/suggestion/InlineSuggestionHandler.kt new file mode 100644 index 0000000000..e15ead4e6a --- /dev/null +++ b/app/src/main/java/com/osfans/trime/ime/candidates/suggestion/InlineSuggestionHandler.kt @@ -0,0 +1,121 @@ +/* + * SPDX-FileCopyrightText: 2015 - 2025 Rime community + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.osfans.trime.ime.candidates.suggestion + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.drawable.Icon +import android.os.Build +import android.util.Size +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InlineSuggestionsRequest +import android.view.inputmethod.InlineSuggestionsResponse +import android.widget.inline.InlinePresentationSpec +import androidx.annotation.RequiresApi +import androidx.autofill.inline.UiVersions +import androidx.autofill.inline.common.ImageViewStyle +import androidx.autofill.inline.common.TextViewStyle +import androidx.autofill.inline.common.ViewStyle +import androidx.autofill.inline.v1.InlineSuggestionUi +import androidx.core.graphics.ColorUtils +import com.osfans.trime.R +import com.osfans.trime.data.theme.ColorManager +import splitties.dimensions.dp +import java.util.concurrent.Executor +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class InlineSuggestionHandler( + private val context: Context, +) { + @SuppressLint("NewApi", "RestrictedApi") + fun createRequest(): InlineSuggestionsRequest { + val firstTextColor = ColorManager.getColor("candidate_text_color")!! + val backColor = ColorManager.getColor("candidate_background")!! + + val style = + InlineSuggestionUi + .newStyleBuilder() + .setSingleIconChipStyle( + ViewStyle + .Builder() + .setBackgroundColor(Color.TRANSPARENT) + .setPadding(0, 0, 0, 0) + .build(), + ).setChipStyle( + ViewStyle + .Builder() + .setBackground( + Icon.createWithResource(context, R.drawable.bg_inline_suggestion).apply { + setTint(ColorUtils.blendARGB(backColor, firstTextColor, 0.2f)) + }, + ).build(), + ).setTitleStyle( + TextViewStyle + .Builder() + .setLayoutMargin(context.dp(4), 0, context.dp(4), 0) + .setTextColor(firstTextColor) + .setTextSize(14f) + .build(), + ).setSubtitleStyle( + TextViewStyle + .Builder() + .setTextColor( + ColorUtils.blendARGB(firstTextColor, backColor, 0.3f), + ).setTextSize(12f) + .build(), + ).setStartIconStyle( + ImageViewStyle + .Builder() + .setTintList(ColorStateList.valueOf(firstTextColor)) + .build(), + ).setEndIconStyle( + ImageViewStyle + .Builder() + .setTintList(ColorStateList.valueOf(firstTextColor)) + .build(), + ).build() + val styleBundle = + UiVersions + .newStylesBuilder() + .addStyle(style) + .build() + val spec = + InlinePresentationSpec + .Builder(Size(0, 0), Size(context.dp(160), Int.MAX_VALUE)) + .setStyle(styleBundle) + .build() + return InlineSuggestionsRequest + .Builder(listOf(spec)) + .setMaxSuggestionCount(InlineSuggestionsRequest.SUGGESTION_COUNT_UNLIMITED) + .build() + } + + private val suggestionSize by lazy { + Size(ViewGroup.LayoutParams.WRAP_CONTENT, context.dp(INLINE_SUGGESTION_HEIGHT)) + } + + @RequiresApi(Build.VERSION_CODES.R) + suspend fun inflateSuggestion(response: InlineSuggestionsResponse): List = + response.inlineSuggestions.map { + suspendCoroutine { c -> + it.inflate(context, suggestionSize, directExecutor) { v -> + c.resume(v) + } + } + } + + companion object { + private const val INLINE_SUGGESTION_HEIGHT = 40 + + private val directExecutor by lazy { + Executor { it.run() } + } + } +} diff --git a/app/src/main/java/com/osfans/trime/ime/core/TrimeInputMethodService.kt b/app/src/main/java/com/osfans/trime/ime/core/TrimeInputMethodService.kt index 58b80a3575..e53adc9e9b 100644 --- a/app/src/main/java/com/osfans/trime/ime/core/TrimeInputMethodService.kt +++ b/app/src/main/java/com/osfans/trime/ime/core/TrimeInputMethodService.kt @@ -13,6 +13,7 @@ import android.content.Intent import android.content.res.Configuration import android.inputmethodservice.InputMethodService import android.os.Build +import android.os.Bundle import android.os.SystemClock import android.text.InputType import android.view.InputDevice @@ -25,6 +26,8 @@ import android.view.WindowManager import android.view.inputmethod.CursorAnchorInfo import android.view.inputmethod.EditorInfo import android.view.inputmethod.ExtractedTextRequest +import android.view.inputmethod.InlineSuggestionsRequest +import android.view.inputmethod.InlineSuggestionsResponse import android.widget.FrameLayout import androidx.annotation.Keep import androidx.core.content.ContextCompat @@ -48,6 +51,7 @@ import com.osfans.trime.data.theme.ColorManager import com.osfans.trime.data.theme.Theme import com.osfans.trime.data.theme.ThemeManager import com.osfans.trime.ime.broadcast.IntentReceiver +import com.osfans.trime.ime.candidates.suggestion.InlineSuggestionHandler import com.osfans.trime.ime.composition.ComposingPopupWindow import com.osfans.trime.ime.keyboard.InitializationUi import com.osfans.trime.ime.keyboard.InputFeedbackManager @@ -87,6 +91,8 @@ open class TrimeInputMethodService : LifecycleInputMethodService() { private var mIntentReceiver: IntentReceiver? = null private val locales = Array(2) { Locale.getDefault() } + private lateinit var inlineSuggestionHandler: InlineSuggestionHandler + var lastCommittedText: CharSequence = "" private set @@ -204,6 +210,7 @@ open class TrimeInputMethodService : LifecycleInputMethodService() { 2 -> Locale(latinLocale[0], latinLocale[1]) else -> Locale.US } + inlineSuggestionHandler = InlineSuggestionHandler(this@TrimeInputMethodService) Timber.d("Trime.onCreate completed") } } catch (e: Exception) { @@ -438,6 +445,19 @@ open class TrimeInputMethodService : LifecycleInputMethodService() { } } + @SuppressLint("NewApi", "RestrictedApi") + override fun onCreateInlineSuggestionsRequest(uiExtras: Bundle): InlineSuggestionsRequest? = inlineSuggestionHandler.createRequest() + + @SuppressLint("NewApi") + override fun onInlineSuggestionsResponse(response: InlineSuggestionsResponse): Boolean { + lifecycleScope.launch { + val views = inlineSuggestionHandler.inflateSuggestion(response) + + inputView?.updateInlineSuggestion(views) + } + return true + } + override fun onStartInputView( attribute: EditorInfo, restarting: Boolean, diff --git a/app/src/main/res/drawable/bg_inline_suggestion.xml b/app/src/main/res/drawable/bg_inline_suggestion.xml new file mode 100644 index 0000000000..151468b321 --- /dev/null +++ b/app/src/main/res/drawable/bg_inline_suggestion.xml @@ -0,0 +1,22 @@ + + + + + + + + + + +