Refactors androidx.compose.ui:ui-text to the androidx.compose.ui.text package name
Bug: b/160233169
Test: ./gradlew checkApi
Relnote: N/A
Change-Id: Ide9b7d12c9f46dac717a602592bd168312253ce6
diff --git a/ui/ui-core/src/androidMain/kotlin/androidx/compose/ui/text/input/InputState.kt b/ui/ui-core/src/androidMain/kotlin/androidx/compose/ui/text/input/InputState.kt
new file mode 100644
index 0000000..e8fc13b
--- /dev/null
+++ b/ui/ui-core/src/androidMain/kotlin/androidx/compose/ui/text/input/InputState.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.text.input
+
+import android.view.inputmethod.ExtractedText
+
+internal fun TextFieldValue.toExtractedText(): ExtractedText {
+ val res = ExtractedText()
+ res.text = text
+ res.partialEndOffset = text.length
+ res.partialStartOffset = -1 // -1 means full text
+ res.selectionStart = selection.min
+ res.selectionEnd = selection.max
+ res.flags = if ('\n' in text) 0 else ExtractedText.FLAG_SINGLE_LINE
+ return res
+}
diff --git a/ui/ui-core/src/androidMain/kotlin/androidx/compose/ui/text/input/RecordingInputConnection.kt b/ui/ui-core/src/androidMain/kotlin/androidx/compose/ui/text/input/RecordingInputConnection.kt
new file mode 100644
index 0000000..eebdad6
--- /dev/null
+++ b/ui/ui-core/src/androidMain/kotlin/androidx/compose/ui/text/input/RecordingInputConnection.kt
@@ -0,0 +1,336 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.text.input
+
+import android.os.Bundle
+import android.os.Handler
+import android.text.TextUtils
+import android.util.Log
+import android.view.KeyEvent
+import android.view.View
+import android.view.inputmethod.CompletionInfo
+import android.view.inputmethod.CorrectionInfo
+import android.view.inputmethod.EditorInfo
+import android.view.inputmethod.ExtractedText
+import android.view.inputmethod.ExtractedTextRequest
+import android.view.inputmethod.InputConnection
+import android.view.inputmethod.InputContentInfo
+import android.view.inputmethod.InputMethodManager
+import androidx.annotation.VisibleForTesting
+
+private val DEBUG = false
+private val TAG = "RecordingIC"
+
+internal class RecordingInputConnection(
+ /**
+ * The initial input state
+ */
+ initState: TextFieldValue,
+
+ /**
+ * An input event listener.
+ */
+ val eventListener: InputEventListener
+) : InputConnection {
+
+ // The depth of the batch session. 0 means no session.
+ private var batchDepth: Int = 0
+
+ // The input state.
+ @VisibleForTesting
+ internal var mTextFieldValue: TextFieldValue = initState
+ set(value) {
+ if (DEBUG) { Log.d(TAG, "New InputState has set: $value -> $mTextFieldValue") }
+ field = value
+ }
+
+ /**
+ * The token to be used for reporting updateExtractedText API.
+ *
+ * 0 if no token was specified from IME.
+ */
+ private var currentExtractedTextRequestToken = 0
+
+ /**
+ * True if IME requested extracted text monitor mode.
+ *
+ * If extracted text monitor mode is ON, need to call updateExtractedText API whenever the text
+ * is changed.
+ */
+ private var extractedTextMonitorMode = false
+
+ /**
+ * Updates the input state and tells it to the IME.
+ *
+ * This function may emits updateSelection and updateExtractedText to notify IMEs that the text
+ * contents has changed if needed.
+ */
+ fun updateInputState(state: TextFieldValue, imm: InputMethodManager, view: View) {
+ val prev = mTextFieldValue
+ val next = state
+ mTextFieldValue = next
+
+ if (prev == next) {
+ return
+ }
+
+ if (extractedTextMonitorMode) {
+ imm.updateExtractedText(view, currentExtractedTextRequestToken, next.toExtractedText())
+ }
+
+ // The candidateStart and candidateEnd is composition start and composition end in
+ // updateSelection API. Need to pass -1 if there is no composition.
+ val candidateStart = next.composition?.min ?: -1
+ val candidateEnd = next.composition?.max ?: -1
+ if (DEBUG) {
+ Log.d(
+ TAG, "updateSelection(" +
+ "selection = (${next.selection.min},${next.selection.max}), " +
+ "compoairion = ($candidateStart, $candidateEnd)")
+ }
+ imm.updateSelection(view, next.selection.min, next.selection.max,
+ candidateStart, candidateEnd)
+ }
+
+ // The recoding editing ops.
+ private val editOps = mutableListOf<EditOperation>()
+
+ // Add edit op to internal list with wrapping batch edit.
+ private fun addEditOpWithBatch(editOp: EditOperation) {
+ beginBatchEdit()
+ try {
+ editOps.add(editOp)
+ } finally {
+ endBatchEdit()
+ }
+ }
+
+ // /////////////////////////////////////////////////////////////////////////////////////////////
+ // Callbacks for text editing session
+ // /////////////////////////////////////////////////////////////////////////////////////////////
+
+ override fun beginBatchEdit(): Boolean {
+ if (DEBUG) { Log.d(TAG, "beginBatchEdit()") }
+ batchDepth++
+ return true
+ }
+
+ override fun endBatchEdit(): Boolean {
+ if (DEBUG) { Log.d(TAG, "endBatchEdit()") }
+ batchDepth--
+ if (batchDepth == 0 && editOps.isNotEmpty()) {
+ eventListener.onEditOperations(editOps.toList())
+ editOps.clear()
+ }
+ return batchDepth > 0
+ }
+
+ override fun closeConnection() {
+ if (DEBUG) { Log.d(TAG, "closeConnection()") }
+ editOps.clear()
+ batchDepth = 0
+ }
+
+ // /////////////////////////////////////////////////////////////////////////////////////////////
+ // Callbacks for text editing
+ // /////////////////////////////////////////////////////////////////////////////////////////////
+
+ override fun commitText(text: CharSequence?, newCursorPosition: Int): Boolean {
+ if (DEBUG) { Log.d(TAG, "commitText(\"$text\", $newCursorPosition)") }
+ addEditOpWithBatch(CommitTextEditOp(text.toString(), newCursorPosition))
+ return true
+ }
+
+ override fun setComposingRegion(start: Int, end: Int): Boolean {
+ if (DEBUG) { Log.d(TAG, "setComposingRegion($start, $end)") }
+ addEditOpWithBatch(SetComposingRegionEditOp(start, end))
+ return true
+ }
+
+ override fun setComposingText(text: CharSequence?, newCursorPosition: Int): Boolean {
+ if (DEBUG) { Log.d(TAG, "setComposingText(\"$text\", $newCursorPosition)") }
+ addEditOpWithBatch(SetComposingTextEditOp(text.toString(), newCursorPosition))
+ return true
+ }
+
+ override fun deleteSurroundingTextInCodePoints(beforeLength: Int, afterLength: Int): Boolean {
+ if (DEBUG) { Log.d(TAG, "deleteSurroundingTextInCodePoints($beforeLength, $afterLength)") }
+ addEditOpWithBatch(DeleteSurroundingTextInCodePointsEditOp(beforeLength, afterLength))
+ return true
+ }
+
+ override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
+ if (DEBUG) { Log.d(TAG, "deleteSurroundingText($beforeLength, $afterLength)") }
+ addEditOpWithBatch(DeleteSurroundingTextEditOp(beforeLength, afterLength))
+ return true
+ }
+
+ override fun setSelection(start: Int, end: Int): Boolean {
+ if (DEBUG) { Log.d(TAG, "setSelection($start, $end)") }
+ addEditOpWithBatch(SetSelectionEditOp(start, end))
+ return true
+ }
+
+ override fun finishComposingText(): Boolean {
+ if (DEBUG) { Log.d(TAG, "finishComposingText()") }
+ addEditOpWithBatch(FinishComposingTextEditOp())
+ return true
+ }
+
+ override fun sendKeyEvent(event: KeyEvent): Boolean {
+ if (DEBUG) { Log.d(TAG, "sendKeyEvent($event)") }
+ if (event.action != KeyEvent.ACTION_DOWN) {
+ return true // Only interested in KEY_DOWN event.
+ }
+
+ val op = when (event.keyCode) {
+ KeyEvent.KEYCODE_DEL -> BackspaceKeyEditOp()
+ KeyEvent.KEYCODE_DPAD_LEFT -> MoveCursorEditOp(-1)
+ KeyEvent.KEYCODE_DPAD_RIGHT -> MoveCursorEditOp(1)
+ else -> CommitTextEditOp(String(Character.toChars(event.getUnicodeChar())), 1)
+ }
+
+ addEditOpWithBatch(op)
+ return true
+ }
+
+ // /////////////////////////////////////////////////////////////////////////////////////////////
+ // Callbacks for retrieving editing buffers
+ // /////////////////////////////////////////////////////////////////////////////////////////////
+
+ override fun getTextBeforeCursor(maxChars: Int, flags: Int): CharSequence {
+ if (DEBUG) { Log.d(TAG, "getTextBeforeCursor($maxChars, $flags)") }
+ return mTextFieldValue.getTextBeforeSelection(maxChars)
+ }
+
+ override fun getTextAfterCursor(maxChars: Int, flags: Int): CharSequence {
+ if (DEBUG) { Log.d(TAG, "getTextAfterCursor($maxChars, $flags)") }
+ return mTextFieldValue.getTextAfterSelection(maxChars)
+ }
+
+ override fun getSelectedText(flags: Int): CharSequence {
+ if (DEBUG) { Log.d(TAG, "getSelectedText($flags)") }
+ return mTextFieldValue.getSelectedText()
+ }
+
+ override fun requestCursorUpdates(cursorUpdateMode: Int): Boolean {
+ if (DEBUG) { Log.d(TAG, "requestCursorUpdates($cursorUpdateMode)") }
+ Log.w(TAG, "requestCursorUpdates is not supported")
+ return false
+ }
+
+ override fun getExtractedText(request: ExtractedTextRequest?, flags: Int): ExtractedText {
+ if (DEBUG) { Log.d(TAG, "getExtractedText($request, $flags)") }
+ extractedTextMonitorMode = (flags and InputConnection.GET_EXTRACTED_TEXT_MONITOR) != 0
+ if (extractedTextMonitorMode) {
+ currentExtractedTextRequestToken = request?.token ?: 0
+ }
+ return mTextFieldValue.toExtractedText()
+ }
+
+ // /////////////////////////////////////////////////////////////////////////////////////////////
+ // Editor action and Key events.
+ // /////////////////////////////////////////////////////////////////////////////////////////////
+
+ override fun performContextMenuAction(id: Int): Boolean {
+ if (DEBUG) { Log.d(TAG, "performContextMenuAction($id)") }
+ Log.w(TAG, "performContextMenuAction is not supported")
+ return false
+ }
+
+ override fun performEditorAction(editorAction: Int): Boolean {
+ if (DEBUG) { Log.d(TAG, "performEditorAction($editorAction)") }
+ val imeAction = when (editorAction) {
+ EditorInfo.IME_ACTION_UNSPECIFIED -> ImeAction.Unspecified
+ EditorInfo.IME_ACTION_DONE -> ImeAction.Done
+ EditorInfo.IME_ACTION_SEND -> ImeAction.Send
+ EditorInfo.IME_ACTION_SEARCH -> ImeAction.Search
+ EditorInfo.IME_ACTION_PREVIOUS -> ImeAction.Previous
+ EditorInfo.IME_ACTION_NEXT -> ImeAction.Next
+ EditorInfo.IME_ACTION_GO -> ImeAction.Go
+ else -> {
+ Log.w(TAG, "IME sends unsupported Editor Action: $editorAction")
+ ImeAction.Unspecified
+ }
+ }
+ eventListener.onImeAction(imeAction)
+ return true
+ }
+
+ // /////////////////////////////////////////////////////////////////////////////////////////////
+ // Unsupported callbacks
+ // /////////////////////////////////////////////////////////////////////////////////////////////
+
+ override fun commitCompletion(text: CompletionInfo?): Boolean {
+ if (DEBUG) { Log.d(TAG, "commitCompletion(${text?.text})") }
+ // We don't support this callback.
+ // The API documents says this should return if the input connection is no longer valid, but
+ // The Chromium implementation already returning false, so assuming it is safe to return
+ // false if not supported.
+ // see https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnection.java
+ return false
+ }
+
+ override fun commitCorrection(correctionInfo: CorrectionInfo?): Boolean {
+ if (DEBUG) { Log.d(TAG, "commitCorrection($correctionInfo)") }
+ // We don't support this callback.
+ // The API documents says this should return if the input connection is no longer valid, but
+ // The Chromium implementation already returning false, so assuming it is safe to return
+ // false if not supported.
+ // see https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnection.java
+ return false
+ }
+
+ override fun getHandler(): Handler? {
+ if (DEBUG) { Log.d(TAG, "getHandler()") }
+ return null // Returns null means using default Handler
+ }
+
+ override fun clearMetaKeyStates(states: Int): Boolean {
+ if (DEBUG) { Log.d(TAG, "clearMetaKeyStates($states)") }
+ // We don't support this callback.
+ // The API documents says this should return if the input connection is no longer valid, but
+ // The Chromium implementation already returning false, so assuming it is safe to return
+ // false if not supported.
+ // see https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnection.java
+ return false
+ }
+
+ override fun reportFullscreenMode(enabled: Boolean): Boolean {
+ if (DEBUG) { Log.d(TAG, "reportFullscreenMode($enabled)") }
+ return false // This value is ignored according to the API docs.
+ }
+
+ override fun getCursorCapsMode(reqModes: Int): Int {
+ if (DEBUG) { Log.d(TAG, "getCursorCapsMode($reqModes)") }
+ return TextUtils.getCapsMode(mTextFieldValue.text, mTextFieldValue.selection.min, reqModes)
+ }
+
+ override fun performPrivateCommand(action: String?, data: Bundle?): Boolean {
+ if (DEBUG) { Log.d(TAG, "performPrivateCommand($action, $data)") }
+ return true // API doc says we should return true even if we didn't understand the command.
+ }
+
+ override fun commitContent(
+ inputContentInfo: InputContentInfo,
+ flags: Int,
+ opts: Bundle?
+ ): Boolean {
+ if (DEBUG) { Log.d(TAG, "commitContent($inputContentInfo, $flags, $opts)") }
+ return false // We don't accept any contents.
+ }
+}
\ No newline at end of file
diff --git a/ui/ui-core/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.kt b/ui/ui-core/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.kt
new file mode 100644
index 0000000..172194a
--- /dev/null
+++ b/ui/ui-core/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.kt
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.text.input
+
+import android.content.Context
+import android.text.InputType
+import android.view.View
+import android.view.ViewTreeObserver
+import android.view.inputmethod.EditorInfo
+import android.view.inputmethod.InputConnection
+import android.view.inputmethod.InputMethodManager
+import androidx.ui.geometry.Rect
+import androidx.compose.ui.text.TextRange
+import kotlin.math.roundToInt
+
+/**
+ * Provide Android specific input service with the Operating System.
+ */
+internal class TextInputServiceAndroid(val view: View) : PlatformTextInputService {
+ /** True if the currently editable composable has connected */
+ private var editorHasFocus = false
+
+ /**
+ * The following three observers are set when the editable composable has initiated the input
+ * session
+ */
+ private var onEditCommand: (List<EditOperation>) -> Unit = {}
+ private var onImeActionPerformed: (ImeAction) -> Unit = {}
+
+ private var state = TextFieldValue(text = "", selection = TextRange.Zero)
+ private var keyboardType = KeyboardType.Text
+ private var imeAction = ImeAction.Unspecified
+ private var ic: RecordingInputConnection? = null
+
+ private var focusedRect: android.graphics.Rect? = null
+
+ /**
+ * The editable buffer used for BaseInputConnection.
+ */
+ private lateinit var imm: InputMethodManager
+
+ private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
+ // focusedRect is null if there is not ongoing text input session. So safe to request
+ // latest focused rectangle whenever global layout has changed.
+ focusedRect?.let { view.requestRectangleOnScreen(it) }
+ }
+
+ init {
+ view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
+ override fun onViewDetachedFromWindow(v: View?) {
+ v?.rootView?.viewTreeObserver?.removeOnGlobalLayoutListener(layoutListener)
+ }
+
+ override fun onViewAttachedToWindow(v: View?) {
+ v?.rootView?.viewTreeObserver?.addOnGlobalLayoutListener(layoutListener)
+ }
+ })
+ }
+
+ /**
+ * Creates new input connection.
+ */
+ fun createInputConnection(outAttrs: EditorInfo): InputConnection? {
+ if (!editorHasFocus) {
+ return null
+ }
+ fillEditorInfo(keyboardType, imeAction, outAttrs)
+
+ return RecordingInputConnection(
+ initState = state,
+ eventListener = object : InputEventListener {
+ override fun onEditOperations(editOps: List<EditOperation>) {
+ onEditCommand(editOps)
+ }
+
+ override fun onImeAction(imeAction: ImeAction) {
+ onImeActionPerformed(imeAction)
+ }
+ }
+ ).also { ic = it }
+ }
+
+ /**
+ * Returns true if some editable component is focused.
+ */
+ fun isEditorFocused(): Boolean = editorHasFocus
+
+ override fun startInput(
+ value: TextFieldValue,
+ keyboardType: KeyboardType,
+ imeAction: ImeAction,
+ onEditCommand: (List<EditOperation>) -> Unit,
+ onImeActionPerformed: (ImeAction) -> Unit
+ ) {
+ imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ editorHasFocus = true
+ state = value
+ this.keyboardType = keyboardType
+ this.imeAction = imeAction
+ this.>
+ this.>
+
+ view.requestFocus()
+ view.post {
+ imm.restartInput(view)
+ imm.showSoftInput(view, 0)
+ }
+ }
+
+ override fun stopInput() {
+ editorHasFocus = false
+ >
+ >
+ focusedRect = null
+
+ imm.restartInput(view)
+ editorHasFocus = false
+ }
+
+ override fun showSoftwareKeyboard() {
+ imm.showSoftInput(view, 0)
+ }
+
+ override fun hideSoftwareKeyboard() {
+ imm.hideSoftInputFromWindow(view.windowToken, 0)
+ }
+
+ override fun onStateUpdated(value: TextFieldValue) {
+ this.state = value
+ ic?.updateInputState(this.state, imm, view)
+ }
+
+ override fun notifyFocusedRect(rect: Rect) {
+ focusedRect = android.graphics.Rect(
+ rect.left.roundToInt(),
+ rect.top.roundToInt(),
+ rect.right.roundToInt(),
+ rect.bottom.roundToInt()
+ )
+
+ // Requesting rectangle too early after obtaining focus may bring view into wrong place
+ // probably due to transient IME inset change. We don't know the correct timing of calling
+ // requestRectangleOnScreen API, so try to call this API only after the IME is ready to
+ // use, i.e. InputConnection has created.
+ // Even if we miss all the timing of requesting rectangle during initial text field focus,
+ // focused rectangle will be requested when software keyboard has shown.
+ if (ic == null) {
+ view.requestRectangleOnScreen(focusedRect)
+ }
+ }
+
+ /**
+ * Fills necessary info of EditorInfo.
+ */
+ private fun fillEditorInfo(
+ keyboardType: KeyboardType,
+ imeAction: ImeAction,
+ outInfo: EditorInfo
+ ) {
+ outInfo.imeOptions = when (imeAction) {
+ ImeAction.Unspecified -> EditorInfo.IME_ACTION_UNSPECIFIED
+ ImeAction.NoAction -> EditorInfo.IME_ACTION_NONE
+ ImeAction.Go -> EditorInfo.IME_ACTION_GO
+ ImeAction.Next -> EditorInfo.IME_ACTION_NEXT
+ ImeAction.Previous -> EditorInfo.IME_ACTION_PREVIOUS
+ ImeAction.Search -> EditorInfo.IME_ACTION_SEARCH
+ ImeAction.Send -> EditorInfo.IME_ACTION_SEND
+ ImeAction.Done -> EditorInfo.IME_ACTION_DONE
+ else -> throw IllegalArgumentException("Unknown ImeAction: $imeAction")
+ }
+ when (keyboardType) {
+ KeyboardType.Text -> outInfo.inputType = InputType.TYPE_CLASS_TEXT
+ KeyboardType.Ascii -> {
+ outInfo.inputType = InputType.TYPE_CLASS_TEXT
+ outInfo.imeOptions = outInfo.imeOptions or EditorInfo.IME_FLAG_FORCE_ASCII
+ }
+ KeyboardType.Number -> outInfo.inputType = InputType.TYPE_CLASS_NUMBER
+ KeyboardType.Phone -> outInfo.inputType = InputType.TYPE_CLASS_PHONE
+ KeyboardType.Uri ->
+ outInfo.inputType = InputType.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_URI
+ KeyboardType.Email ->
+ outInfo.inputType =
+ InputType.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
+ KeyboardType.Password -> {
+ outInfo.inputType =
+ InputType.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
+ }
+ KeyboardType.NumberPassword -> {
+ outInfo.inputType =
+ InputType.TYPE_CLASS_NUMBER or EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD
+ }
+ else -> throw IllegalArgumentException("Unknown KeyboardType: $keyboardType")
+ }
+ outInfo.imeOptions =
+ outInfo.imeOptions or outInfo.imeOptions or EditorInfo.IME_FLAG_NO_FULLSCREEN
+ }
+}
\ No newline at end of file