diff options
Diffstat (limited to 'platform/android/java/lib')
16 files changed, 685 insertions, 314 deletions
diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle index 6b82326a27..c9e2a5d7d2 100644 --- a/platform/android/java/lib/build.gradle +++ b/platform/android/java/lib/build.gradle @@ -100,25 +100,34 @@ android {              throw new GradleException("Invalid product flavor: $flavorName")          } -        boolean toolsFlag = flavorName == "editor" -          def buildType = variant.buildType.name -        if (buildType == null || buildType == "" || !supportedTargetsMap.containsKey(buildType)) { +        if (buildType == null || buildType == "" || !supportedFlavorsBuildTypes[flavorName].contains(buildType)) {              throw new GradleException("Invalid build type: $buildType")          } -        def sconsTarget = supportedTargetsMap[buildType] -        if (sconsTarget == null || sconsTarget == "") { -            throw new GradleException("Invalid scons target: $sconsTarget") +        boolean devBuild = buildType == "dev" + +        def sconsTarget = flavorName +        if (sconsTarget == "template") { +            switch (buildType) { +                case "release": +                    sconsTarget += "_release" +                    break +                case "debug": +                case "dev": +                default: +                    sconsTarget += "_debug" +                    break; +            }          }          // Update the name of the generated library -        def outputSuffix = "${buildType}.aar" -        if (toolsFlag) { -            outputSuffix = "tools.$outputSuffix" +        def outputSuffix = "${sconsTarget}" +        if (devBuild) { +            outputSuffix = "${outputSuffix}.dev"          }          variant.outputs.all { output -> -            output.outputFileName = "godot-lib.${outputSuffix}" +            output.outputFileName = "godot-lib.${outputSuffix}.aar"          }          // Find scons' executable path @@ -159,7 +168,7 @@ android {              def taskName = getSconsTaskName(flavorName, buildType, selectedAbi)              tasks.create(name: taskName, type: Exec) {                  executable sconsExecutableFile.absolutePath -                args "--directory=${pathToRootDir}", "platform=android", "tools=${toolsFlag}", "target=${sconsTarget}", "android_arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors() +                args "--directory=${pathToRootDir}", "platform=android", "dev_mode=${devBuild}", "dev_build=${devBuild}", "target=${sconsTarget}", "arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors()              }              // Schedule the tasks so the generated libs are present before the aar file is packaged. diff --git a/platform/android/java/lib/res/values/strings.xml b/platform/android/java/lib/res/values/strings.xml index 010006b81e..f5a4ab1071 100644 --- a/platform/android/java/lib/res/values/strings.xml +++ b/platform/android/java/lib/res/values/strings.xml @@ -12,6 +12,8 @@      <string name="text_button_resume">Resume Download</string>      <string name="text_button_cancel">Cancel</string>      <string name="text_button_cancel_verify">Cancel Verification</string> +    <string name="text_error_title">Error!</string> +    <string name="error_engine_setup_message">Unable to setup the Godot Engine! Aborting…</string>      <!-- APK Expansion Strings --> diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.java b/platform/android/java/lib/src/org/godotengine/godot/Godot.java index 28e689e63a..92e5e59496 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java @@ -57,6 +57,7 @@ import android.content.SharedPreferences.Editor;  import android.content.pm.ConfigurationInfo;  import android.content.pm.PackageManager;  import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Resources;  import android.graphics.Point;  import android.graphics.Rect;  import android.hardware.Sensor; @@ -69,6 +70,7 @@ import android.os.Environment;  import android.os.Messenger;  import android.os.VibrationEffect;  import android.os.Vibrator; +import android.util.Log;  import android.view.Display;  import android.view.LayoutInflater;  import android.view.Surface; @@ -85,6 +87,8 @@ import android.widget.TextView;  import androidx.annotation.CallSuper;  import androidx.annotation.Keep;  import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes;  import androidx.fragment.app.Fragment;  import com.google.android.vending.expansion.downloader.DownloadProgressInfo; @@ -105,6 +109,8 @@ import java.util.List;  import java.util.Locale;  public class Godot extends Fragment implements SensorEventListener, IDownloaderClient { +	private static final String TAG = Godot.class.getSimpleName(); +  	private IStub mDownloaderClientStub;  	private TextView mStatusText;  	private TextView mProgressFraction; @@ -250,7 +256,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC  	 * Used by the native code (java_godot_lib_jni.cpp) to complete initialization of the GLSurfaceView view and renderer.  	 */  	@Keep -	private void onVideoInit() { +	private boolean onVideoInit() {  		final Activity activity = getActivity();  		containerLayout = new FrameLayout(activity);  		containerLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); @@ -262,13 +268,17 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC  		// ...add to FrameLayout  		containerLayout.addView(editText); -		GodotLib.setup(command_line); +		if (!GodotLib.setup(command_line)) { +			Log.e(TAG, "Unable to setup the Godot engine! Aborting..."); +			alert(R.string.error_engine_setup_message, R.string.text_error_title, this::forceQuit); +			return false; +		} -		final String videoDriver = GodotLib.getGlobal("rendering/driver/driver_name"); -		if (videoDriver.equals("vulkan")) { -			mRenderView = new GodotVulkanRenderView(activity, this); -		} else { +		final String renderer = GodotLib.getGlobal("rendering/renderer/rendering_method"); +		if (renderer.equals("gl_compatibility")) {  			mRenderView = new GodotGLRenderView(activity, this, xrMode, use_debug_opengl); +		} else { +			mRenderView = new GodotVulkanRenderView(activity, this);  		}  		View view = mRenderView.getView(); @@ -303,6 +313,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC  				}  			}  		} +		return true;  	}  	public void setKeepScreenOn(final boolean p_enabled) { @@ -344,13 +355,27 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC  	}  	public void alert(final String message, final String title) { +		alert(message, title, null); +	} + +	private void alert(@StringRes int messageResId, @StringRes int titleResId, @Nullable Runnable okCallback) { +		Resources res = getResources(); +		alert(res.getString(messageResId), res.getString(titleResId), okCallback); +	} + +	private void alert(final String message, final String title, @Nullable Runnable okCallback) {  		final Activity activity = getActivity();  		runOnUiThread(() -> {  			AlertDialog.Builder builder = new AlertDialog.Builder(activity);  			builder.setMessage(message).setTitle(title);  			builder.setPositiveButton(  					"OK", -					(dialog, id) -> dialog.cancel()); +					(dialog, id) -> { +						if (okCallback != null) { +							okCallback.run(); +						} +						dialog.cancel(); +					});  			AlertDialog dialog = builder.create();  			dialog.show();  		}); @@ -471,7 +496,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC  		mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);  		mGyroscope = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); -		GodotLib.initialize(activity, +		godot_initialized = GodotLib.initialize(activity,  				this,  				activity.getAssets(),  				io, @@ -482,8 +507,6 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC  				tts);  		result_callback = null; - -		godot_initialized = true;  	}  	@Override @@ -1023,7 +1046,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC  	}  	@Keep -	private GodotRenderView getRenderView() { // used by native side to get renderView +	public GodotRenderView getRenderView() { // used by native side to get renderView  		return mRenderView;  	} diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java index 08da1b1832..3dfc37f6b0 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java @@ -31,7 +31,6 @@  package org.godotengine.godot;  import org.godotengine.godot.gl.GLSurfaceView;  import org.godotengine.godot.gl.GodotRenderer; -import org.godotengine.godot.input.GodotGestureHandler;  import org.godotengine.godot.input.GodotInputHandler;  import org.godotengine.godot.utils.GLUtils;  import org.godotengine.godot.xr.XRMode; @@ -46,7 +45,6 @@ import android.annotation.SuppressLint;  import android.content.Context;  import android.graphics.PixelFormat;  import android.os.Build; -import android.view.GestureDetector;  import android.view.KeyEvent;  import android.view.MotionEvent;  import android.view.PointerIcon; @@ -75,9 +73,7 @@ import androidx.annotation.Keep;  public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView {  	private final Godot godot;  	private final GodotInputHandler inputHandler; -	private final GestureDetector detector;  	private final GodotRenderer godotRenderer; -	private PointerIcon pointerIcon;  	public GodotGLRenderView(Context context, Godot godot, XRMode xrMode, boolean p_use_debug_opengl) {  		super(context); @@ -85,10 +81,9 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView  		this.godot = godot;  		this.inputHandler = new GodotInputHandler(this); -		this.detector = new GestureDetector(context, new GodotGestureHandler(this));  		this.godotRenderer = new GodotRenderer();  		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { -			pointerIcon = PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT); +			setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT));  		}  		init(xrMode, false);  	} @@ -132,7 +127,6 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView  	@Override  	public boolean onTouchEvent(MotionEvent event) {  		super.onTouchEvent(event); -		this.detector.onTouchEvent(event);  		return inputHandler.onTouchEvent(event);  	} @@ -156,19 +150,40 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView  		return inputHandler.onGenericMotionEvent(event);  	} +	@Override +	public void onPointerCaptureChange(boolean hasCapture) { +		super.onPointerCaptureChange(hasCapture); +		inputHandler.onPointerCaptureChange(hasCapture); +	} + +	@Override +	public void requestPointerCapture() { +		super.requestPointerCapture(); +		inputHandler.onPointerCaptureChange(true); +	} + +	@Override +	public void releasePointerCapture() { +		super.releasePointerCapture(); +		inputHandler.onPointerCaptureChange(false); +	} +  	/**  	 * called from JNI to change pointer icon  	 */  	@Keep  	public void setPointerIcon(int pointerType) {  		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { -			pointerIcon = PointerIcon.getSystemIcon(getContext(), pointerType); +			setPointerIcon(PointerIcon.getSystemIcon(getContext(), pointerType));  		}  	}  	@Override  	public PointerIcon onResolvePointerIcon(MotionEvent me, int pointerIndex) { -		return pointerIcon; +		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { +			return getPointerIcon(); +		} +		return super.onResolvePointerIcon(me, pointerIndex);  	}  	private void init(XRMode xrMode, boolean translucent) { diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java index 0434efdf4c..d283de8ce8 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java @@ -203,9 +203,10 @@ public class GodotIO {  		return result;  	} -	public void showKeyboard(String p_existing_text, boolean p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end) { -		if (edit != null) -			edit.showKeyboard(p_existing_text, p_multiline, p_max_input_length, p_cursor_start, p_cursor_end); +	public void showKeyboard(String p_existing_text, int p_type, int p_max_input_length, int p_cursor_start, int p_cursor_end) { +		if (edit != null) { +			edit.showKeyboard(p_existing_text, GodotEditText.VirtualKeyboardType.values()[p_type], p_max_input_length, p_cursor_start, p_cursor_end); +		}  		//InputMethodManager inputMgr = (InputMethodManager)activity.getSystemService(Context.INPUT_METHOD_SERVICE);  		//inputMgr.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java index e2ae62d9cf..33896ecb95 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -54,7 +54,7 @@ public class GodotLib {  	/**  	 * Invoked on the main thread to initialize Godot native layer.  	 */ -	public static native void initialize(Activity activity, +	public static native boolean initialize(Activity activity,  			Godot p_instance,  			AssetManager p_asset_manager,  			GodotIO godotIO, @@ -74,7 +74,7 @@ public class GodotLib {  	 * Invoked on the GL thread to complete setup for the Godot native layer logic.  	 * @param p_cmdline Command line arguments used to configure Godot native layer components.  	 */ -	public static native void setup(String[] p_cmdline); +	public static native boolean setup(String[] p_cmdline);  	/**  	 * Invoked on the GL thread when the underlying Android surface has changed size. @@ -92,7 +92,7 @@ public class GodotLib {  	public static native void newcontext(Surface p_surface);  	/** -	 * Forward {@link Activity#onBackPressed()} event from the main thread to the GL thread. +	 * Forward {@link Activity#onBackPressed()} event.  	 */  	public static native void back(); @@ -108,63 +108,60 @@ public class GodotLib {  	public static native void ttsCallback(int event, int id, int pos);  	/** -	 * Forward touch events from the main thread to the GL thread. +	 * Forward touch events.  	 */ -	public static native void touch(int inputDevice, int event, int pointer, int pointerCount, float[] positions); -	public static native void touch(int inputDevice, int event, int pointer, int pointerCount, float[] positions, int buttonsMask); -	public static native void touch(int inputDevice, int event, int pointer, int pointerCount, float[] positions, int buttonsMask, float verticalFactor, float horizontalFactor); +	public static native void dispatchTouchEvent(int event, int pointer, int pointerCount, float[] positions, boolean doubleTap);  	/** -	 * Forward hover events from the main thread to the GL thread. +	 * Dispatch mouse events  	 */ -	public static native void hover(int type, float x, float y); +	public static native void dispatchMouseEvent(int event, int buttonMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative); -	/** -	 * Forward double_tap events from the main thread to the GL thread. -	 */ -	public static native void doubleTap(int buttonMask, int x, int y); +	public static native void magnify(float x, float y, float factor); + +	public static native void pan(float x, float y, float deltaX, float deltaY);  	/** -	 * Forward accelerometer sensor events from the main thread to the GL thread. +	 * Forward accelerometer sensor events.  	 * @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent)  	 */  	public static native void accelerometer(float x, float y, float z);  	/** -	 * Forward gravity sensor events from the main thread to the GL thread. +	 * Forward gravity sensor events.  	 * @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent)  	 */  	public static native void gravity(float x, float y, float z);  	/** -	 * Forward magnetometer sensor events from the main thread to the GL thread. +	 * Forward magnetometer sensor events.  	 * @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent)  	 */  	public static native void magnetometer(float x, float y, float z);  	/** -	 * Forward gyroscope sensor events from the main thread to the GL thread. +	 * Forward gyroscope sensor events.  	 * @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent)  	 */  	public static native void gyroscope(float x, float y, float z);  	/** -	 * Forward regular key events from the main thread to the GL thread. +	 * Forward regular key events.  	 */ -	public static native void key(int p_keycode, int p_scancode, int p_unicode_char, boolean p_pressed); +	public static native void key(int p_keycode, int p_physical_keycode, int p_unicode, boolean p_pressed);  	/** -	 * Forward game device's key events from the main thread to the GL thread. +	 * Forward game device's key events.  	 */  	public static native void joybutton(int p_device, int p_but, boolean p_pressed);  	/** -	 * Forward joystick devices axis motion events from the main thread to the GL thread. +	 * Forward joystick devices axis motion events.  	 */  	public static native void joyaxis(int p_device, int p_axis, float p_value);  	/** -	 * Forward joystick devices hat motion events from the main thread to the GL thread. +	 * Forward joystick devices hat motion events.  	 */  	public static native void joyhat(int p_device, int p_hat_x, int p_hat_y); diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java index c386a2d2eb..0becf00d93 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java @@ -30,7 +30,6 @@  package org.godotengine.godot; -import org.godotengine.godot.input.GodotGestureHandler;  import org.godotengine.godot.input.GodotInputHandler;  import org.godotengine.godot.vulkan.VkRenderer;  import org.godotengine.godot.vulkan.VkSurfaceView; @@ -38,7 +37,6 @@ import org.godotengine.godot.vulkan.VkSurfaceView;  import android.annotation.SuppressLint;  import android.content.Context;  import android.os.Build; -import android.view.GestureDetector;  import android.view.KeyEvent;  import android.view.MotionEvent;  import android.view.PointerIcon; @@ -49,19 +47,16 @@ import androidx.annotation.Keep;  public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView {  	private final Godot godot;  	private final GodotInputHandler mInputHandler; -	private final GestureDetector mGestureDetector;  	private final VkRenderer mRenderer; -	private PointerIcon pointerIcon;  	public GodotVulkanRenderView(Context context, Godot godot) {  		super(context);  		this.godot = godot;  		mInputHandler = new GodotInputHandler(this); -		mGestureDetector = new GestureDetector(context, new GodotGestureHandler(this));  		mRenderer = new VkRenderer();  		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { -			pointerIcon = PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT); +			setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT));  		}  		setFocusableInTouchMode(true);  		startRenderer(mRenderer); @@ -106,7 +101,6 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV  	@Override  	public boolean onTouchEvent(MotionEvent event) {  		super.onTouchEvent(event); -		mGestureDetector.onTouchEvent(event);  		return mInputHandler.onTouchEvent(event);  	} @@ -130,19 +124,40 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV  		return mInputHandler.onGenericMotionEvent(event);  	} +	@Override +	public void requestPointerCapture() { +		super.requestPointerCapture(); +		mInputHandler.onPointerCaptureChange(true); +	} + +	@Override +	public void releasePointerCapture() { +		super.releasePointerCapture(); +		mInputHandler.onPointerCaptureChange(false); +	} + +	@Override +	public void onPointerCaptureChange(boolean hasCapture) { +		super.onPointerCaptureChange(hasCapture); +		mInputHandler.onPointerCaptureChange(hasCapture); +	} +  	/**  	 * called from JNI to change pointer icon  	 */  	@Keep  	public void setPointerIcon(int pointerType) {  		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { -			pointerIcon = PointerIcon.getSystemIcon(getContext(), pointerType); +			setPointerIcon(PointerIcon.getSystemIcon(getContext(), pointerType));  		}  	}  	@Override  	public PointerIcon onResolvePointerIcon(MotionEvent me, int pointerIndex) { -		return pointerIcon; +		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { +			return getPointerIcon(); +		} +		return super.onResolvePointerIcon(me, pointerIndex);  	}  	@Override diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java index ecb2af0a7b..7925b54fc4 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java @@ -52,6 +52,18 @@ public class GodotEditText extends EditText {  	private final static int HANDLER_OPEN_IME_KEYBOARD = 2;  	private final static int HANDLER_CLOSE_IME_KEYBOARD = 3; +	// Enum must be kept up-to-date with DisplayServer::VirtualKeyboardType +	public enum VirtualKeyboardType { +		KEYBOARD_TYPE_DEFAULT, +		KEYBOARD_TYPE_MULTILINE, +		KEYBOARD_TYPE_NUMBER, +		KEYBOARD_TYPE_NUMBER_DECIMAL, +		KEYBOARD_TYPE_PHONE, +		KEYBOARD_TYPE_EMAIL_ADDRESS, +		KEYBOARD_TYPE_PASSWORD, +		KEYBOARD_TYPE_URL +	} +  	// ===========================================================  	// Fields  	// =========================================================== @@ -60,7 +72,7 @@ public class GodotEditText extends EditText {  	private EditHandler sHandler = new EditHandler(this);  	private String mOriginText;  	private int mMaxInputLength = Integer.MAX_VALUE; -	private boolean mMultiline = false; +	private VirtualKeyboardType mKeyboardType = VirtualKeyboardType.KEYBOARD_TYPE_DEFAULT;  	private static class EditHandler extends Handler {  		private final WeakReference<GodotEditText> mEdit; @@ -100,8 +112,8 @@ public class GodotEditText extends EditText {  		setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_ACTION_DONE);  	} -	public boolean isMultiline() { -		return mMultiline; +	public VirtualKeyboardType getKeyboardType() { +		return mKeyboardType;  	}  	private void handleMessage(final Message msg) { @@ -122,8 +134,31 @@ public class GodotEditText extends EditText {  					}  					int inputType = InputType.TYPE_CLASS_TEXT; -					if (edit.isMultiline()) { -						inputType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE; +					switch (edit.getKeyboardType()) { +						case KEYBOARD_TYPE_DEFAULT: +							inputType = InputType.TYPE_CLASS_TEXT; +							break; +						case KEYBOARD_TYPE_MULTILINE: +							inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE; +							break; +						case KEYBOARD_TYPE_NUMBER: +							inputType = InputType.TYPE_CLASS_NUMBER; +							break; +						case KEYBOARD_TYPE_NUMBER_DECIMAL: +							inputType = InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED; +							break; +						case KEYBOARD_TYPE_PHONE: +							inputType = InputType.TYPE_CLASS_PHONE; +							break; +						case KEYBOARD_TYPE_EMAIL_ADDRESS: +							inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; +							break; +						case KEYBOARD_TYPE_PASSWORD: +							inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD; +							break; +						case KEYBOARD_TYPE_URL: +							inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI; +							break;  					}  					edit.setInputType(inputType); @@ -201,7 +236,7 @@ public class GodotEditText extends EditText {  	// ===========================================================  	// Methods  	// =========================================================== -	public void showKeyboard(String p_existing_text, boolean p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end) { +	public void showKeyboard(String p_existing_text, VirtualKeyboardType p_type, int p_max_input_length, int p_cursor_start, int p_cursor_end) {  		int maxInputLength = (p_max_input_length <= 0) ? Integer.MAX_VALUE : p_max_input_length;  		if (p_cursor_start == -1) { // cursor position not given  			this.mOriginText = p_existing_text; @@ -214,7 +249,7 @@ public class GodotEditText extends EditText {  			this.mMaxInputLength = maxInputLength - (p_existing_text.length() - p_cursor_end);  		} -		this.mMultiline = p_multiline; +		this.mKeyboardType = p_type;  		final Message msg = new Message();  		msg.what = HANDLER_OPEN_IME_KEYBOARD; diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.java deleted file mode 100644 index 778efa914a..0000000000 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.java +++ /dev/null @@ -1,87 +0,0 @@ -/*************************************************************************/ -/*  GodotGestureHandler.java                                             */ -/*************************************************************************/ -/*                       This file is part of:                           */ -/*                           GODOT ENGINE                                */ -/*                      https://godotengine.org                          */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */ -/*                                                                       */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the       */ -/* "Software"), to deal in the Software without restriction, including   */ -/* without limitation the rights to use, copy, modify, merge, publish,   */ -/* distribute, sublicense, and/or sell copies of the Software, and to    */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions:                                             */ -/*                                                                       */ -/* The above copyright notice and this permission notice shall be        */ -/* included in all copies or substantial portions of the Software.       */ -/*                                                                       */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */ -/*************************************************************************/ - -package org.godotengine.godot.input; - -import org.godotengine.godot.GodotLib; -import org.godotengine.godot.GodotRenderView; - -import android.view.GestureDetector; -import android.view.MotionEvent; - -/** - * Handles gesture input related events for the {@link GodotRenderView} view. - * https://developer.android.com/reference/android/view/GestureDetector.SimpleOnGestureListener - */ -public class GodotGestureHandler extends GestureDetector.SimpleOnGestureListener { -	private final GodotRenderView mRenderView; - -	public GodotGestureHandler(GodotRenderView godotView) { -		mRenderView = godotView; -	} - -	private void queueEvent(Runnable task) { -		mRenderView.queueOnRenderThread(task); -	} - -	@Override -	public boolean onDown(MotionEvent event) { -		super.onDown(event); -		//Log.i("GodotGesture", "onDown"); -		return true; -	} - -	@Override -	public boolean onSingleTapConfirmed(MotionEvent event) { -		super.onSingleTapConfirmed(event); -		return true; -	} - -	@Override -	public void onLongPress(MotionEvent event) { -		//Log.i("GodotGesture", "onLongPress"); -	} - -	@Override -	public boolean onDoubleTap(MotionEvent event) { -		//Log.i("GodotGesture", "onDoubleTap"); -		final int x = Math.round(event.getX()); -		final int y = Math.round(event.getY()); -		final int buttonMask = event.getButtonState(); -		GodotLib.doubleTap(buttonMask, x, y); -		return true; -	} - -	@Override -	public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) { -		//Log.i("GodotGesture", "onFling"); -		return true; -	} -} diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt new file mode 100644 index 0000000000..a7a57621de --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt @@ -0,0 +1,276 @@ +/*************************************************************************/ +/*  GodotGestureHandler.kt                                               */ +/*************************************************************************/ +/*                       This file is part of:                           */ +/*                           GODOT ENGINE                                */ +/*                      https://godotengine.org                          */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */ +/*                                                                       */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the       */ +/* "Software"), to deal in the Software without restriction, including   */ +/* without limitation the rights to use, copy, modify, merge, publish,   */ +/* distribute, sublicense, and/or sell copies of the Software, and to    */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions:                                             */ +/*                                                                       */ +/* The above copyright notice and this permission notice shall be        */ +/* included in all copies or substantial portions of the Software.       */ +/*                                                                       */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */ +/*************************************************************************/ + +package org.godotengine.godot.input + +import android.os.Build +import android.view.GestureDetector.SimpleOnGestureListener +import android.view.InputDevice +import android.view.MotionEvent +import android.view.ScaleGestureDetector +import android.view.ScaleGestureDetector.OnScaleGestureListener +import org.godotengine.godot.GodotLib + +/** + * Handles regular and scale gesture input related events for the [GodotView] view. + * + * @See https://developer.android.com/reference/android/view/GestureDetector.SimpleOnGestureListener + * @See https://developer.android.com/reference/android/view/ScaleGestureDetector.OnScaleGestureListener + */ +internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureListener { + +	companion object { +		private val TAG = GodotGestureHandler::class.java.simpleName +	} + +	/** +	 * Enable pan and scale gestures +	 */ +	var panningAndScalingEnabled = false + +	private var nextDownIsDoubleTap = false +	private var dragInProgress = false +	private var scaleInProgress = false +	private var contextClickInProgress = false +	private var pointerCaptureInProgress = false + +	override fun onDown(event: MotionEvent): Boolean { +		GodotInputHandler.handleMotionEvent(event.source, MotionEvent.ACTION_DOWN, event.buttonState, event.x, event.y, nextDownIsDoubleTap) +		nextDownIsDoubleTap = false +		return true +	} + +	override fun onSingleTapUp(event: MotionEvent): Boolean { +		GodotInputHandler.handleMotionEvent(event) +		return true +	} + +	override fun onLongPress(event: MotionEvent) { +		contextClickRouter(event) +	} + +	private fun contextClickRouter(event: MotionEvent) { +		if (scaleInProgress) { +			return +		} + +		// Cancel the previous down event +		GodotInputHandler.handleMotionEvent( +			event.source, +			MotionEvent.ACTION_CANCEL, +			event.buttonState, +			event.x, +			event.y +		) + +		// Turn a context click into a single tap right mouse button click. +		GodotInputHandler.handleMouseEvent( +			MotionEvent.ACTION_DOWN, +			MotionEvent.BUTTON_SECONDARY, +			event.x, +			event.y +		) +		contextClickInProgress = true +	} + +	fun onPointerCaptureChange(hasCapture: Boolean) { +		if (pointerCaptureInProgress == hasCapture) { +			return +		} + +		if (!hasCapture) { +			// Dispatch a mouse relative ACTION_UP event to signal the end of the capture +			GodotInputHandler.handleMouseEvent( +				MotionEvent.ACTION_UP, +				0, +				0f, +				0f, +				0f, +				0f, +				false, +				true +			) +		} +		pointerCaptureInProgress = hasCapture +	} + +	fun onMotionEvent(event: MotionEvent): Boolean { +		return when (event.actionMasked) { +			MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_BUTTON_RELEASE -> { +				onActionUp(event) +			} +			MotionEvent.ACTION_MOVE -> { +				onActionMove(event) +			} +			else -> false +		} +	} + +	private fun onActionUp(event: MotionEvent): Boolean { +		val sourceMouseRelative = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { +			event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE) +		} else { +			false +		} +		when { +			pointerCaptureInProgress -> { +				return if (event.actionMasked == MotionEvent.ACTION_CANCEL) { +					// Don't dispatch the ACTION_CANCEL while a capture is in progress +					true +				} else { +					GodotInputHandler.handleMouseEvent( +						MotionEvent.ACTION_UP, +						event.buttonState, +						event.x, +						event.y, +						0f, +						0f, +						false, +						sourceMouseRelative +					) +					pointerCaptureInProgress = false +					true +				} +			} +			dragInProgress -> { +				GodotInputHandler.handleMotionEvent(event) +				dragInProgress = false +				return true +			} +			contextClickInProgress -> { +				GodotInputHandler.handleMouseEvent( +					event.actionMasked, +					0, +					event.x, +					event.y, +					0f, +					0f, +					false, +					sourceMouseRelative +				) +				contextClickInProgress = false +				return true +			} +			else -> return false +		} +	} + +	private fun onActionMove(event: MotionEvent): Boolean { +		if (contextClickInProgress) { +			val sourceMouseRelative = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { +				event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE) +			} else { +				false +			} +			GodotInputHandler.handleMouseEvent( +				event.actionMasked, +				MotionEvent.BUTTON_SECONDARY, +				event.x, +				event.y, +				0f, +				0f, +				false, +				sourceMouseRelative +			) +			return true +		} +		return false +	} + +	override fun onDoubleTapEvent(event: MotionEvent): Boolean { +		if (event.actionMasked == MotionEvent.ACTION_UP) { +			nextDownIsDoubleTap = false +			GodotInputHandler.handleMotionEvent(event) +		} +		return true +	} + +	override fun onDoubleTap(event: MotionEvent): Boolean { +		nextDownIsDoubleTap = true +		return true +	} + +	override fun onScroll( +		originEvent: MotionEvent, +		terminusEvent: MotionEvent, +		distanceX: Float, +		distanceY: Float +	): Boolean { +		if (scaleInProgress) { +			if (dragInProgress) { +				// Cancel the drag +				GodotInputHandler.handleMotionEvent( +					originEvent.source, +					MotionEvent.ACTION_CANCEL, +					originEvent.buttonState, +					originEvent.x, +					originEvent.y +				) +				dragInProgress = false +			} +			return true +		} + +		dragInProgress = true + +		val x = terminusEvent.x +		val y = terminusEvent.y +		if (terminusEvent.pointerCount >= 2 && panningAndScalingEnabled) { +			GodotLib.pan(x, y, distanceX / 5f, distanceY / 5f) +		} else { +			GodotInputHandler.handleMotionEvent(terminusEvent) +		} +		return true +	} + +	override fun onScale(detector: ScaleGestureDetector?): Boolean { +		if (detector == null || !panningAndScalingEnabled) { +			return false +		} +		GodotLib.magnify( +			detector.focusX, +			detector.focusY, +			detector.scaleFactor +		) +		return true +	} + +	override fun onScaleBegin(detector: ScaleGestureDetector?): Boolean { +		if (detector == null || !panningAndScalingEnabled) { +			return false +		} +		scaleInProgress = true +		return true +	} + +	override fun onScaleEnd(detector: ScaleGestureDetector?) { +		scaleInProgress = false +	} +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java index ccfb865b1a..d2f3c5aed2 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java @@ -41,13 +41,13 @@ import android.os.Build;  import android.util.Log;  import android.util.SparseArray;  import android.util.SparseIntArray; +import android.view.GestureDetector;  import android.view.InputDevice; -import android.view.InputDevice.MotionRange;  import android.view.KeyEvent;  import android.view.MotionEvent; +import android.view.ScaleGestureDetector;  import java.util.Collections; -import java.util.Comparator;  import java.util.HashSet;  import java.util.Set; @@ -55,21 +55,49 @@ import java.util.Set;   * Handles input related events for the {@link GodotRenderView} view.   */  public class GodotInputHandler implements InputManager.InputDeviceListener { -	private final GodotRenderView mRenderView; -	private final InputManager mInputManager; - -	private final String tag = this.getClass().getSimpleName(); +	private static final String TAG = GodotInputHandler.class.getSimpleName();  	private final SparseIntArray mJoystickIds = new SparseIntArray(4);  	private final SparseArray<Joystick> mJoysticksDevices = new SparseArray<>(4); +	private final GodotRenderView mRenderView; +	private final InputManager mInputManager; +	private final GestureDetector gestureDetector; +	private final ScaleGestureDetector scaleGestureDetector; +	private final GodotGestureHandler godotGestureHandler; +  	public GodotInputHandler(GodotRenderView godotView) { +		final Context context = godotView.getView().getContext();  		mRenderView = godotView; -		mInputManager = (InputManager)mRenderView.getView().getContext().getSystemService(Context.INPUT_SERVICE); +		mInputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE);  		mInputManager.registerInputDeviceListener(this, null); + +		this.godotGestureHandler = new GodotGestureHandler(); +		this.gestureDetector = new GestureDetector(context, godotGestureHandler); +		this.gestureDetector.setIsLongpressEnabled(false); +		this.scaleGestureDetector = new ScaleGestureDetector(context, godotGestureHandler); +		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { +			this.scaleGestureDetector.setStylusScaleEnabled(true); +		}  	} -	private boolean isKeyEvent_GameDevice(int source) { +	/** +	 * Enable long press events. This is false by default. +	 */ +	public void enableLongPress(boolean enable) { +		this.gestureDetector.setIsLongpressEnabled(enable); +	} + +	/** +	 * Enable multi-fingers pan & scale gestures. This is false by default. +	 * +	 * Note: This may interfere with multi-touch handling / support. +	 */ +	public void enablePanningAndScalingGestures(boolean enable) { +		this.godotGestureHandler.setPanningAndScalingEnabled(enable); +	} + +	private boolean isKeyEventGameDevice(int source) {  		// Note that keyboards are often (SOURCE_KEYBOARD | SOURCE_DPAD)  		if (source == (InputDevice.SOURCE_KEYBOARD | InputDevice.SOURCE_DPAD))  			return false; @@ -77,6 +105,10 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {  		return (source & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK || (source & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD || (source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD;  	} +	public void onPointerCaptureChange(boolean hasCapture) { +		godotGestureHandler.onPointerCaptureChange(hasCapture); +	} +  	public boolean onKeyUp(final int keyCode, KeyEvent event) {  		if (keyCode == KeyEvent.KEYCODE_BACK) {  			return true; @@ -87,7 +119,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {  		}  		int source = event.getSource(); -		if (isKeyEvent_GameDevice(source)) { +		if (isKeyEventGameDevice(source)) {  			// Check if the device exists  			final int deviceId = event.getDeviceId();  			if (mJoystickIds.indexOfKey(deviceId) >= 0) { @@ -96,10 +128,14 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {  				GodotLib.joybutton(godotJoyId, button, false);  			}  		} else { -			final int scanCode = event.getScanCode(); -			final int chr = event.getUnicodeChar(0); -			GodotLib.key(keyCode, scanCode, chr, false); -		} +			// getKeyCode(): The physical key that was pressed. +			// Godot's keycodes match the ASCII codes, so for single byte unicode characters, +			// we can use the unmodified unicode character to determine Godot's keycode. +			final int keycode = event.getUnicodeChar(0); +			final int physical_keycode = event.getKeyCode(); +			final int unicode = event.getUnicodeChar(); +			GodotLib.key(keycode, physical_keycode, unicode, false); +		};  		return true;  	} @@ -117,11 +153,10 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {  		}  		int source = event.getSource(); -		//Log.e(TAG, String.format("Key down! source %d, device %d, joystick %d, %d, %d", event.getDeviceId(), source, (source & InputDevice.SOURCE_JOYSTICK), (source & InputDevice.SOURCE_DPAD), (source & InputDevice.SOURCE_GAMEPAD)));  		final int deviceId = event.getDeviceId();  		// Check if source is a game device and that the device is a registered gamepad -		if (isKeyEvent_GameDevice(source)) { +		if (isKeyEventGameDevice(source)) {  			if (event.getRepeatCount() > 0) // ignore key echo  				return true; @@ -131,56 +166,51 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {  				GodotLib.joybutton(godotJoyId, button, true);  			}  		} else { -			final int scanCode = event.getScanCode(); -			final int chr = event.getUnicodeChar(0); -			GodotLib.key(keyCode, scanCode, chr, true); +			final int keycode = event.getUnicodeChar(0); +			final int physical_keycode = event.getKeyCode(); +			final int unicode = event.getUnicodeChar(); +			GodotLib.key(keycode, physical_keycode, unicode, true);  		}  		return true;  	}  	public boolean onTouchEvent(final MotionEvent event) { -		// Mouse drag (mouse pressed and move) doesn't fire onGenericMotionEvent so this is needed -		if (event.isFromSource(InputDevice.SOURCE_MOUSE)) { -			if (event.getAction() != MotionEvent.ACTION_MOVE) { -				// we return true because every time a mouse event is fired, the event is already handled -				// in onGenericMotionEvent, so by touch event we can say that the event is also handled -				return true; -			} -			return handleMouseEvent(event); +		this.scaleGestureDetector.onTouchEvent(event); +		if (this.gestureDetector.onTouchEvent(event)) { +			// The gesture detector has handled the event. +			return true;  		} -		final int evcount = event.getPointerCount(); -		if (evcount == 0) +		if (godotGestureHandler.onMotionEvent(event)) { +			// The gesture handler has handled the event.  			return true; +		} -		if (mRenderView != null) { -			final float[] arr = new float[event.getPointerCount() * 3]; // pointerId1, x1, y1, pointerId2, etc... +		// Drag events are handled by the [GodotGestureHandler] +		if (event.getActionMasked() == MotionEvent.ACTION_MOVE) { +			return true; +		} -			for (int i = 0; i < event.getPointerCount(); i++) { -				arr[i * 3 + 0] = event.getPointerId(i); -				arr[i * 3 + 1] = event.getX(i); -				arr[i * 3 + 2] = event.getY(i); -			} -			final int action = event.getActionMasked(); -			final int pointer_idx = event.getPointerId(event.getActionIndex()); - -			switch (action) { -				case MotionEvent.ACTION_DOWN: -				case MotionEvent.ACTION_CANCEL: -				case MotionEvent.ACTION_UP: -				case MotionEvent.ACTION_MOVE: -				case MotionEvent.ACTION_POINTER_UP: -				case MotionEvent.ACTION_POINTER_DOWN: { -					GodotLib.touch(event.getSource(), action, pointer_idx, evcount, arr); -				} break; -			} +		if (isMouseEvent(event)) { +			return handleMouseEvent(event);  		} -		return true; + +		return handleTouchEvent(event);  	}  	public boolean onGenericMotionEvent(MotionEvent event) { -		if (event.isFromSource(InputDevice.SOURCE_JOYSTICK) && event.getAction() == MotionEvent.ACTION_MOVE) { +		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && gestureDetector.onGenericMotionEvent(event)) { +			// The gesture detector has handled the event. +			return true; +		} + +		if (godotGestureHandler.onMotionEvent(event)) { +			// The gesture handler has handled the event. +			return true; +		} + +		if (event.isFromSource(InputDevice.SOURCE_JOYSTICK) && event.getActionMasked() == MotionEvent.ACTION_MOVE) {  			// Check if the device exists  			final int deviceId = event.getDeviceId();  			if (mJoystickIds.indexOfKey(deviceId) >= 0) { @@ -193,15 +223,14 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {  				for (int i = 0; i < joystick.axes.size(); i++) {  					final int axis = joystick.axes.get(i);  					final float value = event.getAxisValue(axis); -					/** -					 * As all axes are polled for each event, only fire an axis event if the value has actually changed. -					 * Prevents flooding Godot with repeated events. +					/* +					  As all axes are polled for each event, only fire an axis event if the value has actually changed. +					  Prevents flooding Godot with repeated events.  					 */  					if (joystick.axesValues.indexOfKey(axis) < 0 || (float)joystick.axesValues.get(axis) != value) {  						// save value to prevent repeats  						joystick.axesValues.put(axis, value); -						final int godotAxisIdx = i; -						GodotLib.joyaxis(godotJoyId, godotAxisIdx, value); +						GodotLib.joyaxis(godotJoyId, i, value);  					}  				} @@ -216,17 +245,8 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {  				}  				return true;  			} -		} else if (event.isFromSource(InputDevice.SOURCE_STYLUS)) { -			final float x = event.getX(); -			final float y = event.getY(); -			final int type = event.getAction(); -			GodotLib.hover(type, x, y); -			return true; - -		} else if (event.isFromSource(InputDevice.SOURCE_MOUSE) || event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE)) { -			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { -				return handleMouseEvent(event); -			} +		} else if (isMouseEvent(event)) { +			return handleMouseEvent(event);  		}  		return false; @@ -238,7 +258,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {  		for (int deviceId : deviceIds) {  			InputDevice device = mInputManager.getInputDevice(deviceId);  			if (DEBUG) { -				Log.v("GodotInputHandler", String.format("init() deviceId:%d, Name:%s\n", deviceId, device.getName())); +				Log.v(TAG, String.format("init() deviceId:%d, Name:%s\n", deviceId, device.getName()));  			}  			onInputDeviceAdded(deviceId);  		} @@ -283,13 +303,12 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {  		joystick.name = device.getName();  		//Helps with creating new joypad mappings. -		Log.i(tag, "=== New Input Device: " + joystick.name); +		Log.i(TAG, "=== New Input Device: " + joystick.name);  		Set<Integer> already = new HashSet<>();  		for (InputDevice.MotionRange range : device.getMotionRanges()) {  			boolean isJoystick = range.isFromSource(InputDevice.SOURCE_JOYSTICK);  			boolean isGamepad = range.isFromSource(InputDevice.SOURCE_GAMEPAD); -			//Log.i(tag, "axis: "+range.getAxis()+ ", isJoystick: "+isJoystick+", isGamepad: "+isGamepad);  			if (!isJoystick && !isGamepad) {  				continue;  			} @@ -301,14 +320,14 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {  					already.add(axis);  					joystick.axes.add(axis);  				} else { -					Log.w(tag, " - DUPLICATE AXIS VALUE IN LIST: " + axis); +					Log.w(TAG, " - DUPLICATE AXIS VALUE IN LIST: " + axis);  				}  			}  		}  		Collections.sort(joystick.axes);  		for (int idx = 0; idx < joystick.axes.size(); idx++) {  			//Helps with creating new joypad mappings. -			Log.i(tag, " - Mapping Android axis " + joystick.axes.get(idx) + " to Godot axis " + idx); +			Log.i(TAG, " - Mapping Android axis " + joystick.axes.get(idx) + " to Godot axis " + idx);  		}  		mJoysticksDevices.put(deviceId, joystick); @@ -333,13 +352,6 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {  		onInputDeviceAdded(deviceId);  	} -	private static class RangeComparator implements Comparator<MotionRange> { -		@Override -		public int compare(MotionRange arg0, MotionRange arg1) { -			return arg0.getAxis() - arg1.getAxis(); -		} -	} -  	public static int getGodotButton(int keyCode) {  		int button;  		switch (keyCode) { @@ -405,39 +417,113 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {  		return button;  	} -	private boolean handleMouseEvent(final MotionEvent event) { -		switch (event.getActionMasked()) { +	static boolean isMouseEvent(MotionEvent event) { +		return isMouseEvent(event.getSource()); +	} + +	private static boolean isMouseEvent(int eventSource) { +		boolean mouseSource = ((eventSource & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) || ((eventSource & InputDevice.SOURCE_STYLUS) == InputDevice.SOURCE_STYLUS); +		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { +			mouseSource = mouseSource || ((eventSource & InputDevice.SOURCE_MOUSE_RELATIVE) == InputDevice.SOURCE_MOUSE_RELATIVE); +		} +		return mouseSource; +	} + +	static boolean handleMotionEvent(final MotionEvent event) { +		if (isMouseEvent(event)) { +			return handleMouseEvent(event); +		} + +		return handleTouchEvent(event); +	} + +	static boolean handleMotionEvent(int eventSource, int eventAction, int buttonsMask, float x, float y) { +		return handleMotionEvent(eventSource, eventAction, buttonsMask, x, y, false); +	} + +	static boolean handleMotionEvent(int eventSource, int eventAction, int buttonsMask, float x, float y, boolean doubleTap) { +		return handleMotionEvent(eventSource, eventAction, buttonsMask, x, y, 0, 0, doubleTap); +	} + +	static boolean handleMotionEvent(int eventSource, int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleTap) { +		if (isMouseEvent(eventSource)) { +			return handleMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleTap, false); +		} + +		return handleTouchEvent(eventAction, x, y, doubleTap); +	} + +	static boolean handleMouseEvent(final MotionEvent event) { +		final int eventAction = event.getActionMasked(); +		final float x = event.getX(); +		final float y = event.getY(); +		final int buttonsMask = event.getButtonState(); + +		final float verticalFactor = event.getAxisValue(MotionEvent.AXIS_VSCROLL); +		final float horizontalFactor = event.getAxisValue(MotionEvent.AXIS_HSCROLL); +		boolean sourceMouseRelative = false; +		if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { +			sourceMouseRelative = event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE); +		} +		return handleMouseEvent(eventAction, buttonsMask, x, y, horizontalFactor, verticalFactor, false, sourceMouseRelative); +	} + +	static boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y) { +		return handleMouseEvent(eventAction, buttonsMask, x, y, 0, 0, false, false); +	} + +	static boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative) { +		switch (eventAction) { +			case MotionEvent.ACTION_CANCEL: +			case MotionEvent.ACTION_UP: +				// Zero-up the button state +				buttonsMask = 0; +				// FALL THROUGH +			case MotionEvent.ACTION_DOWN:  			case MotionEvent.ACTION_HOVER_ENTER: +			case MotionEvent.ACTION_HOVER_EXIT:  			case MotionEvent.ACTION_HOVER_MOVE: -			case MotionEvent.ACTION_HOVER_EXIT: { -				final float x = event.getX(); -				final float y = event.getY(); -				final int type = event.getAction(); -				GodotLib.hover(type, x, y); -				return true; -			} -			case MotionEvent.ACTION_BUTTON_PRESS: -			case MotionEvent.ACTION_BUTTON_RELEASE: -			case MotionEvent.ACTION_MOVE: { -				final float x = event.getX(); -				final float y = event.getY(); -				final int buttonsMask = event.getButtonState(); -				final int action = event.getAction(); -				GodotLib.touch(event.getSource(), action, 0, 1, new float[] { 0, x, y }, buttonsMask); -				return true; -			} +			case MotionEvent.ACTION_MOVE:  			case MotionEvent.ACTION_SCROLL: { -				final float x = event.getX(); -				final float y = event.getY(); -				final int buttonsMask = event.getButtonState(); -				final int action = event.getAction(); -				final float verticalFactor = event.getAxisValue(MotionEvent.AXIS_VSCROLL); -				final float horizontalFactor = event.getAxisValue(MotionEvent.AXIS_HSCROLL); -				GodotLib.touch(event.getSource(), action, 0, 1, new float[] { 0, x, y }, buttonsMask, verticalFactor, horizontalFactor); +				GodotLib.dispatchMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative); +				return true;  			} +		} +		return false; +	} + +	static boolean handleTouchEvent(final MotionEvent event) { +		final int pointerCount = event.getPointerCount(); +		if (pointerCount == 0) { +			return true; +		} + +		final float[] positions = new float[pointerCount * 3]; // pointerId1, x1, y1, pointerId2, etc... + +		for (int i = 0; i < pointerCount; i++) { +			positions[i * 3 + 0] = event.getPointerId(i); +			positions[i * 3 + 1] = event.getX(i); +			positions[i * 3 + 2] = event.getY(i); +		} +		final int action = event.getActionMasked(); +		final int actionPointerId = event.getPointerId(event.getActionIndex()); + +		return handleTouchEvent(action, actionPointerId, pointerCount, positions, false); +	} + +	static boolean handleTouchEvent(int eventAction, float x, float y, boolean doubleTap) { +		return handleTouchEvent(eventAction, 0, 1, new float[] { 0, x, y }, doubleTap); +	} + +	static boolean handleTouchEvent(int eventAction, int actionPointerId, int pointerCount, float[] positions, boolean doubleTap) { +		switch (eventAction) {  			case MotionEvent.ACTION_DOWN: -			case MotionEvent.ACTION_UP: { -				// we can safely ignore these cases because they are always come beside ACTION_BUTTON_PRESS and ACTION_BUTTON_RELEASE +			case MotionEvent.ACTION_CANCEL: +			case MotionEvent.ACTION_UP: +			case MotionEvent.ACTION_MOVE: +			case MotionEvent.ACTION_POINTER_UP: +			case MotionEvent.ACTION_POINTER_DOWN: { +				GodotLib.dispatchTouchEvent(eventAction, actionPointerId, pointerCount, positions, doubleTap);  				return true;  			}  		} diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java index e940aafa9e..01ad5ee415 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java @@ -92,11 +92,9 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene  	@Override  	public void beforeTextChanged(final CharSequence pCharSequence, final int start, final int count, final int after) { -		//Log.d(TAG, "beforeTextChanged(" + pCharSequence + ")start: " + start + ",count: " + count + ",after: " + after); -  		for (int i = 0; i < count; ++i) { -			GodotLib.key(KeyEvent.KEYCODE_DEL, KeyEvent.KEYCODE_DEL, 0, true); -			GodotLib.key(KeyEvent.KEYCODE_DEL, KeyEvent.KEYCODE_DEL, 0, false); +			GodotLib.key(0, KeyEvent.KEYCODE_DEL, 0, true); +			GodotLib.key(0, KeyEvent.KEYCODE_DEL, 0, false);  			if (mHasSelection) {  				mHasSelection = false; @@ -107,40 +105,38 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene  	@Override  	public void onTextChanged(final CharSequence pCharSequence, final int start, final int before, final int count) { -		//Log.d(TAG, "onTextChanged(" + pCharSequence + ")start: " + start + ",count: " + count + ",before: " + before); -  		final int[] newChars = new int[count];  		for (int i = start; i < start + count; ++i) {  			newChars[i - start] = pCharSequence.charAt(i);  		}  		for (int i = 0; i < count; ++i) {  			int key = newChars[i]; -			if ((key == '\n') && !mEdit.isMultiline()) { +			if ((key == '\n') && !(mEdit.getKeyboardType() == GodotEditText.VirtualKeyboardType.KEYBOARD_TYPE_MULTILINE)) {  				// Return keys are handled through action events  				continue;  			} -			GodotLib.key(0, 0, key, true); -			GodotLib.key(0, 0, key, false); +			GodotLib.key(key, 0, key, true); +			GodotLib.key(key, 0, key, false);  		}  	}  	@Override  	public boolean onEditorAction(final TextView pTextView, final int pActionID, final KeyEvent pKeyEvent) { -		if (mEdit == pTextView && isFullScreenEdit()) { +		if (mEdit == pTextView && isFullScreenEdit() && pKeyEvent != null) {  			final String characters = pKeyEvent.getCharacters();  			for (int i = 0; i < characters.length(); i++) {  				final int ch = characters.codePointAt(i); -				GodotLib.key(0, 0, ch, true); -				GodotLib.key(0, 0, ch, false); +				GodotLib.key(ch, 0, ch, true); +				GodotLib.key(ch, 0, ch, false);  			}  		}  		if (pActionID == EditorInfo.IME_ACTION_DONE) {  			// Enter key has been pressed  			mRenderView.queueOnRenderThread(() -> { -				GodotLib.key(KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_ENTER, 0, true); -				GodotLib.key(KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_ENTER, 0, false); +				GodotLib.key(0, KeyEvent.KEYCODE_ENTER, 0, true); +				GodotLib.key(0, KeyEvent.KEYCODE_ENTER, 0, false);  			});  			mRenderView.getView().requestFocus();  			return true; diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt b/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt index c7bd55b620..c9282dd247 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt @@ -54,11 +54,19 @@ internal enum class StorageScope {  	 */  	UNKNOWN; -	companion object { +	class Identifier(context: Context) { + +		private val internalAppDir: String? = context.filesDir.canonicalPath +		private val internalCacheDir: String? = context.cacheDir.canonicalPath +		private val externalAppDir: String? = context.getExternalFilesDir(null)?.canonicalPath +		private val sharedDir : String? = Environment.getExternalStorageDirectory().canonicalPath +		private val downloadsSharedDir: String? = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).canonicalPath +		private val documentsSharedDir: String? = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).canonicalPath +  		/**  		 * Determines which [StorageScope] the given path falls under.  		 */ -		fun getStorageScope(context: Context, path: String?): StorageScope { +		fun identifyStorageScope(path: String?): StorageScope {  			if (path == null) {  				return UNKNOWN  			} @@ -70,23 +78,19 @@ internal enum class StorageScope {  			val canonicalPathFile = pathFile.canonicalPath -			val internalAppDir = context.filesDir.canonicalPath ?: return UNKNOWN -			if (canonicalPathFile.startsWith(internalAppDir)) { +			if (internalAppDir != null && canonicalPathFile.startsWith(internalAppDir)) {  				return APP  			} -			val internalCacheDir = context.cacheDir.canonicalPath ?: return UNKNOWN -			if (canonicalPathFile.startsWith(internalCacheDir)) { +			if (internalCacheDir != null && canonicalPathFile.startsWith(internalCacheDir)) {  				return APP  			} -			val externalAppDir = context.getExternalFilesDir(null)?.canonicalPath ?: return UNKNOWN -			if (canonicalPathFile.startsWith(externalAppDir)) { +			if (externalAppDir != null && canonicalPathFile.startsWith(externalAppDir)) {  				return APP  			} -			val sharedDir =	Environment.getExternalStorageDirectory().canonicalPath ?: return UNKNOWN -			if (canonicalPathFile.startsWith(sharedDir)) { +			if (sharedDir != null && canonicalPathFile.startsWith(sharedDir)) {  				if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {  					// Before R, apps had access to shared storage so long as they have the right  					// permissions (and flag on Q). @@ -95,13 +99,8 @@ internal enum class StorageScope {  				// Post R, access is limited based on the target destination  				// 'Downloads' and 'Documents' are still accessible -				val downloadsSharedDir = -					Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).canonicalPath -						?: return SHARED -				val documentsSharedDir = -					Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).canonicalPath -						?: return SHARED -				if (canonicalPathFile.startsWith(downloadsSharedDir) || canonicalPathFile.startsWith(documentsSharedDir)) { +				if ((downloadsSharedDir != null && canonicalPathFile.startsWith(downloadsSharedDir)) +					|| (documentsSharedDir != null && canonicalPathFile.startsWith(documentsSharedDir))) {  					return APP  				} diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt index c3acf42568..54fc56fa3e 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt @@ -54,6 +54,7 @@ internal class FilesystemDirectoryAccess(private val context: Context):  	private data class DirData(val dirFile: File, val files: Array<File>, var current: Int = 0) +	private val storageScopeIdentifier = StorageScope.Identifier(context)  	private val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager  	private var lastDirId = STARTING_DIR_ID  	private val dirs = SparseArray<DirData>() @@ -62,7 +63,7 @@ internal class FilesystemDirectoryAccess(private val context: Context):  		// Directory access is available for shared storage on Android 11+  		// On Android 10, access is also available as long as the `requestLegacyExternalStorage`  		// tag is available. -		return StorageScope.getStorageScope(context, path) != StorageScope.UNKNOWN +		return storageScopeIdentifier.identifyStorageScope(path) != StorageScope.UNKNOWN  	}  	override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0 @@ -102,7 +103,7 @@ internal class FilesystemDirectoryAccess(private val context: Context):  		}  	} -	override fun fileExists(path: String) = FileAccessHandler.fileExists(context, path) +	override fun fileExists(path: String) = FileAccessHandler.fileExists(context, storageScopeIdentifier, path)  	override fun dirNext(dirId: Int): String {  		val dirData = dirs[dirId] @@ -199,7 +200,7 @@ internal class FilesystemDirectoryAccess(private val context: Context):  			if (fromFile.isDirectory) {  				fromFile.renameTo(File(to))  			} else { -				FileAccessHandler.renameFile(context, from, to) +				FileAccessHandler.renameFile(context, storageScopeIdentifier, from, to)  			}  		} catch (e: SecurityException) {  			false @@ -218,7 +219,7 @@ internal class FilesystemDirectoryAccess(private val context: Context):  				if (deleteFile.isDirectory) {  					deleteFile.delete()  				} else { -					FileAccessHandler.removeFile(context, filename) +					FileAccessHandler.removeFile(context, storageScopeIdentifier, filename)  				}  			} else {  				true diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt index aef1bed8ce..f23537a29e 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt @@ -104,7 +104,6 @@ internal abstract class DataAccess(private val filePath: String) {  	protected abstract val fileChannel: FileChannel  	internal var endOfFile = false -		private set  	fun close() {  		try { @@ -125,9 +124,7 @@ internal abstract class DataAccess(private val filePath: String) {  	fun seek(position: Long) {  		try {  			fileChannel.position(position) -			if (position <= size()) { -				endOfFile = false -			} +			endOfFile = position >= fileChannel.size()  		} catch (e: Exception) {  			Log.w(TAG, "Exception when seeking file $filePath.", e)  		} @@ -161,8 +158,8 @@ internal abstract class DataAccess(private val filePath: String) {  	fun read(buffer: ByteBuffer): Int {  		return try {  			val readBytes = fileChannel.read(buffer) +			endOfFile = readBytes == -1 || (fileChannel.position() >= fileChannel.size())  			if (readBytes == -1) { -				endOfFile = true  				0  			} else {  				readBytes diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt index a4e0a82d6e..83da3a24b3 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt @@ -49,8 +49,8 @@ class FileAccessHandler(val context: Context) {  		private const val INVALID_FILE_ID = 0  		private const val STARTING_FILE_ID = 1 -		fun fileExists(context: Context, path: String?): Boolean { -			val storageScope = StorageScope.getStorageScope(context, path) +		internal fun fileExists(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean { +			val storageScope = storageScopeIdentifier.identifyStorageScope(path)  			if (storageScope == StorageScope.UNKNOWN) {  				return false  			} @@ -62,8 +62,8 @@ class FileAccessHandler(val context: Context) {  			}  		} -		fun removeFile(context: Context, path: String?): Boolean { -			val storageScope = StorageScope.getStorageScope(context, path) +		internal fun removeFile(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean { +			val storageScope = storageScopeIdentifier.identifyStorageScope(path)  			if (storageScope == StorageScope.UNKNOWN) {  				return false  			} @@ -75,8 +75,8 @@ class FileAccessHandler(val context: Context) {  			}  		} -		fun renameFile(context: Context, from: String?, to: String?): Boolean { -			val storageScope = StorageScope.getStorageScope(context, from) +		internal fun renameFile(context: Context, storageScopeIdentifier: StorageScope.Identifier, from: String?, to: String?): Boolean { +			val storageScope = storageScopeIdentifier.identifyStorageScope(from)  			if (storageScope == StorageScope.UNKNOWN) {  				return false  			} @@ -89,13 +89,14 @@ class FileAccessHandler(val context: Context) {  		}  	} +	private val storageScopeIdentifier = StorageScope.Identifier(context)  	private val files = SparseArray<DataAccess>()  	private var lastFileId = STARTING_FILE_ID  	private fun hasFileId(fileId: Int) = files.indexOfKey(fileId) >= 0  	fun fileOpen(path: String?, modeFlags: Int): Int { -		val storageScope = StorageScope.getStorageScope(context, path) +		val storageScope = storageScopeIdentifier.identifyStorageScope(path)  		if (storageScope == StorageScope.UNKNOWN) {  			return INVALID_FILE_ID  		} @@ -162,10 +163,10 @@ class FileAccessHandler(val context: Context) {  		files[fileId].flush()  	} -	fun fileExists(path: String?) = Companion.fileExists(context, path) +	fun fileExists(path: String?) = Companion.fileExists(context, storageScopeIdentifier, path)  	fun fileLastModified(filepath: String?): Long { -		val storageScope = StorageScope.getStorageScope(context, filepath) +		val storageScope = storageScopeIdentifier.identifyStorageScope(filepath)  		if (storageScope == StorageScope.UNKNOWN) {  			return 0L  		} @@ -193,6 +194,11 @@ class FileAccessHandler(val context: Context) {  		return files[fileId].endOfFile  	} +	fun setFileEof(fileId: Int, eof: Boolean) { +		val file = files[fileId] ?: return +		file.endOfFile = eof +	} +  	fun fileClose(fileId: Int) {  		if (hasFileId(fileId)) {  			files[fileId].close()  |