1/*
2 * Copyright (C) 2009 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.inputmethod.voice;
18
19import android.view.inputmethod.ExtractedText;
20import android.view.inputmethod.ExtractedTextRequest;
21import android.view.inputmethod.InputConnection;
22
23/**
24 * Utility methods to deal with editing text through an InputConnection.
25 */
26public class EditingUtil {
27    private EditingUtil() {};
28
29    /**
30     * Append newText to the text field represented by connection.
31     * The new text becomes selected.
32     */
33    public static void appendText(InputConnection connection, String newText) {
34        if (connection == null) {
35            return;
36        }
37
38        // Commit the composing text
39        connection.finishComposingText();
40
41        // Add a space if the field already has text.
42        CharSequence charBeforeCursor = connection.getTextBeforeCursor(1, 0);
43        if (charBeforeCursor != null
44                && !charBeforeCursor.equals(" ")
45                && (charBeforeCursor.length() > 0)) {
46            newText = " " + newText;
47        }
48
49        connection.setComposingText(newText, 1);
50    }
51
52    private static int getCursorPosition(InputConnection connection) {
53        ExtractedText extracted = connection.getExtractedText(
54            new ExtractedTextRequest(), 0);
55        if (extracted == null) {
56          return -1;
57        }
58        return extracted.startOffset + extracted.selectionStart;
59    }
60
61    private static int getSelectionEnd(InputConnection connection) {
62        ExtractedText extracted = connection.getExtractedText(
63            new ExtractedTextRequest(), 0);
64        if (extracted == null) {
65          return -1;
66        }
67        return extracted.startOffset + extracted.selectionEnd;
68    }
69
70    /**
71     * @param connection connection to the current text field.
72     * @param sep characters which may separate words
73     * @return the word that surrounds the cursor, including up to one trailing
74     *   separator. For example, if the field contains "he|llo world", where |
75     *   represents the cursor, then "hello " will be returned.
76     */
77    public static String getWordAtCursor(
78        InputConnection connection, String separators) {
79        Range range = getWordRangeAtCursor(connection, separators);
80        return (range == null) ? null : range.word;
81    }
82
83    /**
84     * Removes the word surrounding the cursor. Parameters are identical to
85     * getWordAtCursor.
86     */
87    public static void deleteWordAtCursor(
88        InputConnection connection, String separators) {
89
90        Range range = getWordRangeAtCursor(connection, separators);
91        if (range == null) return;
92
93        connection.finishComposingText();
94        // Move cursor to beginning of word, to avoid crash when cursor is outside
95        // of valid range after deleting text.
96        int newCursor = getCursorPosition(connection) - range.charsBefore;
97        connection.setSelection(newCursor, newCursor);
98        connection.deleteSurroundingText(0, range.charsBefore + range.charsAfter);
99    }
100
101    /**
102     * Represents a range of text, relative to the current cursor position.
103     */
104    private static class Range {
105        /** Characters before selection start */
106        int charsBefore;
107
108        /**
109         * Characters after selection start, including one trailing word
110         * separator.
111         */
112        int charsAfter;
113
114        /** The actual characters that make up a word */
115        String word;
116
117        public Range(int charsBefore, int charsAfter, String word) {
118            if (charsBefore < 0 || charsAfter < 0) {
119                throw new IndexOutOfBoundsException();
120            }
121            this.charsBefore = charsBefore;
122            this.charsAfter = charsAfter;
123            this.word = word;
124        }
125    }
126
127    private static Range getWordRangeAtCursor(
128        InputConnection connection, String sep) {
129        if (connection == null || sep == null) {
130            return null;
131        }
132        CharSequence before = connection.getTextBeforeCursor(1000, 0);
133        CharSequence after = connection.getTextAfterCursor(1000, 0);
134        if (before == null || after == null) {
135            return null;
136        }
137
138        // Find first word separator before the cursor
139        int start = before.length();
140        while (--start > 0 && !isWhitespace(before.charAt(start - 1), sep));
141
142        // Find last word separator after the cursor
143        int end = -1;
144        while (++end < after.length() && !isWhitespace(after.charAt(end), sep));
145        if (end < after.length() - 1) {
146            end++; // Include trailing space, if it exists, in word
147        }
148
149        int cursor = getCursorPosition(connection);
150        if (start >= 0 && cursor + end <= after.length() + before.length()) {
151            String word = before.toString().substring(start, before.length())
152                + after.toString().substring(0, end);
153            return new Range(before.length() - start, end, word);
154        }
155
156        return null;
157    }
158
159    private static boolean isWhitespace(int code, String whitespace) {
160        return whitespace.contains(String.valueOf((char) code));
161    }
162}
163