[go: nahoru, domu]

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