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