gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/app/GioView.java (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package org.gioui; 4 5 import java.lang.Class; 6 import java.lang.IllegalAccessException; 7 import java.lang.InstantiationException; 8 import java.lang.ExceptionInInitializerError; 9 import java.lang.SecurityException; 10 import android.app.Activity; 11 import android.app.Fragment; 12 import android.app.FragmentManager; 13 import android.app.FragmentTransaction; 14 import android.content.Context; 15 import android.graphics.Canvas; 16 import android.graphics.Color; 17 import android.graphics.Matrix; 18 import android.graphics.Rect; 19 import android.os.Build; 20 import android.os.Bundle; 21 import android.os.Handler; 22 import android.os.SystemClock; 23 import android.text.TextUtils; 24 import android.text.Selection; 25 import android.text.SpannableStringBuilder; 26 import android.util.AttributeSet; 27 import android.util.TypedValue; 28 import android.view.Choreographer; 29 import android.view.Display; 30 import android.view.KeyCharacterMap; 31 import android.view.KeyEvent; 32 import android.view.MotionEvent; 33 import android.view.PointerIcon; 34 import android.view.View; 35 import android.view.ViewConfiguration; 36 import android.view.WindowInsets; 37 import android.view.Surface; 38 import android.view.SurfaceView; 39 import android.view.SurfaceHolder; 40 import android.view.Window; 41 import android.view.WindowInsetsController; 42 import android.view.WindowManager; 43 import android.view.inputmethod.CorrectionInfo; 44 import android.view.inputmethod.CompletionInfo; 45 import android.view.inputmethod.CursorAnchorInfo; 46 import android.view.inputmethod.EditorInfo; 47 import android.view.inputmethod.ExtractedText; 48 import android.view.inputmethod.ExtractedTextRequest; 49 import android.view.inputmethod.InputConnection; 50 import android.view.inputmethod.InputMethodManager; 51 import android.view.inputmethod.InputContentInfo; 52 import android.view.inputmethod.SurroundingText; 53 import android.view.accessibility.AccessibilityNodeProvider; 54 import android.view.accessibility.AccessibilityNodeInfo; 55 import android.view.accessibility.AccessibilityEvent; 56 import android.view.accessibility.AccessibilityManager; 57 58 import java.io.UnsupportedEncodingException; 59 60 public final class GioView extends SurfaceView implements Choreographer.FrameCallback { 61 private static boolean jniLoaded; 62 63 private final SurfaceHolder.Callback surfCallbacks; 64 private final View.OnFocusChangeListener focusCallback; 65 private final InputMethodManager imm; 66 private final float scrollXScale; 67 private final float scrollYScale; 68 private final AccessibilityManager accessManager; 69 private int keyboardHint; 70 71 private long nhandle; 72 73 public GioView(Context context) { 74 this(context, null); 75 } 76 77 public GioView(Context context, AttributeSet attrs) { 78 super(context, attrs); 79 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 80 setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); 81 } 82 setLayoutParams(new WindowManager.LayoutParams(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT)); 83 84 // Late initialization of the Go runtime to wait for a valid context. 85 Gio.init(context.getApplicationContext()); 86 87 // Set background color to transparent to avoid a flickering 88 // issue on ChromeOS. 89 setBackgroundColor(Color.argb(0, 0, 0, 0)); 90 91 ViewConfiguration conf = ViewConfiguration.get(context); 92 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 93 scrollXScale = conf.getScaledHorizontalScrollFactor(); 94 scrollYScale = conf.getScaledVerticalScrollFactor(); 95 96 // The platform focus highlight is not aware of Gio's widgets. 97 setDefaultFocusHighlightEnabled(false); 98 } else { 99 float listItemHeight = 48; // dp 100 float px = TypedValue.applyDimension( 101 TypedValue.COMPLEX_UNIT_DIP, 102 listItemHeight, 103 getResources().getDisplayMetrics() 104 ); 105 scrollXScale = px; 106 scrollYScale = px; 107 } 108 109 setHighRefreshRate(); 110 111 accessManager = (AccessibilityManager)context.getSystemService(Context.ACCESSIBILITY_SERVICE); 112 imm = (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE); 113 nhandle = onCreateView(this); 114 setFocusable(true); 115 setFocusableInTouchMode(true); 116 focusCallback = new View.OnFocusChangeListener() { 117 @Override public void onFocusChange(View v, boolean focus) { 118 GioView.this.onFocusChange(nhandle, focus); 119 } 120 }; 121 setOnFocusChangeListener(focusCallback); 122 surfCallbacks = new SurfaceHolder.Callback() { 123 @Override public void surfaceCreated(SurfaceHolder holder) { 124 // Ignore; surfaceChanged is guaranteed to be called immediately after this. 125 } 126 @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 127 onSurfaceChanged(nhandle, getHolder().getSurface()); 128 } 129 @Override public void surfaceDestroyed(SurfaceHolder holder) { 130 onSurfaceDestroyed(nhandle); 131 } 132 }; 133 getHolder().addCallback(surfCallbacks); 134 } 135 136 @Override public boolean onKeyDown(int keyCode, KeyEvent event) { 137 if (nhandle != 0) { 138 onKeyEvent(nhandle, keyCode, event.getUnicodeChar(), true, event.getEventTime()); 139 } 140 return false; 141 } 142 143 @Override public boolean onKeyUp(int keyCode, KeyEvent event) { 144 if (nhandle != 0) { 145 onKeyEvent(nhandle, keyCode, event.getUnicodeChar(), false, event.getEventTime()); 146 } 147 return false; 148 } 149 150 @Override public boolean onGenericMotionEvent(MotionEvent event) { 151 dispatchMotionEvent(event); 152 return true; 153 } 154 155 @Override public boolean onTouchEvent(MotionEvent event) { 156 // Ask for unbuffered events. Flutter and Chrome do it 157 // so assume it's good for us as well. 158 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 159 requestUnbufferedDispatch(event); 160 } 161 162 dispatchMotionEvent(event); 163 return true; 164 } 165 166 private void setCursor(int id) { 167 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { 168 return; 169 } 170 PointerIcon pointerIcon = PointerIcon.getSystemIcon(getContext(), id); 171 setPointerIcon(pointerIcon); 172 } 173 174 private void setOrientation(int id, int fallback) { 175 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { 176 id = fallback; 177 } 178 ((Activity) this.getContext()).setRequestedOrientation(id); 179 } 180 181 private void setFullscreen(boolean enabled) { 182 int flags = this.getSystemUiVisibility(); 183 if (enabled) { 184 flags |= SYSTEM_UI_FLAG_IMMERSIVE_STICKY; 185 flags |= SYSTEM_UI_FLAG_HIDE_NAVIGATION; 186 flags |= SYSTEM_UI_FLAG_FULLSCREEN; 187 flags |= SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; 188 } else { 189 flags &= ~SYSTEM_UI_FLAG_IMMERSIVE_STICKY; 190 flags &= ~SYSTEM_UI_FLAG_HIDE_NAVIGATION; 191 flags &= ~SYSTEM_UI_FLAG_FULLSCREEN; 192 flags &= ~SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; 193 } 194 this.setSystemUiVisibility(flags); 195 } 196 197 private enum Bar { 198 NAVIGATION, 199 STATUS, 200 } 201 202 private void setBarColor(Bar t, int color, int luminance) { 203 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 204 return; 205 } 206 207 Window window = ((Activity) this.getContext()).getWindow(); 208 209 int insetsMask; 210 int viewMask; 211 212 switch (t) { 213 case STATUS: 214 insetsMask = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; 215 viewMask = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; 216 window.setStatusBarColor(color); 217 break; 218 case NAVIGATION: 219 insetsMask = WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; 220 viewMask = View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; 221 window.setNavigationBarColor(color); 222 break; 223 default: 224 throw new RuntimeException("invalid bar type"); 225 } 226 227 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 228 return; 229 } 230 231 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { 232 int flags = this.getSystemUiVisibility(); 233 if (luminance > 128) { 234 flags |= viewMask; 235 } else { 236 flags &= ~viewMask; 237 } 238 this.setSystemUiVisibility(flags); 239 return; 240 } 241 242 WindowInsetsController insetsController = window.getInsetsController(); 243 if (insetsController == null) { 244 return; 245 } 246 if (luminance > 128) { 247 insetsController.setSystemBarsAppearance(insetsMask, insetsMask); 248 } else { 249 insetsController.setSystemBarsAppearance(0, insetsMask); 250 } 251 } 252 253 private void setStatusColor(int color, int luminance) { 254 this.setBarColor(Bar.STATUS, color, luminance); 255 } 256 257 private void setNavigationColor(int color, int luminance) { 258 this.setBarColor(Bar.NAVIGATION, color, luminance); 259 } 260 261 private void setHighRefreshRate() { 262 Context context = getContext(); 263 Display display = context.getDisplay(); 264 Display.Mode[] supportedModes = display.getSupportedModes(); 265 if (supportedModes.length <= 1) { 266 // Nothing to set 267 return; 268 } 269 270 Display.Mode currentMode = display.getMode(); 271 int currentWidth = currentMode.getPhysicalWidth(); 272 int currentHeight = currentMode.getPhysicalHeight(); 273 274 float minRefreshRate = -1; 275 float maxRefreshRate = -1; 276 float bestRefreshRate = -1; 277 int bestModeId = -1; 278 for (Display.Mode mode : supportedModes) { 279 float refreshRate = mode.getRefreshRate(); 280 float width = mode.getPhysicalWidth(); 281 float height = mode.getPhysicalHeight(); 282 283 if (minRefreshRate == -1 || refreshRate < minRefreshRate) { 284 minRefreshRate = refreshRate; 285 } 286 if (maxRefreshRate == -1 || refreshRate > maxRefreshRate) { 287 maxRefreshRate = refreshRate; 288 } 289 290 boolean refreshRateIsBetter = bestRefreshRate == -1 || refreshRate > bestRefreshRate; 291 if (width == currentWidth && height == currentHeight && refreshRateIsBetter) { 292 int modeId = mode.getModeId(); 293 bestRefreshRate = refreshRate; 294 bestModeId = modeId; 295 } 296 } 297 298 if (bestModeId == -1) { 299 // Not expecting this but just in case 300 return; 301 } 302 303 if (minRefreshRate == maxRefreshRate) { 304 // Can't improve the refresh rate 305 return; 306 } 307 308 Window window = ((Activity) context).getWindow(); 309 WindowManager.LayoutParams layoutParams = window.getAttributes(); 310 layoutParams.preferredDisplayModeId = bestModeId; 311 window.setAttributes(layoutParams); 312 } 313 314 @Override protected boolean dispatchHoverEvent(MotionEvent event) { 315 if (!accessManager.isTouchExplorationEnabled()) { 316 return super.dispatchHoverEvent(event); 317 } 318 switch (event.getAction()) { 319 case MotionEvent.ACTION_HOVER_ENTER: 320 // Fall through. 321 case MotionEvent.ACTION_HOVER_MOVE: 322 onTouchExploration(nhandle, event.getX(), event.getY()); 323 break; 324 case MotionEvent.ACTION_HOVER_EXIT: 325 onExitTouchExploration(nhandle); 326 break; 327 } 328 return true; 329 } 330 331 void sendA11yEvent(int eventType, int viewId) { 332 if (!accessManager.isEnabled()) { 333 return; 334 } 335 AccessibilityEvent event = obtainA11yEvent(eventType, viewId); 336 getParent().requestSendAccessibilityEvent(this, event); 337 } 338 339 AccessibilityEvent obtainA11yEvent(int eventType, int viewId) { 340 AccessibilityEvent event = AccessibilityEvent.obtain(eventType); 341 event.setPackageName(getContext().getPackageName()); 342 event.setSource(this, viewId); 343 return event; 344 } 345 346 boolean isA11yActive() { 347 return accessManager.isEnabled(); 348 } 349 350 void sendA11yChange(int viewId) { 351 if (!accessManager.isEnabled()) { 352 return; 353 } 354 AccessibilityEvent event = obtainA11yEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED, viewId); 355 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 356 event.setContentChangeTypes(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); 357 } 358 getParent().requestSendAccessibilityEvent(this, event); 359 } 360 361 private void dispatchMotionEvent(MotionEvent event) { 362 if (nhandle == 0) { 363 return; 364 } 365 for (int j = 0; j < event.getHistorySize(); j++) { 366 long time = event.getHistoricalEventTime(j); 367 for (int i = 0; i < event.getPointerCount(); i++) { 368 onTouchEvent( 369 nhandle, 370 event.ACTION_MOVE, 371 event.getPointerId(i), 372 event.getToolType(i), 373 event.getHistoricalX(i, j), 374 event.getHistoricalY(i, j), 375 scrollXScale*event.getHistoricalAxisValue(MotionEvent.AXIS_HSCROLL, i, j), 376 scrollYScale*event.getHistoricalAxisValue(MotionEvent.AXIS_VSCROLL, i, j), 377 event.getButtonState(), 378 time); 379 } 380 } 381 int act = event.getActionMasked(); 382 int idx = event.getActionIndex(); 383 for (int i = 0; i < event.getPointerCount(); i++) { 384 int pact = event.ACTION_MOVE; 385 if (i == idx) { 386 pact = act; 387 } 388 onTouchEvent( 389 nhandle, 390 pact, 391 event.getPointerId(i), 392 event.getToolType(i), 393 event.getX(i), event.getY(i), 394 scrollXScale*event.getAxisValue(MotionEvent.AXIS_HSCROLL, i), 395 scrollYScale*event.getAxisValue(MotionEvent.AXIS_VSCROLL, i), 396 event.getButtonState(), 397 event.getEventTime()); 398 } 399 } 400 401 @Override public InputConnection onCreateInputConnection(EditorInfo editor) { 402 Snippet snip = getSnippet(); 403 editor.inputType = this.keyboardHint; 404 editor.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN | EditorInfo.IME_FLAG_NO_EXTRACT_UI; 405 editor.initialSelStart = imeToUTF16(nhandle, imeSelectionStart(nhandle)); 406 editor.initialSelEnd = imeToUTF16(nhandle, imeSelectionEnd(nhandle)); 407 int selStart = editor.initialSelStart - snip.offset; 408 editor.initialCapsMode = TextUtils.getCapsMode(snip.snippet, selStart, this.keyboardHint); 409 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 410 editor.setInitialSurroundingSubText(snip.snippet, imeToUTF16(nhandle, snip.offset)); 411 } 412 imeSetComposingRegion(nhandle, -1, -1); 413 return new GioInputConnection(); 414 } 415 416 void setInputHint(int hint) { 417 if (hint == this.keyboardHint) { 418 return; 419 } 420 this.keyboardHint = hint; 421 restartInput(); 422 } 423 424 void showTextInput() { 425 GioView.this.requestFocus(); 426 imm.showSoftInput(GioView.this, 0); 427 } 428 429 void hideTextInput() { 430 imm.hideSoftInputFromWindow(getWindowToken(), 0); 431 } 432 433 @Override protected boolean fitSystemWindows(Rect insets) { 434 if (nhandle != 0) { 435 onWindowInsets(nhandle, insets.top, insets.right, insets.bottom, insets.left); 436 } 437 return true; 438 } 439 440 void postFrameCallback() { 441 Choreographer.getInstance().removeFrameCallback(this); 442 Choreographer.getInstance().postFrameCallback(this); 443 } 444 445 @Override public void doFrame(long nanos) { 446 if (nhandle != 0) { 447 onFrameCallback(nhandle); 448 } 449 } 450 451 int getDensity() { 452 return getResources().getDisplayMetrics().densityDpi; 453 } 454 455 float getFontScale() { 456 return getResources().getConfiguration().fontScale; 457 } 458 459 public void start() { 460 if (nhandle != 0) { 461 onStartView(nhandle); 462 } 463 } 464 465 public void stop() { 466 if (nhandle != 0) { 467 onStopView(nhandle); 468 } 469 } 470 471 public void destroy() { 472 if (nhandle != 0) { 473 onDestroyView(nhandle); 474 } 475 } 476 477 protected void unregister() { 478 setOnFocusChangeListener(null); 479 getHolder().removeCallback(surfCallbacks); 480 nhandle = 0; 481 } 482 483 public void configurationChanged() { 484 if (nhandle != 0) { 485 onConfigurationChanged(nhandle); 486 } 487 } 488 489 public boolean backPressed() { 490 if (nhandle == 0) { 491 return false; 492 } 493 return onBack(nhandle); 494 } 495 496 void restartInput() { 497 imm.restartInput(this); 498 } 499 500 void updateSelection() { 501 int selStart = imeToUTF16(nhandle, imeSelectionStart(nhandle)); 502 int selEnd = imeToUTF16(nhandle, imeSelectionEnd(nhandle)); 503 int compStart = imeToUTF16(nhandle, imeComposingStart(nhandle)); 504 int compEnd = imeToUTF16(nhandle, imeComposingEnd(nhandle)); 505 imm.updateSelection(this, selStart, selEnd, compStart, compEnd); 506 } 507 508 void updateCaret(float m00, float m01, float m02, float m10, float m11, float m12, float caretX, float caretTop, float caretBase, float caretBottom) { 509 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 510 return; 511 } 512 Matrix m = new Matrix(); 513 m.setValues(new float[]{m00, m01, m02, m10, m11, m12, 0.0f, 0.0f, 1.0f}); 514 m.setConcat(getMatrix(), m); 515 int selStart = imeSelectionStart(nhandle); 516 int selEnd = imeSelectionEnd(nhandle); 517 int compStart = imeComposingStart(nhandle); 518 int compEnd = imeComposingEnd(nhandle); 519 Snippet snip = getSnippet(); 520 String composing = ""; 521 if (compStart != -1) { 522 composing = snip.substringRunes(compStart, compEnd); 523 } 524 CursorAnchorInfo inf = new CursorAnchorInfo.Builder() 525 .setMatrix(m) 526 .setComposingText(imeToUTF16(nhandle, compStart), composing) 527 .setSelectionRange(imeToUTF16(nhandle, selStart), imeToUTF16(nhandle, selEnd)) 528 .setInsertionMarkerLocation(caretX, caretTop, caretBase, caretBottom, 0) 529 .build(); 530 imm.updateCursorAnchorInfo(this, inf); 531 } 532 533 static private native long onCreateView(GioView view); 534 static private native void onDestroyView(long handle); 535 static private native void onStartView(long handle); 536 static private native void onStopView(long handle); 537 static private native void onSurfaceDestroyed(long handle); 538 static private native void onSurfaceChanged(long handle, Surface surface); 539 static private native void onConfigurationChanged(long handle); 540 static private native void onWindowInsets(long handle, int top, int right, int bottom, int left); 541 static public native void onLowMemory(); 542 static private native void onTouchEvent(long handle, int action, int pointerID, int tool, float x, float y, float scrollX, float scrollY, int buttons, long time); 543 static private native void onKeyEvent(long handle, int code, int character, boolean pressed, long time); 544 static private native void onFrameCallback(long handle); 545 static private native boolean onBack(long handle); 546 static private native void onFocusChange(long handle, boolean focus); 547 static private native AccessibilityNodeInfo initializeAccessibilityNodeInfo(long handle, int viewId, int screenX, int screenY, AccessibilityNodeInfo info); 548 static private native void onTouchExploration(long handle, float x, float y); 549 static private native void onExitTouchExploration(long handle); 550 static private native void onA11yFocus(long handle, int viewId); 551 static private native void onClearA11yFocus(long handle, int viewId); 552 static private native void imeSetSnippet(long handle, int start, int end); 553 static private native String imeSnippet(long handle); 554 static private native int imeSnippetStart(long handle); 555 static private native int imeSelectionStart(long handle); 556 static private native int imeSelectionEnd(long handle); 557 static private native int imeComposingStart(long handle); 558 static private native int imeComposingEnd(long handle); 559 static private native int imeReplace(long handle, int start, int end, String text); 560 static private native int imeSetSelection(long handle, int start, int end); 561 static private native int imeSetComposingRegion(long handle, int start, int end); 562 // imeToRunes converts the Java character index into runes (Java code points). 563 static private native int imeToRunes(long handle, int chars); 564 // imeToUTF16 converts the rune index into Java characters. 565 static private native int imeToUTF16(long handle, int runes); 566 567 private class GioInputConnection implements InputConnection { 568 private int batchDepth; 569 570 @Override public boolean beginBatchEdit() { 571 batchDepth++; 572 return true; 573 } 574 575 @Override public boolean endBatchEdit() { 576 batchDepth--; 577 return batchDepth > 0; 578 } 579 580 @Override public boolean clearMetaKeyStates(int states) { 581 return false; 582 } 583 584 @Override public boolean commitCompletion(CompletionInfo text) { 585 return false; 586 } 587 588 @Override public boolean commitCorrection(CorrectionInfo info) { 589 return false; 590 } 591 592 @Override public boolean commitText(CharSequence text, int cursor) { 593 setComposingText(text, cursor); 594 return finishComposingText(); 595 } 596 597 @Override public boolean deleteSurroundingText(int beforeChars, int afterChars) { 598 // translate before and after to runes. 599 int selStart = imeSelectionStart(nhandle); 600 int selEnd = imeSelectionEnd(nhandle); 601 int before = selStart - imeToRunes(nhandle, imeToUTF16(nhandle, selStart) - beforeChars); 602 int after = selEnd - imeToRunes(nhandle, imeToUTF16(nhandle, selEnd) - afterChars); 603 return deleteSurroundingTextInCodePoints(before, after); 604 } 605 606 @Override public boolean finishComposingText() { 607 imeSetComposingRegion(nhandle, -1, -1); 608 return true; 609 } 610 611 @Override public int getCursorCapsMode(int reqModes) { 612 Snippet snip = getSnippet(); 613 int selStart = imeSelectionStart(nhandle); 614 int off = imeToUTF16(nhandle, selStart - snip.offset); 615 if (off < 0 || off > snip.snippet.length()) { 616 return 0; 617 } 618 return TextUtils.getCapsMode(snip.snippet, off, reqModes); 619 } 620 621 @Override public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { 622 return null; 623 } 624 625 @Override public CharSequence getSelectedText(int flags) { 626 Snippet snip = getSnippet(); 627 int selStart = imeSelectionStart(nhandle); 628 int selEnd = imeSelectionEnd(nhandle); 629 String sub = snip.substringRunes(selStart, selEnd); 630 return sub; 631 } 632 633 @Override public CharSequence getTextAfterCursor(int n, int flags) { 634 Snippet snip = getSnippet(); 635 int selStart = imeSelectionStart(nhandle); 636 int selEnd = imeSelectionEnd(nhandle); 637 // n are in Java characters, but in worst case we'll just ask for more runes 638 // than wanted. 639 imeSetSnippet(nhandle, selStart - n, selEnd + n); 640 int start = selEnd; 641 int end = imeToRunes(nhandle, imeToUTF16(nhandle, selEnd) + n); 642 String ret = snip.substringRunes(start, end); 643 return ret; 644 } 645 646 @Override public CharSequence getTextBeforeCursor(int n, int flags) { 647 Snippet snip = getSnippet(); 648 int selStart = imeSelectionStart(nhandle); 649 int selEnd = imeSelectionEnd(nhandle); 650 // n are in Java characters, but in worst case we'll just ask for more runes 651 // than wanted. 652 imeSetSnippet(nhandle, selStart - n, selEnd + n); 653 int start = imeToRunes(nhandle, imeToUTF16(nhandle, selStart) - n); 654 int end = selStart; 655 String ret = snip.substringRunes(start, end); 656 return ret; 657 } 658 659 @Override public boolean performContextMenuAction(int id) { 660 return false; 661 } 662 663 @Override public boolean performEditorAction(int editorAction) { 664 long eventTime = SystemClock.uptimeMillis(); 665 // Translate to enter key. 666 onKeyEvent(nhandle, KeyEvent.KEYCODE_ENTER, '\n', true, eventTime); 667 onKeyEvent(nhandle, KeyEvent.KEYCODE_ENTER, '\n', false, eventTime); 668 return true; 669 } 670 671 @Override public boolean performPrivateCommand(String action, Bundle data) { 672 return false; 673 } 674 675 @Override public boolean reportFullscreenMode(boolean enabled) { 676 return false; 677 } 678 679 @Override public boolean sendKeyEvent(KeyEvent event) { 680 boolean pressed = event.getAction() == KeyEvent.ACTION_DOWN; 681 onKeyEvent(nhandle, event.getKeyCode(), event.getUnicodeChar(), pressed, event.getEventTime()); 682 return true; 683 } 684 685 @Override public boolean setComposingRegion(int startChars, int endChars) { 686 int compStart = imeToRunes(nhandle, startChars); 687 int compEnd = imeToRunes(nhandle, endChars); 688 imeSetComposingRegion(nhandle, compStart, compEnd); 689 return true; 690 } 691 692 @Override public boolean setComposingText(CharSequence text, int relCursor) { 693 int start = imeComposingStart(nhandle); 694 int end = imeComposingEnd(nhandle); 695 if (start == -1 || end == -1) { 696 start = imeSelectionStart(nhandle); 697 end = imeSelectionEnd(nhandle); 698 } 699 String str = text.toString(); 700 imeReplace(nhandle, start, end, str); 701 int cursor = start; 702 int runes = str.codePointCount(0, str.length()); 703 if (relCursor > 0) { 704 cursor += runes; 705 relCursor--; 706 } 707 imeSetComposingRegion(nhandle, start, start + runes); 708 709 // Move cursor. 710 Snippet snip = getSnippet(); 711 cursor = imeToRunes(nhandle, imeToUTF16(nhandle, cursor) + relCursor); 712 imeSetSelection(nhandle, cursor, cursor); 713 return true; 714 } 715 716 @Override public boolean setSelection(int startChars, int endChars) { 717 int start = imeToRunes(nhandle, startChars); 718 int end = imeToRunes(nhandle, endChars); 719 imeSetSelection(nhandle, start, end); 720 return true; 721 } 722 723 /*@Override*/ public boolean requestCursorUpdates(int cursorUpdateMode) { 724 // We always provide cursor updates. 725 return true; 726 } 727 728 /*@Override*/ public void closeConnection() { 729 } 730 731 /*@Override*/ public Handler getHandler() { 732 return null; 733 } 734 735 /*@Override*/ public boolean commitContent(InputContentInfo info, int flags, Bundle opts) { 736 return false; 737 } 738 739 /*@Override*/ public boolean deleteSurroundingTextInCodePoints(int before, int after) { 740 if (after > 0) { 741 int selEnd = imeSelectionEnd(nhandle); 742 imeReplace(nhandle, selEnd, selEnd + after, ""); 743 } 744 if (before > 0) { 745 int selStart = imeSelectionStart(nhandle); 746 imeReplace(nhandle, selStart - before, selStart, ""); 747 } 748 return true; 749 } 750 751 /*@Override*/ public SurroundingText getSurroundingText(int beforeChars, int afterChars, int flags) { 752 Snippet snip = getSnippet(); 753 int selStart = imeSelectionStart(nhandle); 754 int selEnd = imeSelectionEnd(nhandle); 755 // Expanding in Java characters is ok. 756 imeSetSnippet(nhandle, selStart - beforeChars, selEnd + afterChars); 757 return new SurroundingText(snip.snippet, imeToUTF16(nhandle, selStart), imeToUTF16(nhandle, selEnd), imeToUTF16(nhandle, snip.offset)); 758 } 759 } 760 761 private Snippet getSnippet() { 762 Snippet snip = new Snippet(); 763 snip.snippet = imeSnippet(nhandle); 764 snip.offset = imeSnippetStart(nhandle); 765 return snip; 766 } 767 768 // Snippet is like android.view.inputmethod.SurroundingText but available for Android < 31. 769 private static class Snippet { 770 String snippet; 771 // offset of snippet into the entire editor content. It is in runes because we won't require 772 // Gio editors to keep track of UTF-16 offsets. The distinction won't matter in practice because IMEs only 773 // ever see snippets. 774 int offset; 775 776 // substringRunes returns the substring from start to end in runes. The resuls is 777 // truncated to the snippet. 778 String substringRunes(int start, int end) { 779 start -= this.offset; 780 end -= this.offset; 781 int runes = snippet.codePointCount(0, snippet.length()); 782 if (start < 0) { 783 start = 0; 784 } 785 if (end < 0) { 786 end = 0; 787 } 788 if (start > runes) { 789 start = runes; 790 } 791 if (end > runes) { 792 end = runes; 793 } 794 return snippet.substring( 795 snippet.offsetByCodePoints(0, start), 796 snippet.offsetByCodePoints(0, end) 797 ); 798 } 799 } 800 801 @Override public AccessibilityNodeProvider getAccessibilityNodeProvider() { 802 return new AccessibilityNodeProvider() { 803 private final int[] screenOff = new int[2]; 804 805 @Override public AccessibilityNodeInfo createAccessibilityNodeInfo(int viewId) { 806 AccessibilityNodeInfo info = null; 807 if (viewId == View.NO_ID) { 808 info = AccessibilityNodeInfo.obtain(GioView.this); 809 GioView.this.onInitializeAccessibilityNodeInfo(info); 810 } else { 811 info = AccessibilityNodeInfo.obtain(GioView.this, viewId); 812 info.setPackageName(getContext().getPackageName()); 813 info.setVisibleToUser(true); 814 } 815 GioView.this.getLocationOnScreen(screenOff); 816 info = GioView.this.initializeAccessibilityNodeInfo(nhandle, viewId, screenOff[0], screenOff[1], info); 817 return info; 818 } 819 820 @Override public boolean performAction(int viewId, int action, Bundle arguments) { 821 if (viewId == View.NO_ID) { 822 return GioView.this.performAccessibilityAction(action, arguments); 823 } 824 switch (action) { 825 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: 826 GioView.this.onA11yFocus(nhandle, viewId); 827 GioView.this.sendA11yEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED, viewId); 828 return true; 829 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: 830 GioView.this.onClearA11yFocus(nhandle, viewId); 831 GioView.this.sendA11yEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED, viewId); 832 return true; 833 } 834 return false; 835 } 836 }; 837 } 838 }