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
This commit is contained in:
Ensar Sarajčić 2023-07-05 12:18:36 +02:00
parent e41c6c0817
commit ab3cfc36f0
10 changed files with 276 additions and 33 deletions

View File

@ -67,6 +67,7 @@ android {
dependencies { dependencies {
implementation 'com.github.SimpleMobileTools:Simple-Commons:d6cddfa7d8' implementation 'com.github.SimpleMobileTools:Simple-Commons:d6cddfa7d8'
implementation 'androidx.emoji2:emoji2-bundled:1.2.0' implementation 'androidx.emoji2:emoji2-bundled:1.2.0'
implementation 'androidx.autofill:autofill:1.1.0'
kapt 'androidx.room:room-compiler:2.5.1' kapt 'androidx.room:room-compiler:2.5.1'
implementation 'androidx.room:room-runtime:2.5.1' implementation 'androidx.room:room-runtime:2.5.1'

View File

@ -33,7 +33,7 @@ class ManageClipboardItemsActivity : SimpleActivity(), RefreshRecyclerViewListen
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_manage_clipboard_items) setContentView(R.layout.activity_manage_clipboard_items)
setupOptionsMenu() setupOptionsMenu()
updateTextColors(clipboard_items_holder) updateTextColors(suggestions_items_holder)
updateClips() updateClips()
updateMaterialActivityViews(clipboard_coordinator, clipboard_items_list, useTransparentNavigation = true, useTopSearchMenu = false) updateMaterialActivityViews(clipboard_coordinator, clipboard_items_list, useTransparentNavigation = true, useTopSearchMenu = false)
@ -73,14 +73,17 @@ class ManageClipboardItemsActivity : SimpleActivity(), RefreshRecyclerViewListen
addOrEditClip() addOrEditClip()
true true
} }
R.id.export_clips -> { R.id.export_clips -> {
exportClips() exportClips()
true true
} }
R.id.import_clips -> { R.id.import_clips -> {
importClips() importClips()
true true
} }
else -> false else -> false
} }
} }

View File

@ -1,17 +1,28 @@
package com.simplemobiletools.keyboard.services package com.simplemobiletools.keyboard.services
import android.annotation.SuppressLint
import android.content.SharedPreferences import android.content.SharedPreferences
import android.graphics.drawable.Icon
import android.inputmethodservice.InputMethodService import android.inputmethodservice.InputMethodService
import android.os.Build
import android.os.Bundle
import android.text.InputType.* import android.text.InputType.*
import android.text.TextUtils import android.text.TextUtils
import android.util.Size
import android.view.KeyEvent import android.view.KeyEvent
import android.view.View import android.view.View
import android.view.inputmethod.CursorAnchorInfo import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.*
import android.view.inputmethod.EditorInfo.IME_ACTION_NONE import android.view.inputmethod.EditorInfo.IME_ACTION_NONE
import android.view.inputmethod.EditorInfo.IME_FLAG_NO_ENTER_ACTION import android.view.inputmethod.EditorInfo.IME_FLAG_NO_ENTER_ACTION
import android.view.inputmethod.EditorInfo.IME_MASK_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.commons.extensions.getSharedPrefs
import com.simplemobiletools.keyboard.R import com.simplemobiletools.keyboard.R
import com.simplemobiletools.keyboard.extensions.config import com.simplemobiletools.keyboard.extensions.config
@ -90,6 +101,38 @@ class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, Shared
keyboardView?.invalidateAllKeys() 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) { override fun onKey(code: Int) {
val inputConnection = currentInputConnection val inputConnection = currentInputConnection
if (keyboard == null || inputConnection == null) { if (keyboard == null || inputConnection == null) {
@ -201,6 +244,7 @@ class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, Shared
inputConnection.commitText(codeChar.toString(), 1) inputConnection.commitText(codeChar.toString(), 1)
} }
} }
else -> { else -> {
inputConnection.commitText(codeChar.toString(), 1) inputConnection.commitText(codeChar.toString(), 1)
if (originalText == null) { 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?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
keyboardView?.setupKeyboard() keyboardView?.setupKeyboard()
} }

View File

@ -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
}
}
}
}

View File

