diff options
Diffstat (limited to 'platform/android/java/lib')
23 files changed, 819 insertions, 360 deletions
| diff --git a/platform/android/java/lib/AndroidManifest.xml b/platform/android/java/lib/AndroidManifest.xml index 79b5aadf2a..1f77e2fc34 100644 --- a/platform/android/java/lib/AndroidManifest.xml +++ b/platform/android/java/lib/AndroidManifest.xml @@ -4,9 +4,6 @@      android:versionCode="1"      android:versionName="1.0"> -    <!-- Should match the mindSdk and targetSdk values in platform/android/java/app/config.gradle --> -    <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="32" /> -      <application>          <!-- Records the version of the Godot library --> diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle index 6b82326a27..841656a240 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. @@ -167,11 +176,10 @@ android {          }      } -    // TODO: Enable when issues with AGP 7.1+ are resolved (https://github.com/GodotVR/godot_openxr/issues/187). -//    publishing { -//        singleVariant("templateRelease") { -//            withSourcesJar() -//            withJavadocJar() -//        } -//    } +    publishing { +        singleVariant("templateRelease") { +            withSourcesJar() +            withJavadocJar() +        } +    }  } 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..3487e5019c 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; @@ -169,6 +175,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC  	public GodotIO io;  	public GodotNetUtils netUtils;  	public GodotTTS tts; +	DirectoryAccessHandler directoryAccessHandler;  	public interface ResultCallback {  		void callback(int requestCode, int resultCode, Intent data); @@ -250,7 +257,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 +269,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(); @@ -289,7 +300,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC  			for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {  				plugin.onRegisterPluginWithGodotNative();  			} -			setKeepScreenOn("True".equals(GodotLib.getGlobal("display/window/energy_saving/keep_screen_on"))); +			setKeepScreenOn(Boolean.parseBoolean(GodotLib.getGlobal("display/window/energy_saving/keep_screen_on")));  		});  		// Include the returned non-null views in the Godot view hierarchy. @@ -303,6 +314,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC  				}  			}  		} +		return true;  	}  	public void setKeepScreenOn(final boolean p_enabled) { @@ -344,13 +356,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();  		}); @@ -463,7 +489,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC  		netUtils = new GodotNetUtils(activity);  		tts = new GodotTTS(activity);  		Context context = getContext(); -		DirectoryAccessHandler directoryAccessHandler = new DirectoryAccessHandler(context); +		directoryAccessHandler = new DirectoryAccessHandler(context);  		FileAccessHandler fileAccessHandler = new FileAccessHandler(context);  		mSensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE);  		mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); @@ -471,7 +497,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 +508,6 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC  				tts);  		result_callback = null; - -		godot_initialized = true;  	}  	@Override @@ -1023,7 +1047,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..f8d937521b 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; @@ -44,9 +43,13 @@ import org.godotengine.godot.xr.regular.RegularFallbackConfigChooser;  import android.annotation.SuppressLint;  import android.content.Context; +import android.content.res.AssetManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory;  import android.graphics.PixelFormat;  import android.os.Build; -import android.view.GestureDetector; +import android.text.TextUtils; +import android.util.SparseArray;  import android.view.KeyEvent;  import android.view.MotionEvent;  import android.view.PointerIcon; @@ -54,6 +57,8 @@ import android.view.SurfaceView;  import androidx.annotation.Keep; +import java.io.InputStream; +  /**   * A simple GLSurfaceView sub-class that demonstrate how to perform   * OpenGL ES 2.0 rendering into a GL Surface. Note the following important @@ -63,7 +68,7 @@ import androidx.annotation.Keep;   *   See ContextFactory class definition below.   *   * - The class must use a custom EGLConfigChooser to be able to select - *   an EGLConfig that supports 2.0. This is done by providing a config + *   an EGLConfig that supports 3.0. This is done by providing a config   *   specification to eglChooseConfig() that has the attribute   *   EGL10.ELG_RENDERABLE_TYPE containing the EGL_OPENGL_ES2_BIT flag   *   set. See ConfigChooser class definition below. @@ -75,9 +80,8 @@ 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; +	private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>();  	public GodotGLRenderView(Context context, Godot godot, XRMode xrMode, boolean p_use_debug_opengl) {  		super(context); @@ -85,10 +89,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 +135,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 +158,77 @@ 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); +	} + +	/** +	 * Used to configure the PointerIcon for the given type. +	 * +	 * Called from JNI +	 */ +	@Keep +	@Override +	public void configurePointerIcon(int pointerType, String imagePath, float hotSpotX, float hotSpotY) { +		if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { +			try { +				Bitmap bitmap = null; +				if (!TextUtils.isEmpty(imagePath)) { +					if (godot.directoryAccessHandler.filesystemFileExists(imagePath)) { +						// Try to load the bitmap from the file system +						bitmap = BitmapFactory.decodeFile(imagePath); +					} else if (godot.directoryAccessHandler.assetsFileExists(imagePath)) { +						// Try to load the bitmap from the assets directory +						AssetManager am = getContext().getAssets(); +						InputStream imageInputStream = am.open(imagePath); +						bitmap = BitmapFactory.decodeStream(imageInputStream); +					} +				} + +				PointerIcon customPointerIcon = PointerIcon.create(bitmap, hotSpotX, hotSpotY); +				customPointerIcons.put(pointerType, customPointerIcon); +			} catch (Exception e) { +				// Reset the custom pointer icon +				customPointerIcons.delete(pointerType); +			} +		} +	} +  	/**  	 * called from JNI to change pointer icon  	 */  	@Keep +	@Override  	public void setPointerIcon(int pointerType) {  		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { -			pointerIcon = PointerIcon.getSystemIcon(getContext(), pointerType); +			PointerIcon pointerIcon = customPointerIcons.get(pointerType); +			if (pointerIcon == null) { +				pointerIcon = PointerIcon.getSystemIcon(getContext(), pointerType); +			} +			setPointerIcon(pointerIcon);  		}  	}  	@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/GodotRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java index cb63fd885f..ab74ba037d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java @@ -48,5 +48,7 @@ public interface GodotRenderView {  	GodotInputHandler getInputHandler(); +	void configurePointerIcon(int pointerType, String imagePath, float hotSpotX, float hotSpotY); +  	void setPointerIcon(int pointerType);  } 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..56bc7f9e76 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java @@ -30,15 +30,18 @@  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;  import android.annotation.SuppressLint;  import android.content.Context; +import android.content.res.AssetManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory;  import android.os.Build; -import android.view.GestureDetector; +import android.text.TextUtils; +import android.util.SparseArray;  import android.view.KeyEvent;  import android.view.MotionEvent;  import android.view.PointerIcon; @@ -46,22 +49,22 @@ import android.view.SurfaceView;  import androidx.annotation.Keep; +import java.io.InputStream; +  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; +	private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>();  	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 +109,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 +132,77 @@ 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); +	} + +	/** +	 * Used to configure the PointerIcon for the given type. +	 * +	 * Called from JNI +	 */ +	@Keep +	@Override +	public void configurePointerIcon(int pointerType, String imagePath, float hotSpotX, float hotSpotY) { +		if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { +			try { +				Bitmap bitmap = null; +				if (!TextUtils.isEmpty(imagePath)) { +					if (godot.directoryAccessHandler.filesystemFileExists(imagePath)) { +						// Try to load the bitmap from the file system +						bitmap = BitmapFactory.decodeFile(imagePath); +					} else if (godot.directoryAccessHandler.assetsFileExists(imagePath)) { +						// Try to load the bitmap from the assets directory +						AssetManager am = getContext().getAssets(); +						InputStream imageInputStream = am.open(imagePath); +						bitmap = BitmapFactory.decodeStream(imageInputStream); +					} +				} + +				PointerIcon customPointerIcon = PointerIcon.create(bitmap, hotSpotX, hotSpotY); +				customPointerIcons.put(pointerType, customPointerIcon); +			} catch (Exception e) { +				// Reset the custom pointer icon +				customPointerIcons.delete(pointerType); +			} +		} +	} +  	/**  	 * called from JNI to change pointer icon  	 */  	@Keep +	@Override  	public void setPointerIcon(int pointerType) {  		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { -			pointerIcon = PointerIcon.getSystemIcon(getContext(), pointerType); +			PointerIcon pointerIcon = customPointerIcons.get(pointerType); +			if (pointerIcon == null) { +				pointerIcon = PointerIcon.getSystemIcon(getContext(), pointerType); +			} +			setPointerIcon(pointerIcon);  		}  	}  	@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..804dbaf165 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) { @@ -115,15 +127,40 @@ public class GodotEditText extends EditText {  					edit.setText("");  					edit.append(text);  					if (msg.arg2 != -1) { -						edit.setSelection(msg.arg1, msg.arg2); +						int selectionStart = Math.min(msg.arg1, edit.length()); +						int selectionEnd = Math.min(msg.arg2, edit.length()); +						edit.setSelection(selectionStart, selectionEnd);  						edit.mInputWrapper.setSelection(true);  					} else {  						edit.mInputWrapper.setSelection(false);  					}  					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 +238,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 +251,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..cde8d7cdae --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt @@ -0,0 +1,265 @@ +/*************************************************************************/ +/*  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 || nextDownIsDoubleTap) { +			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 { +		if (event.actionMasked == MotionEvent.ACTION_CANCEL && pointerCaptureInProgress) { +			// Don't dispatch the ACTION_CANCEL while a capture is in progress +			return true +		} + +		val sourceMouseRelative = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { +			event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE) +		} else { +			false +		} + +		if (pointerCaptureInProgress || dragInProgress || contextClickInProgress) { +			if (contextClickInProgress || GodotInputHandler.isMouseEvent(event)) { +				// This may be an ACTION_BUTTON_RELEASE event which we don't handle, +				// so we convert it to an ACTION_UP event. +				GodotInputHandler.handleMouseEvent( +					MotionEvent.ACTION_UP, +					event.buttonState, +					event.x, +					event.y, +					0f, +					0f, +					false, +					sourceMouseRelative +				) +			} else { +				GodotInputHandler.handleTouchEvent(event) +			} +			pointerCaptureInProgress = false +			dragInProgress = false +			contextClickInProgress = false +			return true +		} + +		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 && !pointerCaptureInProgress) { +			GodotLib.pan(x, y, distanceX / 5f, distanceY / 5f) +		} else { +			GodotInputHandler.handleMotionEvent(terminusEvent) +		} +		return true +	} + +	override fun onScale(detector: ScaleGestureDetector?): Boolean { +		if (detector == null || !panningAndScalingEnabled || pointerCaptureInProgress) { +			return false +		} +		GodotLib.magnify( +			detector.focusX, +			detector.focusY, +			detector.scaleFactor +		) +		return true +	} + +	override fun onScaleBegin(detector: ScaleGestureDetector?): Boolean { +		if (detector == null || !panningAndScalingEnabled || pointerCaptureInProgress) { +			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..0ba86e4316 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 { +			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,116 @@ 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_TOUCHSCREEN | 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) { +		// We don't handle ACTION_BUTTON_PRESS and ACTION_BUTTON_RELEASE events as they typically +		// follow ACTION_DOWN and ACTION_UP events. As such, handling them would result in duplicate +		// stream of events to the engine. +		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..1a3576a6a9 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,24 @@ 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 +			} + +			if (internalCacheDir != null && canonicalPathFile.startsWith(internalCacheDir)) {  				return APP  			} -			val internalCacheDir = context.cacheDir.canonicalPath ?: return UNKNOWN -			if (canonicalPathFile.startsWith(internalCacheDir)) { +			if (externalAppDir != null && canonicalPathFile.startsWith(externalAppDir)) {  				return APP  			} -			val externalAppDir = context.getExternalFilesDir(null)?.canonicalPath ?: return UNKNOWN -			if (canonicalPathFile.startsWith(externalAppDir)) { +			var rootDir: String? = System.getenv("ANDROID_ROOT") +			if (rootDir != null && canonicalPathFile.startsWith(rootDir)) {  				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 +104,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/DirectoryAccessHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt index fedcf4843f..6bc317415f 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt @@ -79,6 +79,9 @@ class DirectoryAccessHandler(context: Context) {  	private val assetsDirAccess = AssetsDirectoryAccess(context)  	private val fileSystemDirAccess = FilesystemDirectoryAccess(context) +	fun assetsFileExists(assetsPath: String) = assetsDirAccess.fileExists(assetsPath) +	fun filesystemFileExists(path: String) = fileSystemDirAccess.fileExists(path) +  	private fun hasDirId(accessType: AccessType, dirId: Int): Boolean {  		return when (accessType) {  			ACCESS_RESOURCES -> assetsDirAccess.hasDirId(dirId) 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() diff --git a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java index bb5042fa09..8ca5bcaa6e 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java +++ b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java @@ -71,11 +71,11 @@ import javax.microedition.khronos.opengles.GL10;   * - 'plugin.init.ClassFullName' is the full name (package + class name) of the plugin class   * extending {@link GodotPlugin}.   * - * A plugin can also define and provide c/c++ gdnative libraries and nativescripts for the target + * A plugin can also define and provide c/c++ gdextension libraries and nativescripts for the target   * app/game to leverage. - * The shared library for the gdnative library will be automatically bundled by the aar build + * The shared library for the gdextension library will be automatically bundled by the aar build   * system. - * Godot '*.gdnlib' and '*.gdns' resource files must however be manually defined in the project + * Godot '*.gdextension' resource files must however be manually defined in the project   * 'assets' directory. The recommended path for these resources in the 'assets' directory should be:   * 'godot/plugin/v1/[PluginName]/'   */ @@ -112,7 +112,7 @@ public abstract class GodotPlugin {  	public final void onRegisterPluginWithGodotNative() {  		registeredSignals.putAll(  				registerPluginWithGodotNative(this, getPluginName(), getPluginMethods(), getPluginSignals(), -						getPluginGDNativeLibrariesPaths())); +						getPluginGDExtensionLibrariesPaths()));  	}  	/** @@ -124,7 +124,7 @@ public abstract class GodotPlugin {  			GodotPluginInfoProvider pluginInfoProvider) {  		registerPluginWithGodotNative(pluginObject, pluginInfoProvider.getPluginName(),  				Collections.emptyList(), pluginInfoProvider.getPluginSignals(), -				pluginInfoProvider.getPluginGDNativeLibrariesPaths()); +				pluginInfoProvider.getPluginGDExtensionLibrariesPaths());  		// Notify that registration is complete.  		pluginInfoProvider.onPluginRegistered(); @@ -132,7 +132,7 @@ public abstract class GodotPlugin {  	private static Map<String, SignalInfo> registerPluginWithGodotNative(Object pluginObject,  			String pluginName, List<String> pluginMethods, Set<SignalInfo> pluginSignals, -			Set<String> pluginGDNativeLibrariesPaths) { +			Set<String> pluginGDExtensionLibrariesPaths) {  		nativeRegisterSingleton(pluginName, pluginObject);  		Set<Method> filteredMethods = new HashSet<>(); @@ -176,9 +176,9 @@ public abstract class GodotPlugin {  			registeredSignals.put(signalName, signalInfo);  		} -		// Get the list of gdnative libraries to register. -		if (!pluginGDNativeLibrariesPaths.isEmpty()) { -			nativeRegisterGDNativeLibraries(pluginGDNativeLibrariesPaths.toArray(new String[0])); +		// Get the list of gdextension libraries to register. +		if (!pluginGDExtensionLibrariesPaths.isEmpty()) { +			nativeRegisterGDExtensionLibraries(pluginGDExtensionLibrariesPaths.toArray(new String[0]));  		}  		return registeredSignals; @@ -304,12 +304,12 @@ public abstract class GodotPlugin {  	}  	/** -	 * Returns the paths for the plugin's gdnative libraries. +	 * Returns the paths for the plugin's gdextension libraries.  	 * -	 * The paths must be relative to the 'assets' directory and point to a '*.gdnlib' file. +	 * The paths must be relative to the 'assets' directory and point to a '*.gdextension' file.  	 */  	@NonNull -	protected Set<String> getPluginGDNativeLibrariesPaths() { +	protected Set<String> getPluginGDExtensionLibrariesPaths() {  		return Collections.emptySet();  	} @@ -420,10 +420,10 @@ public abstract class GodotPlugin {  	private static native void nativeRegisterMethod(String p_sname, String p_name, String p_ret, String[] p_params);  	/** -	 * Used to register gdnative libraries bundled by the plugin. -	 * @param gdnlibPaths Paths to the libraries relative to the 'assets' directory. +	 * Used to register gdextension libraries bundled by the plugin. +	 * @param gdextensionPaths Paths to the libraries relative to the 'assets' directory.  	 */ -	private static native void nativeRegisterGDNativeLibraries(String[] gdnlibPaths); +	private static native void nativeRegisterGDExtensionLibraries(String[] gdextensionPaths);  	/**  	 * Used to complete registration of the {@link GodotPlugin} instance's methods. diff --git a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginInfoProvider.java b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginInfoProvider.java index cfb84c3931..53b78aebfb 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginInfoProvider.java +++ b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginInfoProvider.java @@ -54,12 +54,12 @@ public interface GodotPluginInfoProvider {  	}  	/** -	 * Returns the paths for the plugin's gdnative libraries (if any). +	 * Returns the paths for the plugin's gdextension libraries (if any).  	 * -	 * The paths must be relative to the 'assets' directory and point to a '*.gdnlib' file. +	 * The paths must be relative to the 'assets' directory and point to a '*.gdextension' file.  	 */  	@NonNull -	default Set<String> getPluginGDNativeLibrariesPaths() { +	default Set<String> getPluginGDExtensionLibrariesPaths() {  		return Collections.emptySet();  	} diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java index 445238b1c2..9834fdfb88 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java @@ -45,20 +45,18 @@ public class RegularConfigChooser implements GLSurfaceView.EGLConfigChooser {  	private int[] mValue = new int[1]; -	// FIXME: Add support for Vulkan. - -	/* This EGL config specification is used to specify 2.0 rendering. +	/* This EGL config specification is used to specify 3.0 rendering.  	 * We use a minimum size of 4 bits for red/green/blue, but will  	 * perform actual matching in chooseConfig() below.  	 */  	private static int EGL_OPENGL_ES2_BIT = 4; -	private static int[] s_configAttribs2 = { +	private static int[] s_configAttribs = {  		EGL10.EGL_RED_SIZE, 4,  		EGL10.EGL_GREEN_SIZE, 4,  		EGL10.EGL_BLUE_SIZE, 4, -		//  EGL10.EGL_DEPTH_SIZE,     16, +		// EGL10.EGL_DEPTH_SIZE,     16,  		// EGL10.EGL_STENCIL_SIZE,   EGL10.EGL_DONT_CARE, -		EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, +		EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, //apparently there is no EGL_OPENGL_ES3_BIT  		EGL10.EGL_NONE  	}; @@ -75,7 +73,7 @@ public class RegularConfigChooser implements GLSurfaceView.EGLConfigChooser {  		/* Get the number of minimally matching EGL configurations  		 */  		int[] num_config = new int[1]; -		egl.eglChooseConfig(display, s_configAttribs2, null, 0, num_config); +		egl.eglChooseConfig(display, s_configAttribs, null, 0, num_config);  		int numConfigs = num_config[0]; @@ -86,7 +84,7 @@ public class RegularConfigChooser implements GLSurfaceView.EGLConfigChooser {  		/* Allocate then read the array of minimally matching EGL configs  		 */  		EGLConfig[] configs = new EGLConfig[numConfigs]; -		egl.eglChooseConfig(display, s_configAttribs2, configs, numConfigs, num_config); +		egl.eglChooseConfig(display, s_configAttribs, configs, numConfigs, num_config);  		if (GLUtils.DEBUG) {  			GLUtils.printConfigs(egl, display, configs); diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java index 5d62723170..8fb86bf6d0 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java @@ -52,17 +52,16 @@ public class RegularContextFactory implements GLSurfaceView.EGLContextFactory {  	private static int EGL_CONTEXT_CLIENT_VERSION = 0x3098;  	public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) { -		// FIXME: Add support for Vulkan. -		Log.w(TAG, "creating OpenGL ES 2.0 context :"); +		Log.w(TAG, "creating OpenGL ES 3.0 context :");  		GLUtils.checkEglError(TAG, "Before eglCreateContext", egl);  		EGLContext context;  		if (GLUtils.use_debug_opengl) { -			int[] attrib_list2 = { EGL_CONTEXT_CLIENT_VERSION, 2, _EGL_CONTEXT_FLAGS_KHR, _EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR, EGL10.EGL_NONE }; -			context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list2); +			int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 3, _EGL_CONTEXT_FLAGS_KHR, _EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR, EGL10.EGL_NONE }; +			context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);  		} else { -			int[] attrib_list2 = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE }; -			context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list2); +			int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL10.EGL_NONE }; +			context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);  		}  		GLUtils.checkEglError(TAG, "After eglCreateContext", egl);  		return context; |