GeckoEditableChild.java (14866B)
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 package org.mozilla.gecko; 7 8 import android.graphics.RectF; 9 import android.os.IBinder; 10 import android.os.RemoteException; 11 import android.util.Log; 12 import android.view.KeyEvent; 13 import androidx.annotation.Nullable; 14 import org.mozilla.gecko.annotation.WrapForJNI; 15 import org.mozilla.gecko.mozglue.JNIObject; 16 import org.mozilla.gecko.util.ThreadUtils; 17 18 /** 19 * GeckoEditableChild implements the Gecko-facing side of IME operation. Each nsWindow in the main 20 * process and each PuppetWidget in each child content process has an instance of 21 * GeckoEditableChild, which communicates with the GeckoEditableParent instance in the main process. 22 */ 23 public final class GeckoEditableChild extends JNIObject implements IGeckoEditableChild { 24 25 private static final boolean DEBUG = false; 26 private static final String LOGTAG = "GeckoEditableChild"; 27 28 private static final int NOTIFY_IME_TO_CANCEL_COMPOSITION = 9; 29 30 private final class RemoteChild extends IGeckoEditableChild.Stub { 31 @Override // IGeckoEditableChild 32 public void transferParent(final IGeckoEditableParent editableParent) { 33 GeckoEditableChild.this.transferParent(editableParent); 34 } 35 36 @Override // IGeckoEditableChild 37 public void onKeyEvent( 38 final int action, 39 final int keyCode, 40 final int scanCode, 41 final int metaState, 42 final int keyPressMetaState, 43 final long time, 44 final int domPrintableKeyValue, 45 final int repeatCount, 46 final int flags, 47 final boolean isSynthesizedImeKey, 48 final boolean waitingReply, 49 final KeyEvent event) { 50 GeckoEditableChild.this.onKeyEvent( 51 action, 52 keyCode, 53 scanCode, 54 metaState, 55 keyPressMetaState, 56 time, 57 domPrintableKeyValue, 58 repeatCount, 59 flags, 60 isSynthesizedImeKey, 61 waitingReply, 62 event); 63 } 64 65 @Override // IGeckoEditableChild 66 public void onImeSynchronize() { 67 GeckoEditableChild.this.onImeSynchronize(); 68 } 69 70 @Override // IGeckoEditableChild 71 public void onImeReplaceText(final int start, final int end, final String text) { 72 GeckoEditableChild.this.onImeReplaceText(start, end, text); 73 } 74 75 @Override // IGeckoEditableChild 76 public void onImeInsertImage(final byte[] data, final String mimeType) { 77 GeckoEditableChild.this.onImeInsertImage(data, mimeType); 78 } 79 80 @Override // IGeckoEditableChild 81 public void onImeAddCompositionRange( 82 final int start, 83 final int end, 84 final int rangeType, 85 final int rangeStyles, 86 final int rangeLineStyle, 87 final boolean rangeBoldLine, 88 final int rangeForeColor, 89 final int rangeBackColor, 90 final int rangeLineColor) { 91 GeckoEditableChild.this.onImeAddCompositionRange( 92 start, 93 end, 94 rangeType, 95 rangeStyles, 96 rangeLineStyle, 97 rangeBoldLine, 98 rangeForeColor, 99 rangeBackColor, 100 rangeLineColor); 101 } 102 103 @Override // IGeckoEditableChild 104 public void onImeUpdateComposition(final int start, final int end, final int flags) { 105 GeckoEditableChild.this.onImeUpdateComposition(start, end, flags); 106 } 107 108 @Override // IGeckoEditableChild 109 public void onImeRequestCursorUpdates(final int requestMode) { 110 GeckoEditableChild.this.onImeRequestCursorUpdates(requestMode); 111 } 112 113 @Override // IGeckoEditableChild 114 public void onImeRequestCommit() { 115 GeckoEditableChild.this.onImeRequestCommit(); 116 } 117 } 118 119 private final IGeckoEditableChild mEditableChild; 120 private final boolean mIsDefault; 121 122 private IGeckoEditableParent mEditableParent; 123 private int mCurrentTextLength; // Used by Gecko thread 124 125 @WrapForJNI(calledFrom = "gecko") 126 private GeckoEditableChild( 127 @Nullable final IGeckoEditableParent editableParent, final boolean isDefault) { 128 mIsDefault = isDefault; 129 130 if (editableParent != null 131 && editableParent.asBinder().queryLocalInterface(IGeckoEditableParent.class.getName()) 132 != null) { 133 // IGeckoEditableParent is local; i.e. we're in the main process. 134 mEditableChild = this; 135 } else { 136 // IGeckoEditableParent is remote; i.e. we're in a content process. 137 mEditableChild = new RemoteChild(); 138 } 139 140 if (editableParent != null) { 141 setParent(editableParent); 142 } 143 } 144 145 @WrapForJNI(calledFrom = "gecko") 146 private void setParent(final IGeckoEditableParent editableParent) { 147 mEditableParent = editableParent; 148 149 if (mIsDefault) { 150 // Tell the parent we're the default child. 151 try { 152 editableParent.setDefaultChild(mEditableChild); 153 } catch (final RemoteException e) { 154 Log.e(LOGTAG, "Failed to set default child", e); 155 } 156 } 157 } 158 159 @WrapForJNI(dispatchTo = "proxy") 160 @Override // IGeckoEditableChild 161 public native void transferParent(IGeckoEditableParent editableParent); 162 163 @WrapForJNI(dispatchTo = "proxy") 164 @Override // IGeckoEditableChild 165 public native void onKeyEvent( 166 int action, 167 int keyCode, 168 int scanCode, 169 int metaState, 170 int keyPressMetaState, 171 long time, 172 int domPrintableKeyValue, 173 int repeatCount, 174 int flags, 175 boolean isSynthesizedImeKey, 176 boolean waitingReply, 177 KeyEvent event); 178 179 @WrapForJNI(dispatchTo = "proxy") 180 @Override // IGeckoEditableChild 181 public native void onImeSynchronize(); 182 183 @WrapForJNI(dispatchTo = "proxy") 184 @Override // IGeckoEditableChild 185 public native void onImeReplaceText(int start, int end, String text); 186 187 @WrapForJNI(dispatchTo = "proxy") 188 @Override // IGeckoEditableChild 189 public native void onImeAddCompositionRange( 190 int start, 191 int end, 192 int rangeType, 193 int rangeStyles, 194 int rangeLineStyle, 195 boolean rangeBoldLine, 196 int rangeForeColor, 197 int rangeBackColor, 198 int rangeLineColor); 199 200 // Don't update to the new composition if it's different than the current composition. 201 @WrapForJNI public static final int FLAG_KEEP_CURRENT_COMPOSITION = 1; 202 203 @WrapForJNI(dispatchTo = "proxy") 204 @Override // IGeckoEditableChild 205 public native void onImeUpdateComposition(int start, int end, int flags); 206 207 @WrapForJNI(dispatchTo = "proxy") 208 @Override // IGeckoEditableChild 209 public native void onImeRequestCursorUpdates(int requestMode); 210 211 @WrapForJNI(dispatchTo = "proxy") 212 @Override // IGeckoEditableChild 213 public native void onImeRequestCommit(); 214 215 @WrapForJNI(dispatchTo = "proxy") 216 @Override // IGeckoEditableChild 217 public native void onImeInsertImage(byte[] data, String mimeType); 218 219 @Override // JNIObject 220 protected void disposeNative() { 221 // Disposal happens in native code. 222 throw new UnsupportedOperationException(); 223 } 224 225 @WrapForJNI(calledFrom = "gecko") 226 private boolean hasEditableParent() { 227 if (mEditableParent != null) { 228 return true; 229 } 230 Log.w(LOGTAG, "No editable parent"); 231 return false; 232 } 233 234 @Override // IInterface 235 public IBinder asBinder() { 236 // Return the GeckoEditableParent's binder as fallback for comparison purposes. 237 return mEditableChild != this 238 ? mEditableChild.asBinder() 239 : hasEditableParent() ? mEditableParent.asBinder() : null; 240 } 241 242 @WrapForJNI(calledFrom = "gecko") 243 private void notifyIME(final int type) { 244 if (DEBUG) { 245 ThreadUtils.assertOnGeckoThread(); 246 Log.d(LOGTAG, "notifyIME(" + type + ")"); 247 } 248 if (!hasEditableParent()) { 249 return; 250 } 251 if (type == NOTIFY_IME_TO_CANCEL_COMPOSITION) { 252 // Composition should have been canceled on the parent side through text 253 // update notifications. We cannot verify that here because we don't 254 // keep track of spans on the child side, but it's simple to add the 255 // check to the parent side if ever needed. 256 return; 257 } 258 259 try { 260 mEditableParent.notifyIME(mEditableChild, type); 261 } catch (final RemoteException e) { 262 Log.e(LOGTAG, "Remote call failed", e); 263 return; 264 } 265 } 266 267 @WrapForJNI(calledFrom = "gecko") 268 private void notifyIMEContext( 269 final int state, 270 final String typeHint, 271 final String modeHint, 272 final String actionHint, 273 final String autocapitalize, 274 final boolean autocorrect, 275 final int flags) { 276 if (DEBUG) { 277 ThreadUtils.assertOnGeckoThread(); 278 final StringBuilder sb = new StringBuilder("notifyIMEContext("); 279 sb.append(state) 280 .append(", \"") 281 .append(typeHint) 282 .append("\", \"") 283 .append(modeHint) 284 .append("\", \"") 285 .append(actionHint) 286 .append("\", \"") 287 .append(autocapitalize) 288 .append("\", ") 289 .append(autocorrect) 290 .append(", 0x") 291 .append(Integer.toHexString(flags)) 292 .append(")"); 293 Log.d(LOGTAG, sb.toString()); 294 } 295 if (!hasEditableParent()) { 296 return; 297 } 298 299 try { 300 mEditableParent.notifyIMEContext( 301 mEditableChild.asBinder(), 302 state, 303 typeHint, 304 modeHint, 305 actionHint, 306 autocapitalize, 307 autocorrect, 308 flags); 309 } catch (final RemoteException e) { 310 Log.e(LOGTAG, "Remote call failed", e); 311 } 312 } 313 314 @WrapForJNI(calledFrom = "gecko", exceptionMode = "ignore") 315 private void onSelectionChange( 316 final int start, final int end, final boolean causedOnlyByComposition) 317 throws RemoteException { 318 if (DEBUG) { 319 ThreadUtils.assertOnGeckoThread(); 320 final StringBuilder sb = new StringBuilder("onSelectionChange("); 321 sb.append(start) 322 .append(", ") 323 .append(end) 324 .append(", ") 325 .append(causedOnlyByComposition) 326 .append(")"); 327 Log.d(LOGTAG, sb.toString()); 328 } 329 if (!hasEditableParent()) { 330 return; 331 } 332 333 final int currentLength = mCurrentTextLength; 334 if (start < 0 || start > currentLength || end < 0 || end > currentLength) { 335 Log.e( 336 LOGTAG, 337 "invalid selection notification range: " 338 + start 339 + " to " 340 + end 341 + ", length: " 342 + currentLength); 343 throw new IllegalArgumentException("invalid selection notification range"); 344 } 345 346 mEditableParent.onSelectionChange( 347 mEditableChild.asBinder(), start, end, causedOnlyByComposition); 348 } 349 350 @WrapForJNI(calledFrom = "gecko", exceptionMode = "ignore") 351 private void onTextChange( 352 final CharSequence text, 353 final int start, 354 final int unboundedOldEnd, 355 final int unboundedNewEnd, 356 final boolean causedOnlyByComposition) 357 throws RemoteException { 358 if (DEBUG) { 359 ThreadUtils.assertOnGeckoThread(); 360 final StringBuilder sb = new StringBuilder("onTextChange("); 361 sb.append(text) 362 .append(", ") 363 .append(start) 364 .append(", ") 365 .append(unboundedOldEnd) 366 .append(", ") 367 .append(unboundedNewEnd) 368 .append(", ") 369 .append(causedOnlyByComposition) 370 .append(")"); 371 Log.d(LOGTAG, sb.toString()); 372 } 373 if (!hasEditableParent()) { 374 return; 375 } 376 377 if (start < 0 || start > unboundedOldEnd) { 378 Log.e(LOGTAG, "invalid text notification range: " + start + " to " + unboundedOldEnd); 379 throw new IllegalArgumentException("invalid text notification range"); 380 } 381 382 /* For the "end" parameters, Gecko can pass in a large 383 number to denote "end of the text". Fix that here */ 384 final int currentLength = mCurrentTextLength; 385 final int oldEnd = unboundedOldEnd > currentLength ? currentLength : unboundedOldEnd; 386 // new end should always match text 387 if (unboundedOldEnd <= currentLength && unboundedNewEnd != (start + text.length())) { 388 Log.e( 389 LOGTAG, 390 "newEnd does not match text: " + unboundedNewEnd + " vs " + (start + text.length())); 391 throw new IllegalArgumentException("newEnd does not match text"); 392 } 393 394 mCurrentTextLength += start + text.length() - oldEnd; 395 // Need unboundedOldEnd so GeckoEditable can distinguish changed text vs cleared text. 396 if (text.length() == 0) { 397 // Remove text in range. 398 mEditableParent.onTextChange( 399 mEditableChild.asBinder(), text, start, unboundedOldEnd, causedOnlyByComposition); 400 return; 401 } 402 // Using large text causes TransactionTooLargeException, so split text data. 403 int offset = 0; 404 int newUnboundedOldEnd = unboundedOldEnd; 405 while (offset < text.length()) { 406 final int end = Math.min(offset + 1024 * 64 /* 64KB */, text.length()); 407 mEditableParent.onTextChange( 408 mEditableChild.asBinder(), 409 text.subSequence(offset, end), 410 start + offset, 411 newUnboundedOldEnd, 412 causedOnlyByComposition); 413 offset = end; 414 newUnboundedOldEnd = start + offset; 415 } 416 } 417 418 @WrapForJNI(calledFrom = "gecko") 419 private void onDefaultKeyEvent(final KeyEvent event) { 420 if (DEBUG) { 421 // GeckoEditableListener methods should all be called from the Gecko thread 422 ThreadUtils.assertOnGeckoThread(); 423 final StringBuilder sb = new StringBuilder("onDefaultKeyEvent("); 424 sb.append("action=") 425 .append(event.getAction()) 426 .append(", ") 427 .append("keyCode=") 428 .append(event.getKeyCode()) 429 .append(", ") 430 .append("metaState=") 431 .append(event.getMetaState()) 432 .append(", ") 433 .append("time=") 434 .append(event.getEventTime()) 435 .append(", ") 436 .append("repeatCount=") 437 .append(event.getRepeatCount()) 438 .append(")"); 439 Log.d(LOGTAG, sb.toString()); 440 } 441 if (!hasEditableParent()) { 442 return; 443 } 444 445 try { 446 mEditableParent.onDefaultKeyEvent(mEditableChild.asBinder(), event); 447 } catch (final RemoteException e) { 448 Log.e(LOGTAG, "Remote call failed", e); 449 } 450 } 451 452 @WrapForJNI(calledFrom = "gecko") 453 private void updateCompositionRects(final RectF[] rects, final RectF caretRect) { 454 if (DEBUG) { 455 // GeckoEditableListener methods should all be called from the Gecko thread 456 ThreadUtils.assertOnGeckoThread(); 457 Log.d(LOGTAG, "updateCompositionRects(rects.length = " + rects.length + ")"); 458 } 459 if (!hasEditableParent()) { 460 return; 461 } 462 463 try { 464 mEditableParent.updateCompositionRects(mEditableChild.asBinder(), rects, caretRect); 465 } catch (final RemoteException e) { 466 Log.e(LOGTAG, "Remote call failed", e); 467 } 468 } 469 }