@ -10,6 +10,7 @@ import android.content.Intent
import android.graphics.* import android.graphics.*
import android.graphics.Paint.Align import android.graphics.Paint.Align
import android.graphics.drawable.* import android.graphics.drawable.*
import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.Message import android.os.Message
@ -18,11 +19,14 @@ import android.util.TypedValue
import android.view.* import android.view.*
import android.view.animation.AccelerateInterpolator import android.view.animation.AccelerateInterpolator
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.LinearLayout
import android.widget.PopupWindow import android.widget.PopupWindow
import android.widget.TextView import android.widget.TextView
import android.widget.inline.InlineContentView
import androidx.annotation.RequiresApi
import androidx.core.animation.doOnEnd import androidx.core.animation.doOnEnd
import androidx.core.animation.doOnStart import androidx.core.animation.doOnStart
import androidx.core.view.ViewCompat import androidx.core.view.*
import androidx.emoji2.text.EmojiCompat import androidx.emoji2.text.EmojiCompat
import androidx.emoji2.text.EmojiCompat.EMOJI_SUPPORTED import androidx.emoji2.text.EmojiCompat.EMOJI_SUPPORTED
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
@ -746,8 +750,8 @@ class MyKeyboardView @JvmOverloads constructor(context: Context, attrs: Attribut
private fun hideClipboardViews() { private fun hideClipboardViews() {
mToolbarHolder?.apply { mToolbarHolder?.apply {
clipboard_value_holder?.beGone() clipboard_value?.beGone()
clipboard_value_holder?.alpha = 0f clipboard_value?.alpha = 0f
clipboard_clear?.beGone() clipboard_clear?.beGone()
clipboard_clear?.alpha = 0f clipboard_clear?.alpha = 0f
} }
@ -764,10 +768,10 @@ class MyKeyboardView @JvmOverloads constructor(context: Context, attrs: Attribut
} }
private fun toggleClipboardVisibility(show: Boolean) { 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 newAlpha = if (show) 1f else 0f
val animations = ArrayList<ObjectAnimator>() val animations = ArrayList<ObjectAnimator>()
val clipboardValueAnimation = ObjectAnimator.ofFloat(mToolbarHolder!!.clipboard_value_holder!!, "alpha", newAlpha) val clipboardValueAnimation = ObjectAnimator.ofFloat(mToolbarHolder!!.clipboard_value!!, "alpha", newAlpha)
animations.add(clipboardValueAnimation) animations.add(clipboardValueAnimation)
val clipboardClearAnimation = ObjectAnimator.ofFloat(mToolbarHolder!!.clipboard_clear!!, "alpha", newAlpha) 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.interpolator = AccelerateInterpolator()
animSet.doOnStart { animSet.doOnStart {
if (show) { if (show) {
mToolbarHolder?.clipboard_value_holder?.beVisible() mToolbarHolder?.clipboard_value?.beVisible()
mToolbarHolder?.clipboard_clear?.beVisible() mToolbarHolder?.clipboard_clear?.beVisible()
} }
} }
animSet.doOnEnd { animSet.doOnEnd {
if (!show) { if (!show) {
mToolbarHolder?.clipboard_value_holder?.beGone() mToolbarHolder?.clipboard_value?.beGone()
mToolbarHolder?.clipboard_clear?.beGone() mToolbarHolder?.clipboard_clear?.beGone()
} }
} }
@ -1381,10 +1385,12 @@ class MyKeyboardView @JvmOverloads constructor(context: Context, attrs: Attribut
fun closeClipboardManager() { fun closeClipboardManager() {
mClipboardManagerHolder?.clipboard_manager_holder?.beGone() mClipboardManagerHolder?.clipboard_manager_holder?.beGone()
mToolbarHolder?.suggestions_holder?.showAllInlineContentViews()
} }
private fun openClipboardManager() { private fun openClipboardManager() {
mClipboardManagerHolder!!.clipboard_manager_holder.beVisible() mClipboardManagerHolder!!.clipboard_manager_holder.beVisible()
mToolbarHolder?.suggestions_holder?.hideAllInlineContentViews()
setupStoredClips() setupStoredClips()
} }
@ -1614,4 +1620,25 @@ class MyKeyboardView @JvmOverloads constructor(context: Context, attrs: Attribut
} }
return keyColor 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()
}
}
} }

View File

