TextDecorator.java revision de12c1bf49efb6ac9b7127933eebb08956488ace
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.inputmethod.keyboard;
18
19import android.graphics.Matrix;
20import android.graphics.RectF;
21import android.inputmethodservice.InputMethodService;
22import android.os.Message;
23import android.text.TextUtils;
24import android.util.Log;
25import android.view.View;
26import android.view.inputmethod.CursorAnchorInfo;
27
28import com.android.inputmethod.annotations.UsedForTesting;
29import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper;
30import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper;
31
32import javax.annotation.Nonnull;
33
34/**
35 * A controller class of the add-to-dictionary indicator (a.k.a. TextDecorator). This class
36 * is designed to be independent of UI subsystems such as {@link View}. All the UI related
37 * operations are delegated to {@link TextDecoratorUi} via {@link TextDecoratorUiOperator}.
38 */
39public class TextDecorator {
40    private static final String TAG = TextDecorator.class.getSimpleName();
41    private static final boolean DEBUG = false;
42
43    private static final int INVALID_CURSOR_INDEX = -1;
44
45    private static final int MODE_MONITOR = 0;
46    private static final int MODE_WAITING_CURSOR_INDEX = 1;
47    private static final int MODE_SHOWING_INDICATOR = 2;
48
49    private int mMode = MODE_MONITOR;
50
51    private String mLastComposingText = null;
52    private boolean mHasRtlCharsInLastComposingText = false;
53    private RectF mComposingTextBoundsForLastComposingText = new RectF();
54
55    private boolean mIsFullScreenMode = false;
56    private String mWaitingWord = null;
57    private int mWaitingCursorStart = INVALID_CURSOR_INDEX;
58    private int mWaitingCursorEnd = INVALID_CURSOR_INDEX;
59    private CursorAnchorInfoCompatWrapper mCursorAnchorInfoWrapper = null;
60
61    @Nonnull
62    private final Listener mListener;
63
64    @Nonnull
65    private TextDecoratorUiOperator mUiOperator = EMPTY_UI_OPERATOR;
66
67    public interface Listener {
68        /**
69         * Called when the user clicks the indicator to add the word into the dictionary.
70         * @param word the word which the user clicked on.
71         */
72        void onClickComposingTextToAddToDictionary(final String word);
73    }
74
75    public TextDecorator(final Listener listener) {
76        mListener = (listener != null) ? listener : EMPTY_LISTENER;
77    }
78
79    /**
80     * Sets the UI operator for {@link TextDecorator}. Any user visible operations will be
81     * delegated to the associated UI operator.
82     * @param uiOperator the UI operator to be associated.
83     */
84    public void setUiOperator(final TextDecoratorUiOperator uiOperator) {
85        mUiOperator.disposeUi();
86        mUiOperator = uiOperator;
87        mUiOperator.setOnClickListener(getOnClickHandler());
88    }
89
90    private final Runnable mDefaultOnClickHandler = new Runnable() {
91        @Override
92        public void run() {
93            onClickIndicator();
94        }
95    };
96
97    @UsedForTesting
98    final Runnable getOnClickHandler() {
99        return mDefaultOnClickHandler;
100    }
101
102    /**
103     * Shows the "Add to dictionary" indicator and associates it with associating the given word.
104     *
105     * @param word the word which should be associated with the indicator. This object will be
106     * passed back in {@link Listener#onClickComposingTextToAddToDictionary(String)}.
107     * @param selectionStart the cursor index (inclusive) when the indicator should be displayed.
108     * @param selectionEnd the cursor index (exclusive) when the indicator should be displayed.
109     */
110    public void showAddToDictionaryIndicator(final String word, final int selectionStart,
111            final int selectionEnd) {
112        mWaitingWord = word;
113        mWaitingCursorStart = selectionStart;
114        mWaitingCursorEnd = selectionEnd;
115        mMode = MODE_WAITING_CURSOR_INDEX;
116        layoutLater();
117        return;
118    }
119
120    /**
121     * Must be called when the input method is about changing to for from the full screen mode.
122     * @param fullScreenMode {@code true} if the input method is entering the full screen mode.
123     * {@code false} is the input method is finishing the full screen mode.
124     */
125    public void notifyFullScreenMode(final boolean fullScreenMode) {
126        final boolean fullScreenModeChanged = (mIsFullScreenMode != fullScreenMode);
127        mIsFullScreenMode = fullScreenMode;
128        if (fullScreenModeChanged) {
129            layoutLater();
130        }
131    }
132
133    /**
134     * Resets previous requests and makes indicator invisible.
135     */
136    public void reset() {
137        mWaitingWord = null;
138        mMode = MODE_MONITOR;
139        mWaitingCursorStart = INVALID_CURSOR_INDEX;
140        mWaitingCursorEnd = INVALID_CURSOR_INDEX;
141        cancelLayoutInternalExpectedly("Resetting internal state.");
142    }
143
144    /**
145     * Must be called when the {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)}
146     * is called.
147     *
148     * <p>CAVEAT: Currently the input method author is responsible for ignoring
149     * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} called in full screen
150     * mode.</p>
151     * @param info the compatibility wrapper object for the received {@link CursorAnchorInfo}.
152     */
153    public void onUpdateCursorAnchorInfo(final CursorAnchorInfoCompatWrapper info) {
154        mCursorAnchorInfoWrapper = info;
155        // Do not use layoutLater() to minimize the latency.
156        layoutImmediately();
157    }
158
159    private void cancelLayoutInternalUnexpectedly(final String message) {
160        mUiOperator.hideUi();
161        Log.d(TAG, message);
162    }
163
164    private void cancelLayoutInternalExpectedly(final String message) {
165        mUiOperator.hideUi();
166        if (DEBUG) {
167            Log.d(TAG, message);
168        }
169    }
170
171    private void layoutLater() {
172        mLayoutInvalidator.invalidateLayout();
173    }
174
175
176    private void layoutImmediately() {
177        // Clear pending layout requests.
178        mLayoutInvalidator.cancelInvalidateLayout();
179        layoutMain();
180    }
181
182    private void layoutMain() {
183        final CursorAnchorInfoCompatWrapper info = mCursorAnchorInfoWrapper;
184
185        if (info == null || !info.isAvailable()) {
186            cancelLayoutInternalExpectedly("CursorAnchorInfo isn't available.");
187            return;
188        }
189
190        final Matrix matrix = info.getMatrix();
191        if (matrix == null) {
192            cancelLayoutInternalUnexpectedly("Matrix is null");
193        }
194
195        final CharSequence composingText = info.getComposingText();
196        if (!TextUtils.isEmpty(composingText)) {
197            final int composingTextStart = info.getComposingTextStart();
198            final int lastCharRectIndex = composingTextStart + composingText.length() - 1;
199            final RectF lastCharRect = info.getCharacterBounds(lastCharRectIndex);
200            final int lastCharRectFlags = info.getCharacterBoundsFlags(lastCharRectIndex);
201            final boolean hasInvisibleRegionInLastCharRect =
202                    (lastCharRectFlags & CursorAnchorInfoCompatWrapper.FLAG_HAS_INVISIBLE_REGION)
203                            != 0;
204            if (lastCharRect == null || matrix == null || hasInvisibleRegionInLastCharRect) {
205                mUiOperator.hideUi();
206                return;
207            }
208
209            // Note that the following layout information is fragile, and must be invalidated
210            // even when surrounding text next to the composing text is changed because it can
211            // affect how the composing text is rendered.
212            // TODO: Investigate if we can change the input logic to make the target text
213            // composing state so that we can retrieve the character bounds reliably.
214            final String composingTextString = composingText.toString();
215            final float top = lastCharRect.top;
216            final float bottom = lastCharRect.bottom;
217            float left = lastCharRect.left;
218            float right = lastCharRect.right;
219            boolean useRtlLayout = false;
220            for (int i = composingText.length() - 1; i >= 0; --i) {
221                final int characterIndex = composingTextStart + i;
222                final RectF characterBounds = info.getCharacterBounds(characterIndex);
223                final int characterBoundsFlags = info.getCharacterBoundsFlags(characterIndex);
224                if (characterBounds == null) {
225                    break;
226                }
227                if (characterBounds.top != top) {
228                    break;
229                }
230                if (characterBounds.bottom != bottom) {
231                    break;
232                }
233                if ((characterBoundsFlags & CursorAnchorInfoCompatWrapper.FLAG_IS_RTL) != 0) {
234                    // This is for both RTL text and bi-directional text. RTL languages usually mix
235                    // RTL characters with LTR characters and in this case we should display the
236                    // indicator on the left, while in LTR languages that normally never happens.
237                    // TODO: Try to come up with a better algorithm.
238                    useRtlLayout = true;
239                }
240                left = Math.min(characterBounds.left, left);
241                right = Math.max(characterBounds.right, right);
242            }
243            mLastComposingText = composingTextString;
244            mHasRtlCharsInLastComposingText = useRtlLayout;
245            mComposingTextBoundsForLastComposingText.set(left, top, right, bottom);
246        }
247
248        final int selectionStart = info.getSelectionStart();
249        final int selectionEnd = info.getSelectionEnd();
250        switch (mMode) {
251            case MODE_MONITOR:
252                mUiOperator.hideUi();
253                return;
254            case MODE_WAITING_CURSOR_INDEX:
255                if (selectionStart != mWaitingCursorStart || selectionEnd != mWaitingCursorEnd) {
256                    mUiOperator.hideUi();
257                    return;
258                }
259                mMode = MODE_SHOWING_INDICATOR;
260                break;
261            case MODE_SHOWING_INDICATOR:
262                if (selectionStart != mWaitingCursorStart || selectionEnd != mWaitingCursorEnd) {
263                    mUiOperator.hideUi();
264                    mMode = MODE_MONITOR;
265                    mWaitingCursorStart = INVALID_CURSOR_INDEX;
266                    mWaitingCursorEnd = INVALID_CURSOR_INDEX;
267                    return;
268                }
269                break;
270            default:
271                cancelLayoutInternalUnexpectedly("Unexpected internal mode=" + mMode);
272                return;
273        }
274
275        if (!TextUtils.equals(mLastComposingText, mWaitingWord)) {
276            cancelLayoutInternalUnexpectedly("mLastComposingText doesn't match mWaitingWord");
277            return;
278        }
279
280        if ((info.getInsertionMarkerFlags() &
281                CursorAnchorInfoCompatWrapper.FLAG_HAS_INVISIBLE_REGION) != 0) {
282            mUiOperator.hideUi();
283            return;
284        }
285
286        mUiOperator.layoutUi(matrix, mComposingTextBoundsForLastComposingText,
287                mHasRtlCharsInLastComposingText);
288    }
289
290    private void onClickIndicator() {
291        if (mMode != MODE_SHOWING_INDICATOR) {
292            return;
293        }
294        mListener.onClickComposingTextToAddToDictionary(mWaitingWord);
295    }
296
297    private final LayoutInvalidator mLayoutInvalidator = new LayoutInvalidator(this);
298
299    /**
300     * Used for managing pending layout tasks for {@link TextDecorator#layoutLater()}.
301     */
302    private static final class LayoutInvalidator {
303        private final HandlerImpl mHandler;
304        public LayoutInvalidator(final TextDecorator ownerInstance) {
305            mHandler = new HandlerImpl(ownerInstance);
306        }
307
308        private static final int MSG_LAYOUT = 0;
309
310        private static final class HandlerImpl
311                extends LeakGuardHandlerWrapper<TextDecorator> {
312            public HandlerImpl(final TextDecorator ownerInstance) {
313                super(ownerInstance);
314            }
315
316            @Override
317            public void handleMessage(final Message msg) {
318                final TextDecorator owner = getOwnerInstance();
319                if (owner == null) {
320                    return;
321                }
322                switch (msg.what) {
323                    case MSG_LAYOUT:
324                        owner.layoutMain();
325                        break;
326                }
327            }
328        }
329
330        /**
331         * Puts a layout task into the scheduler. Does nothing if one or more layout tasks are
332         * already scheduled.
333         */
334        public void invalidateLayout() {
335            if (!mHandler.hasMessages(MSG_LAYOUT)) {
336                mHandler.obtainMessage(MSG_LAYOUT).sendToTarget();
337            }
338        }
339
340        /**
341         * Clears the pending layout tasks.
342         */
343        public void cancelInvalidateLayout() {
344            mHandler.removeMessages(MSG_LAYOUT);
345        }
346    }
347
348    private final static Listener EMPTY_LISTENER = new Listener() {
349        @Override
350        public void onClickComposingTextToAddToDictionary(final String word) {
351        }
352    };
353
354    private final static TextDecoratorUiOperator EMPTY_UI_OPERATOR = new TextDecoratorUiOperator() {
355        @Override
356        public void disposeUi() {
357        }
358        @Override
359        public void hideUi() {
360        }
361        @Override
362        public void setOnClickListener(Runnable listener) {
363        }
364        @Override
365        public void layoutUi(Matrix matrix, RectF composingTextBounds, boolean useRtlLayout) {
366        }
367    };
368}
369