From ab3cfc36f076f906b954409f218d569e1bc1af35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ensar=20Saraj=C4=8Di=C4=87?= Date: Wed, 5 Jul 2023 12:18:36 +0200 Subject: [PATCH] Add autofill integration to the keyboard * Activated autofill support for our input method in method.xml * Implemented required methods in our IME * Styled views as similar as possible to the clipboard item * Added InlineContentViewHorizontalScrollView which properly clips these views, since they are owned by another process and are usually drawn above our views This closes #199 --- app/build.gradle | 1 + .../ManageClipboardItemsActivity.kt | 5 +- .../keyboard/services/SimpleKeyboardIME.kt | 92 ++++++++++++++++++- .../InlineContentViewHorizontalScrollView.kt | 84 +++++++++++++++++ .../keyboard/views/MyKeyboardView.kt | 41 +++++++-- app/src/main/res/layout/activity_main.xml | 24 ++++- .../activity_manage_clipboard_items.xml | 2 +- .../res/layout/keyboard_view_keyboard.xml | 53 +++++++---- app/src/main/res/values/dimens.xml | 2 + app/src/main/res/xml/method.xml | 5 +- 10 files changed, 276 insertions(+), 33 deletions(-) create mode 100644 app/src/main/kotlin/com/simplemobiletools/keyboard/views/InlineContentViewHorizontalScrollView.kt diff --git a/app/build.gradle b/app/build.gradle index a37b149..e9aed86 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -67,6 +67,7 @@ android { dependencies { implementation 'com.github.SimpleMobileTools:Simple-Commons:d6cddfa7d8' implementation 'androidx.emoji2:emoji2-bundled:1.2.0' + implementation 'androidx.autofill:autofill:1.1.0' kapt 'androidx.room:room-compiler:2.5.1' implementation 'androidx.room:room-runtime:2.5.1' diff --git a/app/src/main/kotlin/com/simplemobiletools/keyboard/activities/ManageClipboardItemsActivity.kt b/app/src/main/kotlin/com/simplemobiletools/keyboard/activities/ManageClipboardItemsActivity.kt index fafea61..f39dee4 100644 --- a/app/src/main/kotlin/com/simplemobiletools/keyboard/activities/ManageClipboardItemsActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/keyboard/activities/ManageClipboardItemsActivity.kt @@ -33,7 +33,7 @@ class ManageClipboardItemsActivity : SimpleActivity(), RefreshRecyclerViewListen super.onCreate(savedInstanceState) setContentView(R.layout.activity_manage_clipboard_items) setupOptionsMenu() - updateTextColors(clipboard_items_holder) + updateTextColors(suggestions_items_holder) updateClips() updateMaterialActivityViews(clipboard_coordinator, clipboard_items_list, useTransparentNavigation = true, useTopSearchMenu = false) @@ -73,14 +73,17 @@ class ManageClipboardItemsActivity : SimpleActivity(), RefreshRecyclerViewListen addOrEditClip() true } + R.id.export_clips -> { exportClips() true } + R.id.import_clips -> { importClips() true } + else -> false } } diff --git a/app/src/main/kotlin/com/simplemobiletools/keyboard/services/SimpleKeyboardIME.kt b/app/src/main/kotlin/com/simplemobiletools/keyboard/services/SimpleKeyboardIME.kt index 6c6ddfe..5a62509 100644 --- a/app/src/main/kotlin/com/simplemobiletools/keyboard/services/SimpleKeyboardIME.kt +++ b/app/src/main/kotlin/com/simplemobiletools/keyboard/services/SimpleKeyboardIME.kt @@ -1,17 +1,28 @@ package com.simplemobiletools.keyboard.services +import android.annotation.SuppressLint import android.content.SharedPreferences +import android.graphics.drawable.Icon import android.inputmethodservice.InputMethodService +import android.os.Build +import android.os.Bundle import android.text.InputType.* import android.text.TextUtils +import android.util.Size import android.view.KeyEvent import android.view.View -import android.view.inputmethod.CursorAnchorInfo -import android.view.inputmethod.EditorInfo +import android.view.ViewGroup +import android.view.inputmethod.* import android.view.inputmethod.EditorInfo.IME_ACTION_NONE import android.view.inputmethod.EditorInfo.IME_FLAG_NO_ENTER_ACTION import android.view.inputmethod.EditorInfo.IME_MASK_ACTION -import android.view.inputmethod.ExtractedTextRequest +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 com.simplemobiletools.commons.extensions.getSharedPrefs import com.simplemobiletools.keyboard.R import com.simplemobiletools.keyboard.extensions.config @@ -90,6 +101,38 @@ class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, Shared keyboardView?.invalidateAllKeys() } + @RequiresApi(Build.VERSION_CODES.R) + override fun onCreateInlineSuggestionsRequest(uiExtras: Bundle): InlineSuggestionsRequest { + val minWidth = resources.getDimensionPixelSize(R.dimen.suggestion_min_width) + val maxWidth = resources.getDimensionPixelSize(R.dimen.suggestion_max_width) + + return InlineSuggestionsRequest.Builder( + listOf( + InlinePresentationSpec.Builder( + Size(minWidth, ViewGroup.LayoutParams.WRAP_CONTENT), + Size(maxWidth, ViewGroup.LayoutParams.WRAP_CONTENT) + ).setStyle(buildSuggestionTextStyle()).build() + ) + ).setMaxSuggestionCount(InlineSuggestionsRequest.SUGGESTION_COUNT_UNLIMITED) + .build() + } + + @RequiresApi(Build.VERSION_CODES.R) + override fun onInlineSuggestionsResponse(response: InlineSuggestionsResponse): Boolean { + keyboardView?.clearClipboardViews() + + response.inlineSuggestions.forEach { + it.inflate(this, Size(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT), this.mainExecutor) { view -> + // If inflation fails for whatever reason, passed view will be null + if (view != null) { + keyboardView?.addToClipboardViews(view, addToFront = it.info.isPinned) + } + } + } + + return true + } + override fun onKey(code: Int) { val inputConnection = currentInputConnection if (keyboard == null || inputConnection == null) { @@ -201,6 +244,7 @@ class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, Shared inputConnection.commitText(codeChar.toString(), 1) } } + else -> { inputConnection.commitText(codeChar.toString(), 1) if (originalText == null) { @@ -335,6 +379,48 @@ class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, Shared } } + @RequiresApi(Build.VERSION_CODES.R) + @SuppressLint("RestrictedApi", "UseCompatLoadingForDrawables") + private fun buildSuggestionTextStyle(): Bundle { + val stylesBuilder = UiVersions.newStylesBuilder() + + val verticalPadding = resources.getDimensionPixelSize(R.dimen.small_margin) + val horizontalPadding = resources.getDimensionPixelSize(R.dimen.activity_margin) + + val textSize = resources.getDimension(R.dimen.label_text_size) / resources.displayMetrics.scaledDensity + + val chipStyle = + ViewStyle.Builder() + .setBackground(Icon.createWithResource(this, R.drawable.clipboard_background)) + .setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding) + .build() + + val iconStyle = ImageViewStyle.Builder().build() + + val style = InlineSuggestionUi.newStyleBuilder() + .setSingleIconChipStyle(chipStyle) + .setChipStyle(chipStyle) + .setStartIconStyle(iconStyle) + .setEndIconStyle(iconStyle) + .setSingleIconChipIconStyle(iconStyle) + .setTitleStyle( + TextViewStyle.Builder() + .setLayoutMargin(0, 0, horizontalPadding, 0) + .setTextColor(resources.getColor(R.color.default_text_color, theme)) + .setTextSize(textSize) + .build() + ) + .setSubtitleStyle( + TextViewStyle.Builder() + .setTextColor(resources.getColor(R.color.default_text_color, theme)) + .setTextSize(textSize) + .build() + ) + .build() + stylesBuilder.addStyle(style) + return stylesBuilder.build() + } + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { keyboardView?.setupKeyboard() } diff --git a/app/src/main/kotlin/com/simplemobiletools/keyboard/views/InlineContentViewHorizontalScrollView.kt b/app/src/main/kotlin/com/simplemobiletools/keyboard/views/InlineContentViewHorizontalScrollView.kt new file mode 100644 index 0000000..c3e7ab1 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/keyboard/views/InlineContentViewHorizontalScrollView.kt @@ -0,0 +1,84 @@ +package com.simplemobiletools.keyboard.views + +import android.content.Context +import android.graphics.Rect +import android.os.Build +import android.util.AttributeSet +import android.view.View +import android.view.ViewTreeObserver.OnDrawListener +import android.widget.HorizontalScrollView +import android.widget.inline.InlineContentView +import androidx.annotation.AttrRes +import androidx.core.view.allViews +import com.simplemobiletools.commons.extensions.beInvisible +import com.simplemobiletools.commons.extensions.beVisible + + +/** + * [HorizontalScrollView] adapted for holding [InlineContentView] instances + * It can hold other views too, but it will ensure [InlineContentView] instances + * these are properly clipped and not drawn over rest of the window, + * but still remaining clickable + * (since setting [InlineContentView.setZOrderedOnTop] to false prevents clicking) + */ +class InlineContentViewHorizontalScrollView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, + @AttrRes defStyleAttr: Int = 0 +) : HorizontalScrollView(context, attrs, defStyleAttr), OnDrawListener { + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + viewTreeObserver.addOnDrawListener(this) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + viewTreeObserver.removeOnDrawListener(this) + } + + override fun onScrollChanged(l: Int, t: Int, oldl: Int, oldt: Int) { + super.onScrollChanged(l, t, oldl, oldt) + clipDescendantInlineContentViews() + } + + override fun onDraw() { + clipDescendantInlineContentViews() + } + + fun hideAllInlineContentViews() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + return + } + allViews.forEach { + if (it is InlineContentView) { + it.beInvisible() + } + } + } + + fun showAllInlineContentViews() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + return + } + allViews.forEach { + if (it is InlineContentView) { + it.beVisible() + } + } + } + + private fun clipDescendantInlineContentViews() { + // This is only needed for InlineContentViews which are not available before this version + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + return + } + + allViews.forEach { + if (it is InlineContentView) { + val parentBounds = Rect(scrollX, scrollY, width + scrollX, height + scrollY) + offsetRectIntoDescendantCoords(it, parentBounds) + it.clipBounds = parentBounds + } + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/keyboard/views/MyKeyboardView.kt b/app/src/main/kotlin/com/simplemobiletools/keyboard/views/MyKeyboardView.kt index 821f3a5..332745d 100644 --- a/app/src/main/kotlin/com/simplemobiletools/keyboard/views/MyKeyboardView.kt +++ b/app/src/main/kotlin/com/simplemobiletools/keyboard/views/MyKeyboardView.kt @@ -10,6 +10,7 @@ import android.content.Intent import android.graphics.* import android.graphics.Paint.Align import android.graphics.drawable.* +import android.os.Build import android.os.Handler import android.os.Looper import android.os.Message @@ -18,11 +19,14 @@ import android.util.TypedValue import android.view.* import android.view.animation.AccelerateInterpolator import android.view.inputmethod.EditorInfo +import android.widget.LinearLayout import android.widget.PopupWindow import android.widget.TextView +import android.widget.inline.InlineContentView +import androidx.annotation.RequiresApi import androidx.core.animation.doOnEnd import androidx.core.animation.doOnStart -import androidx.core.view.ViewCompat +import androidx.core.view.* import androidx.emoji2.text.EmojiCompat import androidx.emoji2.text.EmojiCompat.EMOJI_SUPPORTED import com.simplemobiletools.commons.extensions.* @@ -746,8 +750,8 @@ class MyKeyboardView @JvmOverloads constructor(context: Context, attrs: Attribut private fun hideClipboardViews() { mToolbarHolder?.apply { - clipboard_value_holder?.beGone() - clipboard_value_holder?.alpha = 0f + clipboard_value?.beGone() + clipboard_value?.alpha = 0f clipboard_clear?.beGone() clipboard_clear?.alpha = 0f } @@ -764,10 +768,10 @@ class MyKeyboardView @JvmOverloads constructor(context: Context, attrs: Attribut } private fun toggleClipboardVisibility(show: Boolean) { - if ((show && mToolbarHolder?.clipboard_value_holder!!.alpha == 0f) || (!show && mToolbarHolder?.clipboard_value_holder!!.alpha == 1f)) { + if ((show && mToolbarHolder?.clipboard_value!!.alpha == 0f) || (!show && mToolbarHolder?.clipboard_value!!.alpha == 1f)) { val newAlpha = if (show) 1f else 0f val animations = ArrayList() - val clipboardValueAnimation = ObjectAnimator.ofFloat(mToolbarHolder!!.clipboard_value_holder!!, "alpha", newAlpha) + val clipboardValueAnimation = ObjectAnimator.ofFloat(mToolbarHolder!!.clipboard_value!!, "alpha", newAlpha) animations.add(clipboardValueAnimation) val clipboardClearAnimation = ObjectAnimator.ofFloat(mToolbarHolder!!.clipboard_clear!!, "alpha", newAlpha) @@ -779,13 +783,13 @@ class MyKeyboardView @JvmOverloads constructor(context: Context, attrs: Attribut animSet.interpolator = AccelerateInterpolator() animSet.doOnStart { if (show) { - mToolbarHolder?.clipboard_value_holder?.beVisible() + mToolbarHolder?.clipboard_value?.beVisible() mToolbarHolder?.clipboard_clear?.beVisible() } } animSet.doOnEnd { if (!show) { - mToolbarHolder?.clipboard_value_holder?.beGone() + mToolbarHolder?.clipboard_value?.beGone() mToolbarHolder?.clipboard_clear?.beGone() } } @@ -1381,10 +1385,12 @@ class MyKeyboardView @JvmOverloads constructor(context: Context, attrs: Attribut fun closeClipboardManager() { mClipboardManagerHolder?.clipboard_manager_holder?.beGone() + mToolbarHolder?.suggestions_holder?.showAllInlineContentViews() } private fun openClipboardManager() { mClipboardManagerHolder!!.clipboard_manager_holder.beVisible() + mToolbarHolder?.suggestions_holder?.hideAllInlineContentViews() setupStoredClips() } @@ -1614,4 +1620,25 @@ class MyKeyboardView @JvmOverloads constructor(context: Context, attrs: Attribut } return keyColor } + + @RequiresApi(Build.VERSION_CODES.R) + fun addToClipboardViews(it: InlineContentView, addToFront: Boolean = false) { + if (mToolbarHolder?.autofill_suggestions_holder != null) { + val newLayoutParams = LinearLayout.LayoutParams(it.layoutParams) + newLayoutParams.updateMarginsRelative(start = resources.getDimensionPixelSize(R.dimen.normal_margin)) + it.layoutParams = newLayoutParams + if (addToFront) { + mToolbarHolder?.autofill_suggestions_holder?.addView(it, 0) + } else { + mToolbarHolder?.autofill_suggestions_holder?.addView(it) + } + } + } + + @RequiresApi(Build.VERSION_CODES.R) + fun clearClipboardViews() { + if (mToolbarHolder?.autofill_suggestions_holder != null) { + mToolbarHolder?.autofill_suggestions_holder?.removeAllViews() + } + } } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 95a29fc..f969184 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -49,13 +49,13 @@ + android:layout_marginBottom="@dimen/activity_margin" + android:inputType="textCapSentences" /> + + + + diff --git a/app/src/main/res/layout/activity_manage_clipboard_items.xml b/app/src/main/res/layout/activity_manage_clipboard_items.xml index fa100a1..a828651 100644 --- a/app/src/main/res/layout/activity_manage_clipboard_items.xml +++ b/app/src/main/res/layout/activity_manage_clipboard_items.xml @@ -23,7 +23,7 @@ android:scrollbars="none"> diff --git a/app/src/main/res/layout/keyboard_view_keyboard.xml b/app/src/main/res/layout/keyboard_view_keyboard.xml index 16f5682..fc6527b 100644 --- a/app/src/main/res/layout/keyboard_view_keyboard.xml +++ b/app/src/main/res/layout/keyboard_view_keyboard.xml @@ -27,8 +27,8 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - - + android:layout_height="match_parent" + android:orientation="horizontal"> - + + + + + + + + 46dp 4dp 42dp + 200dp + 100dp 22sp 26sp diff --git a/app/src/main/res/xml/method.xml b/app/src/main/res/xml/method.xml index 3acad80..46771d5 100644 --- a/app/src/main/res/xml/method.xml +++ b/app/src/main/res/xml/method.xml @@ -1,7 +1,10 @@ + android:settingsActivity="com.simplemobiletools.keyboard.activities.SettingsActivity" + android:supportsInlineSuggestions="true" + tools:targetApi="r">