@ -49,13 +49,13 @@
<com.simplemobiletools.commons.views.MyEditText <com.simplemobiletools.commons.views.MyEditText
android:id="@+id/text_edittext" android:id="@+id/text_edittext"
android:inputType="textCapSentences"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/activity_margin" android:layout_marginStart="@dimen/activity_margin"
android:layout_marginTop="@dimen/activity_margin" android:layout_marginTop="@dimen/activity_margin"
android:layout_marginEnd="@dimen/activity_margin" android:layout_marginEnd="@dimen/activity_margin"
android:layout_marginBottom="@dimen/activity_margin" /> android:layout_marginBottom="@dimen/activity_margin"
android:inputType="textCapSentences" />
<com.simplemobiletools.commons.views.MyEditText <com.simplemobiletools.commons.views.MyEditText
android:id="@+id/text_editphone" android:id="@+id/text_editphone"
@ -67,6 +67,26 @@
android:layout_marginBottom="@dimen/activity_margin" android:layout_marginBottom="@dimen/activity_margin"
android:inputType="phone" /> android:inputType="phone" />
<com.simplemobiletools.commons.views.MyEditText
android:id="@+id/text_editemail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/activity_margin"
android:layout_marginTop="@dimen/activity_margin"
android:layout_marginEnd="@dimen/activity_margin"
android:layout_marginBottom="@dimen/activity_margin"
android:inputType="textEmailAddress" />
<com.simplemobiletools.commons.views.MyEditText
android:id="@+id/text_editpassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/activity_margin"
android:layout_marginTop="@dimen/activity_margin"
android:layout_marginEnd="@dimen/activity_margin"
android:layout_marginBottom="@dimen/activity_margin"
android:inputType="textPassword" />
</LinearLayout> </LinearLayout>
</RelativeLayout> </RelativeLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>

View File

@ -23,7 +23,7 @@
android:scrollbars="none"> android:scrollbars="none">
<RelativeLayout <RelativeLayout
android:id="@+id/clipboard_items_holder" android:id="@+id/suggestions_items_holder"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">

View File

@ -27,8 +27,8 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<RelativeLayout <com.simplemobiletools.keyboard.views.InlineContentViewHorizontalScrollView
android:id="@+id/clipboard_value_holder" android:id="@+id/suggestions_holder"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/medium_margin" android:layout_marginStart="@dimen/medium_margin"
@ -38,24 +38,41 @@
app:layout_constraintStart_toEndOf="@+id/clipboard_clear" app:layout_constraintStart_toEndOf="@+id/clipboard_clear"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<TextView <LinearLayout
android:id="@+id/clipboard_value" android:id="@+id/suggestions_items_holder"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_centerInParent="true" android:orientation="horizontal">
android:autoLink="none"
android:background="@drawable/clipboard_background"
android:ellipsize="end"
android:gravity="center"
android:lines="1"
android:paddingStart="@dimen/activity_margin"
android:paddingTop="@dimen/small_margin"
android:paddingEnd="@dimen/activity_margin"
android:paddingBottom="@dimen/small_margin"
android:textSize="@dimen/label_text_size"
tools:text="Clipboard content" />
</RelativeLayout> <TextView
android:id="@+id/clipboard_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="none"
android:background="@drawable/clipboard_background"
android:ellipsize="end"
android:gravity="center"
android:lines="1"
android:maxWidth="@dimen/suggestion_max_width"
android:minWidth="@dimen/suggestion_min_width"
android:paddingStart="@dimen/activity_margin"
android:paddingTop="@dimen/small_margin"
android:paddingEnd="@dimen/activity_margin"
android:paddingBottom="@dimen/small_margin"
android:textSize="@dimen/label_text_size"
app:drawableStartCompat="@drawable/ic_clipboard_vector"
tools:text="Clipboard content" />
<LinearLayout
android:id="@+id/autofill_suggestions_holder"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal" />
</LinearLayout>
</com.simplemobiletools.keyboard.views.InlineContentViewHorizontalScrollView>
<ImageView <ImageView
android:id="@+id/pinned_clipboard_items" android:id="@+id/pinned_clipboard_items"

View File

@ -11,6 +11,8 @@
<dimen name="emoji_item_size">46dp</dimen> <dimen name="emoji_item_size">46dp</dimen>
<dimen name="emoji_top_bar_elevation">4dp</dimen> <dimen name="emoji_top_bar_elevation">4dp</dimen>
<dimen name="emoji_palette_btn_size">42dp</dimen> <dimen name="emoji_palette_btn_size">42dp</dimen>
<dimen name="suggestion_max_width">200dp</dimen>
<dimen name="suggestion_min_width">100dp</dimen>
<dimen name="keyboard_text_size">22sp</dimen> <dimen name="keyboard_text_size">22sp</dimen>
<dimen name="preview_text_size">26sp</dimen> <dimen name="preview_text_size">26sp</dimen>

View File

@ -1,7 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<input-method xmlns:android="http://schemas.android.com/apk/res/android" <input-method xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:settingsActivity="com.simplemobiletools.keyboard.activities.SettingsActivity"> android:settingsActivity="com.simplemobiletools.keyboard.activities.SettingsActivity"
android:supportsInlineSuggestions="true"
tools:targetApi="r">
<subtype android:imeSubtypeMode="Keyboard" /> <subtype android:imeSubtypeMode="Keyboard" />