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  }