summaryrefslogtreecommitdiff
path: root/platform/android/java/lib/src
diff options
context:
space:
mode:
Diffstat (limited to 'platform/android/java/lib/src')
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java24
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/Godot.java84
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotIO.java150
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotLib.java27
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java49
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.java9
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java12
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java2
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java134
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java102
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt113
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt177
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt224
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt231
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt187
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt87
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt203
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt93
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt284
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java298
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/tts/GodotUtterance.java (renamed from platform/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java)37
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java4
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java55
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java141
24 files changed, 2269 insertions, 458 deletions
diff --git a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java
index e8ffbb9481..f21f88db0a 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java
@@ -30,7 +30,8 @@
package org.godotengine.godot;
-import android.content.ComponentName;
+import org.godotengine.godot.utils.ProcessPhoenix;
+
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
@@ -65,16 +66,13 @@ public abstract class FullScreenGodotApp extends FragmentActivity implements God
} else {
Log.v(TAG, "Creating new Godot fragment instance.");
godotFragment = initGodotInstance();
- if (godotFragment == null) {
- throw new IllegalStateException("Godot instance must be non-null.");
- }
-
getSupportFragmentManager().beginTransaction().replace(R.id.godot_fragment_container, godotFragment).setPrimaryNavigationFragment(godotFragment).commitNowAllowingStateLoss();
}
}
@Override
public void onDestroy() {
+ Log.v(TAG, "Destroying Godot app...");
super.onDestroy();
onGodotForceQuit(godotFragment);
}
@@ -82,27 +80,21 @@ public abstract class FullScreenGodotApp extends FragmentActivity implements God
@Override
public final void onGodotForceQuit(Godot instance) {
if (instance == godotFragment) {
- System.exit(0);
+ Log.v(TAG, "Force quitting Godot instance");
+ ProcessPhoenix.forceQuit(this);
}
}
@Override
public final void onGodotRestartRequested(Godot instance) {
if (instance == godotFragment) {
- // HACK:
- //
- // Currently it's very hard to properly deinitialize Godot on Android to restart the game
+ // It's very hard to properly de-initialize Godot on Android to restart the game
// from scratch. Therefore, we need to kill the whole app process and relaunch it.
//
// Restarting only the activity, wouldn't be enough unless it did proper cleanup (including
// releasing and reloading native libs or resetting their state somehow and clearing statics).
- //
- // Using instrumentation is a way of making the whole app process restart, because Android
- // will kill any process of the same package which was already running.
- //
- Bundle args = new Bundle();
- args.putParcelable("intent", getIntent());
- startInstrumentation(new ComponentName(this, GodotInstrumentation.class), null, args);
+ Log.v(TAG, "Restarting Godot instance...");
+ ProcessPhoenix.triggerRebirth(this);
}
}
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 8a86136daf..28e689e63a 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java
@@ -34,8 +34,11 @@ import static android.content.Context.MODE_PRIVATE;
import static android.content.Context.WINDOW_SERVICE;
import org.godotengine.godot.input.GodotEditText;
+import org.godotengine.godot.io.directory.DirectoryAccessHandler;
+import org.godotengine.godot.io.file.FileAccessHandler;
import org.godotengine.godot.plugin.GodotPlugin;
import org.godotengine.godot.plugin.GodotPluginRegistry;
+import org.godotengine.godot.tts.GodotTTS;
import org.godotengine.godot.utils.GodotNetUtils;
import org.godotengine.godot.utils.PermissionsUtil;
import org.godotengine.godot.xr.XRMode;
@@ -163,8 +166,9 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
private Sensor mMagnetometer;
private Sensor mGyroscope;
- public static GodotIO io;
- public static GodotNetUtils netUtils;
+ public GodotIO io;
+ public GodotNetUtils netUtils;
+ public GodotTTS tts;
public interface ResultCallback {
void callback(int requestCode, int resultCode, Intent data);
@@ -456,19 +460,26 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
final Activity activity = getActivity();
io = new GodotIO(activity);
- GodotLib.io = io;
netUtils = new GodotNetUtils(activity);
+ tts = new GodotTTS(activity);
+ Context context = getContext();
+ DirectoryAccessHandler directoryAccessHandler = new DirectoryAccessHandler(context);
+ FileAccessHandler fileAccessHandler = new FileAccessHandler(context);
mSensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
- mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME);
mGravity = mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY);
- mSensorManager.registerListener(this, mGravity, SensorManager.SENSOR_DELAY_GAME);
mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
- mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME);
mGyroscope = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
- mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME);
- GodotLib.initialize(activity, this, activity.getAssets(), use_apk_expansion);
+ GodotLib.initialize(activity,
+ this,
+ activity.getAssets(),
+ io,
+ netUtils,
+ directoryAccessHandler,
+ fileAccessHandler,
+ use_apk_expansion,
+ tts);
result_callback = null;
@@ -509,17 +520,14 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
use_debug_opengl = true;
} else if (command_line[i].equals("--use_immersive")) {
use_immersive = true;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // check if the application runs on an android 4.4+
- window.getDecorView().setSystemUiVisibility(
- View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
- View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
- View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | // hide nav bar
- View.SYSTEM_UI_FLAG_FULLSCREEN | // hide status bar
- View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
-
- UiChangeListener();
- }
+ window.getDecorView().setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
+ View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | // hide nav bar
+ View.SYSTEM_UI_FLAG_FULLSCREEN | // hide status bar
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
+ UiChangeListener();
} else if (command_line[i].equals("--use_apk_expansion")) {
use_apk_expansion = true;
} else if (has_extra && command_line[i].equals("--apk_expansion_md5")) {
@@ -666,14 +674,13 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
}
public String getClipboard() {
- String copiedText = "";
-
- if (mClipboard.hasPrimaryClip()) {
- ClipData.Item item = mClipboard.getPrimaryClip().getItemAt(0);
- copiedText = item.getText().toString();
- }
-
- return copiedText;
+ ClipData clipData = mClipboard.getPrimaryClip();
+ if (clipData == null)
+ return "";
+ CharSequence text = clipData.getItemAt(0).getText();
+ if (text == null)
+ return "";
+ return text.toString();
}
public void setClipboard(String p_text) {
@@ -699,7 +706,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME);
mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME);
- if (use_immersive && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // check if the application runs on an android 4.4+
+ if (use_immersive) {
Window window = getActivity().getWindow();
window.getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
@@ -719,15 +726,13 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
final View decorView = getActivity().getWindow().getDecorView();
decorView.setOnSystemUiVisibilityChangeListener(visibility -> {
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
- decorView.setSystemUiVisibility(
- View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
- View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
- View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
- View.SYSTEM_UI_FLAG_FULLSCREEN |
- View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
- }
+ decorView.setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
+ View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_FULLSCREEN |
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}
});
}
@@ -888,9 +893,8 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
// Create Hex String
StringBuilder hexString = new StringBuilder();
- for (int i = 0; i < messageDigest.length; i++) {
- String s = Integer.toHexString(0xFF & messageDigest[i]);
-
+ for (byte b : messageDigest) {
+ String s = Integer.toHexString(0xFF & b);
if (s.length() == 1) {
s = "0" + s;
}
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 e8e292df5d..d283de8ce8 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java
@@ -36,8 +36,8 @@ import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.ActivityInfo;
-import android.content.res.AssetManager;
import android.graphics.Point;
+import android.graphics.Rect;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
@@ -45,12 +45,11 @@ import android.provider.Settings;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
-import android.util.SparseArray;
import android.view.Display;
import android.view.DisplayCutout;
import android.view.WindowInsets;
-import java.io.IOException;
+import java.util.List;
import java.util.Locale;
// Wrapper for native library
@@ -58,7 +57,6 @@ import java.util.Locale;
public class GodotIO {
private static final String TAG = GodotIO.class.getSimpleName();
- private final AssetManager am;
private final Activity activity;
private final String uniqueId;
GodotEditText edit;
@@ -71,100 +69,8 @@ public class GodotIO {
final int SCREEN_SENSOR_PORTRAIT = 5;
final int SCREEN_SENSOR = 6;
- /////////////////////////
- /// DIRECTORIES
- /////////////////////////
-
- static class AssetDir {
- public String[] files;
- public int current;
- public String path;
- }
-
- private int last_dir_id = 1;
-
- private final SparseArray<AssetDir> dirs;
-
- public int dir_open(String path) {
- AssetDir ad = new AssetDir();
- ad.current = 0;
- ad.path = path;
-
- try {
- ad.files = am.list(path);
- // no way to find path is directory or file exactly.
- // but if ad.files.length==0, then it's an empty directory or file.
- if (ad.files.length == 0) {
- return -1;
- }
- } catch (IOException e) {
- System.out.printf("Exception on dir_open: %s\n", e);
- return -1;
- }
-
- ++last_dir_id;
- dirs.put(last_dir_id, ad);
-
- return last_dir_id;
- }
-
- public boolean dir_is_dir(int id) {
- if (dirs.get(id) == null) {
- System.out.printf("dir_next: invalid dir id: %d\n", id);
- return false;
- }
- AssetDir ad = dirs.get(id);
- //System.out.printf("go next: %d,%d\n",ad.current,ad.files.length);
- int idx = ad.current;
- if (idx > 0)
- idx--;
-
- if (idx >= ad.files.length)
- return false;
- String fname = ad.files[idx];
-
- try {
- if (ad.path.equals(""))
- am.open(fname);
- else
- am.open(ad.path + "/" + fname);
- return false;
- } catch (Exception e) {
- return true;
- }
- }
-
- public String dir_next(int id) {
- if (dirs.get(id) == null) {
- System.out.printf("dir_next: invalid dir id: %d\n", id);
- return "";
- }
-
- AssetDir ad = dirs.get(id);
- //System.out.printf("go next: %d,%d\n",ad.current,ad.files.length);
-
- if (ad.current >= ad.files.length) {
- ad.current++;
- return "";
- }
- String r = ad.files[ad.current];
- ad.current++;
- return r;
- }
-
- public void dir_close(int id) {
- if (dirs.get(id) == null) {
- System.out.printf("dir_close: invalid dir id: %d\n", id);
- return;
- }
-
- dirs.remove(id);
- }
-
GodotIO(Activity p_activity) {
- am = p_activity.getAssets();
activity = p_activity;
- dirs = new SparseArray<>();
String androidId = Settings.Secure.getString(activity.getContentResolver(),
Settings.Secure.ANDROID_ID);
if (androidId == null) {
@@ -222,12 +128,30 @@ public class GodotIO {
}
public int getScreenDPI() {
- DisplayMetrics metrics = activity.getResources().getDisplayMetrics();
- return (int)(metrics.density * 160f);
+ return activity.getResources().getDisplayMetrics().densityDpi;
}
+ /**
+ * Returns bucketized density values.
+ */
public float getScaledDensity() {
- return activity.getResources().getDisplayMetrics().scaledDensity;
+ int densityDpi = activity.getResources().getDisplayMetrics().densityDpi;
+ float selectedScaledDensity;
+ if (densityDpi >= DisplayMetrics.DENSITY_XXXHIGH) {
+ selectedScaledDensity = 4.0f;
+ } else if (densityDpi >= DisplayMetrics.DENSITY_XXHIGH) {
+ selectedScaledDensity = 3.0f;
+ } else if (densityDpi >= DisplayMetrics.DENSITY_XHIGH) {
+ selectedScaledDensity = 2.0f;
+ } else if (densityDpi >= DisplayMetrics.DENSITY_HIGH) {
+ selectedScaledDensity = 1.5f;
+ } else if (densityDpi >= DisplayMetrics.DENSITY_MEDIUM) {
+ selectedScaledDensity = 1.0f;
+ } else {
+ selectedScaledDensity = 0.75f;
+ }
+ Log.d(TAG, "Selected scaled density: " + selectedScaledDensity);
+ return selectedScaledDensity;
}
public double getScreenRefreshRate(double fallback) {
@@ -238,7 +162,7 @@ public class GodotIO {
return fallback;
}
- public int[] screenGetUsableRect() {
+ public int[] getDisplaySafeArea() {
DisplayMetrics metrics = activity.getResources().getDisplayMetrics();
Display display = activity.getWindowManager().getDefaultDisplay();
Point size = new Point();
@@ -260,9 +184,29 @@ 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 int[] getDisplayCutouts() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
+ return new int[0];
+ DisplayCutout cutout = activity.getWindow().getDecorView().getRootWindowInsets().getDisplayCutout();
+ if (cutout == null)
+ return new int[0];
+ List<Rect> rects = cutout.getBoundingRects();
+ int cutouts = rects.size();
+ int[] result = new int[cutouts * 4];
+ int index = 0;
+ for (Rect rect : rects) {
+ result[index++] = rect.left;
+ result[index++] = rect.top;
+ result[index++] = rect.width();
+ result[index++] = rect.height();
+ }
+ return result;
+ }
+
+ 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 253a51b83c..e2ae62d9cf 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
@@ -31,8 +31,13 @@
package org.godotengine.godot;
import org.godotengine.godot.gl.GodotRenderer;
+import org.godotengine.godot.io.directory.DirectoryAccessHandler;
+import org.godotengine.godot.io.file.FileAccessHandler;
+import org.godotengine.godot.tts.GodotTTS;
+import org.godotengine.godot.utils.GodotNetUtils;
import android.app.Activity;
+import android.content.res.AssetManager;
import android.hardware.SensorEvent;
import android.view.Surface;
@@ -42,8 +47,6 @@ import javax.microedition.khronos.opengles.GL10;
* Wrapper for native library
*/
public class GodotLib {
- public static GodotIO io;
-
static {
System.loadLibrary("godot_android");
}
@@ -51,7 +54,15 @@ public class GodotLib {
/**
* Invoked on the main thread to initialize Godot native layer.
*/
- public static native void initialize(Activity activity, Godot p_instance, Object p_asset_manager, boolean use_apk_expansion);
+ public static native void initialize(Activity activity,
+ Godot p_instance,
+ AssetManager p_asset_manager,
+ GodotIO godotIO,
+ GodotNetUtils netUtils,
+ DirectoryAccessHandler directoryAccessHandler,
+ FileAccessHandler fileAccessHandler,
+ boolean use_apk_expansion,
+ GodotTTS tts);
/**
* Invoked on the main thread to clean up Godot native layer.
@@ -92,6 +103,11 @@ public class GodotLib {
public static native boolean step();
/**
+ * TTS callback.
+ */
+ public static native void ttsCallback(int event, int id, int pos);
+
+ /**
* Forward touch events from the main thread to the GL thread.
*/
public static native void touch(int inputDevice, int event, int pointer, int pointerCount, float[] positions);
@@ -109,11 +125,6 @@ public class GodotLib {
public static native void doubleTap(int buttonMask, int x, int y);
/**
- * Forward scroll events from the main thread to the GL thread.
- */
- public static native void scroll(int x, int y);
-
- /**
* Forward accelerometer sensor events from the main thread to the GL thread.
* @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent)
*/
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
index ac13cad23e..778efa914a 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.java
@@ -80,15 +80,6 @@ public class GodotGestureHandler extends GestureDetector.SimpleOnGestureListener
}
@Override
- public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
- //Log.i("GodotGesture", "onScroll");
- final int x = Math.round(distanceX);
- final int y = Math.round(distanceY);
- GodotLib.scroll(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/GodotInputHandler.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java
index c06d89b843..ccfb865b1a 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
@@ -34,8 +34,9 @@ import static org.godotengine.godot.utils.GLUtils.DEBUG;
import org.godotengine.godot.GodotLib;
import org.godotengine.godot.GodotRenderView;
-import org.godotengine.godot.input.InputManagerCompat.InputDeviceListener;
+import android.content.Context;
+import android.hardware.input.InputManager;
import android.os.Build;
import android.util.Log;
import android.util.SparseArray;
@@ -53,9 +54,9 @@ import java.util.Set;
/**
* Handles input related events for the {@link GodotRenderView} view.
*/
-public class GodotInputHandler implements InputDeviceListener {
+public class GodotInputHandler implements InputManager.InputDeviceListener {
private final GodotRenderView mRenderView;
- private final InputManagerCompat mInputManager;
+ private final InputManager mInputManager;
private final String tag = this.getClass().getSimpleName();
@@ -64,7 +65,7 @@ public class GodotInputHandler implements InputDeviceListener {
public GodotInputHandler(GodotRenderView godotView) {
mRenderView = godotView;
- mInputManager = InputManagerCompat.Factory.getInputManager(mRenderView.getView().getContext());
+ mInputManager = (InputManager)mRenderView.getView().getContext().getSystemService(Context.INPUT_SERVICE);
mInputManager.registerInputDeviceListener(this, null);
}
@@ -185,6 +186,9 @@ public class GodotInputHandler implements InputDeviceListener {
if (mJoystickIds.indexOfKey(deviceId) >= 0) {
final int godotJoyId = mJoystickIds.get(deviceId);
Joystick joystick = mJoysticksDevices.get(deviceId);
+ if (joystick == null) {
+ return true;
+ }
for (int i = 0; i < joystick.axes.size(); i++) {
final int axis = joystick.axes.get(i);
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..7714703a21 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
@@ -115,7 +115,7 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene
}
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;
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java b/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java
deleted file mode 100644
index 21fdc658bb..0000000000
--- a/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.godotengine.godot.input;
-
-import android.content.Context;
-import android.os.Handler;
-import android.view.InputDevice;
-import android.view.MotionEvent;
-
-public interface InputManagerCompat {
- /**
- * Gets information about the input device with the specified id.
- *
- * @param id The device id
- * @return The input device or null if not found
- */
- InputDevice getInputDevice(int id);
-
- /**
- * Gets the ids of all input devices in the system.
- *
- * @return The input device ids.
- */
- int[] getInputDeviceIds();
-
- /**
- * Registers an input device listener to receive notifications about when
- * input devices are added, removed or changed.
- *
- * @param listener The listener to register.
- * @param handler The handler on which the listener should be invoked, or
- * null if the listener should be invoked on the calling thread's
- * looper.
- */
- void registerInputDeviceListener(InputManagerCompat.InputDeviceListener listener,
- Handler handler);
-
- /**
- * Unregisters an input device listener.
- *
- * @param listener The listener to unregister.
- */
- void unregisterInputDeviceListener(InputManagerCompat.InputDeviceListener listener);
-
- /*
- * The following three calls are to simulate V16 behavior on pre-Jellybean
- * devices. If you don't call them, your callback will never be called
- * pre-API 16.
- */
-
- /**
- * Pass the motion events to the InputManagerCompat. This is used to
- * optimize for polling for controllers. If you do not pass these events in,
- * polling will cause regular object creation.
- *
- * @param event the motion event from the app
- */
- void onGenericMotionEvent(MotionEvent event);
-
- /**
- * Tell the V9 input manager that it should stop polling for disconnected
- * devices. You can call this during onPause in your activity, although you
- * might want to call it whenever your game is not active (or whenever you
- * don't care about being notified of new input devices)
- */
- void onPause();
-
- /**
- * Tell the V9 input manager that it should start polling for disconnected
- * devices. You can call this during onResume in your activity, although you
- * might want to call it less often (only when the gameplay is actually
- * active)
- */
- void onResume();
-
- interface InputDeviceListener {
- /**
- * Called whenever the input manager detects that a device has been
- * added. This will only be called in the V9 version when a motion event
- * is detected.
- *
- * @param deviceId The id of the input device that was added.
- */
- void onInputDeviceAdded(int deviceId);
-
- /**
- * Called whenever the properties of an input device have changed since
- * they were last queried. This will not be called for the V9 version of
- * the API.
- *
- * @param deviceId The id of the input device that changed.
- */
- void onInputDeviceChanged(int deviceId);
-
- /**
- * Called whenever the input manager detects that a device has been
- * removed. For the V9 version, this can take some time depending on the
- * poll rate.
- *
- * @param deviceId The id of the input device that was removed.
- */
- void onInputDeviceRemoved(int deviceId);
- }
-
- /**
- * Use this to construct a compatible InputManager.
- */
- class Factory {
- /**
- * Constructs and returns a compatible InputManger
- *
- * @param context the Context that will be used to get the system
- * service from
- * @return a compatible implementation of InputManager
- */
- public static InputManagerCompat getInputManager(Context context) {
- return new InputManagerV16(context);
- }
- }
-}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java b/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java
deleted file mode 100644
index 0dbc13c77b..0000000000
--- a/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.godotengine.godot.input;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.hardware.input.InputManager;
-import android.os.Build;
-import android.os.Handler;
-import android.view.InputDevice;
-import android.view.MotionEvent;
-
-import java.util.HashMap;
-import java.util.Map;
-
-@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
-public class InputManagerV16 implements InputManagerCompat {
- private final InputManager mInputManager;
- private final Map<InputManagerCompat.InputDeviceListener, V16InputDeviceListener> mListeners;
-
- public InputManagerV16(Context context) {
- mInputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE);
- mListeners = new HashMap<>();
- }
-
- @Override
- public InputDevice getInputDevice(int id) {
- return mInputManager.getInputDevice(id);
- }
-
- @Override
- public int[] getInputDeviceIds() {
- return mInputManager.getInputDeviceIds();
- }
-
- static class V16InputDeviceListener implements InputManager.InputDeviceListener {
- final InputManagerCompat.InputDeviceListener mIDL;
-
- public V16InputDeviceListener(InputDeviceListener idl) {
- mIDL = idl;
- }
-
- @Override
- public void onInputDeviceAdded(int deviceId) {
- mIDL.onInputDeviceAdded(deviceId);
- }
-
- @Override
- public void onInputDeviceChanged(int deviceId) {
- mIDL.onInputDeviceChanged(deviceId);
- }
-
- @Override
- public void onInputDeviceRemoved(int deviceId) {
- mIDL.onInputDeviceRemoved(deviceId);
- }
- }
-
- @Override
- public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) {
- V16InputDeviceListener v16Listener = new V16InputDeviceListener(listener);
- mInputManager.registerInputDeviceListener(v16Listener, handler);
- mListeners.put(listener, v16Listener);
- }
-
- @Override
- public void unregisterInputDeviceListener(InputDeviceListener listener) {
- V16InputDeviceListener curListener = mListeners.remove(listener);
- if (null != curListener) {
- mInputManager.unregisterInputDeviceListener(curListener);
- }
- }
-
- @Override
- public void onGenericMotionEvent(MotionEvent event) {
- // unused in V16
- }
-
- @Override
- public void onPause() {
- // unused in V16
- }
-
- @Override
- public void onResume() {
- // unused in V16
- }
-}
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
new file mode 100644
index 0000000000..c9282dd247
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt
@@ -0,0 +1,113 @@
+/*************************************************************************/
+/* StorageScope.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.io
+
+import android.content.Context
+import android.os.Build
+import android.os.Environment
+import java.io.File
+
+/**
+ * Represents the different storage scopes.
+ */
+internal enum class StorageScope {
+ /**
+ * Covers internal and external directories accessible to the app without restrictions.
+ */
+ APP,
+
+ /**
+ * Covers shared directories (from Android 10 and higher).
+ */
+ SHARED,
+
+ /**
+ * Everything else..
+ */
+ UNKNOWN;
+
+ 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 identifyStorageScope(path: String?): StorageScope {
+ if (path == null) {
+ return UNKNOWN
+ }
+
+ val pathFile = File(path)
+ if (!pathFile.isAbsolute) {
+ return UNKNOWN
+ }
+
+ val canonicalPathFile = pathFile.canonicalPath
+
+ if (internalAppDir != null && canonicalPathFile.startsWith(internalAppDir)) {
+ return APP
+ }
+
+ if (internalCacheDir != null && canonicalPathFile.startsWith(internalCacheDir)) {
+ return APP
+ }
+
+ if (externalAppDir != null && canonicalPathFile.startsWith(externalAppDir)) {
+ return APP
+ }
+
+ 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).
+ return APP
+ }
+
+ // Post R, access is limited based on the target destination
+ // 'Downloads' and 'Documents' are still accessible
+ if ((downloadsSharedDir != null && canonicalPathFile.startsWith(downloadsSharedDir))
+ || (documentsSharedDir != null && canonicalPathFile.startsWith(documentsSharedDir))) {
+ return APP
+ }
+
+ return SHARED
+ }
+
+ return UNKNOWN
+ }
+ }
+}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt
new file mode 100644
index 0000000000..098b10ae36
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt
@@ -0,0 +1,177 @@
+/*************************************************************************/
+/* AssetsDirectoryAccess.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.io.directory
+
+import android.content.Context
+import android.util.Log
+import android.util.SparseArray
+import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.INVALID_DIR_ID
+import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.STARTING_DIR_ID
+import java.io.File
+import java.io.IOException
+
+/**
+ * Handles directories access within the Android assets directory.
+ */
+internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.DirectoryAccess {
+
+ companion object {
+ private val TAG = AssetsDirectoryAccess::class.java.simpleName
+ }
+
+ private data class AssetDir(val path: String, val files: Array<String>, var current: Int = 0)
+
+ private val assetManager = context.assets
+
+ private var lastDirId = STARTING_DIR_ID
+ private val dirs = SparseArray<AssetDir>()
+
+ private fun getAssetsPath(originalPath: String): String {
+ if (originalPath.startsWith(File.separatorChar)) {
+ return originalPath.substring(1)
+ }
+ return originalPath
+ }
+
+ override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0
+
+ override fun dirOpen(path: String): Int {
+ val assetsPath = getAssetsPath(path) ?: return INVALID_DIR_ID
+ try {
+ val files = assetManager.list(assetsPath) ?: return INVALID_DIR_ID
+ // Empty directories don't get added to the 'assets' directory, so
+ // if ad.files.length > 0 ==> path is directory
+ // if ad.files.length == 0 ==> path is file
+ if (files.isEmpty()) {
+ return INVALID_DIR_ID
+ }
+
+ val ad = AssetDir(assetsPath, files)
+
+ dirs.put(++lastDirId, ad)
+ return lastDirId
+ } catch (e: IOException) {
+ Log.e(TAG, "Exception on dirOpen", e)
+ return INVALID_DIR_ID
+ }
+ }
+
+ override fun dirExists(path: String): Boolean {
+ val assetsPath = getAssetsPath(path)
+ try {
+ val files = assetManager.list(assetsPath) ?: return false
+ // Empty directories don't get added to the 'assets' directory, so
+ // if ad.files.length > 0 ==> path is directory
+ // if ad.files.length == 0 ==> path is file
+ return files.isNotEmpty()
+ } catch (e: IOException) {
+ Log.e(TAG, "Exception on dirExists", e)
+ return false
+ }
+ }
+
+ override fun fileExists(path: String): Boolean {
+ val assetsPath = getAssetsPath(path) ?: return false
+ try {
+ val files = assetManager.list(assetsPath) ?: return false
+ // Empty directories don't get added to the 'assets' directory, so
+ // if ad.files.length > 0 ==> path is directory
+ // if ad.files.length == 0 ==> path is file
+ return files.isEmpty()
+ } catch (e: IOException) {
+ Log.e(TAG, "Exception on fileExists", e)
+ return false
+ }
+ }
+
+ override fun dirIsDir(dirId: Int): Boolean {
+ val ad: AssetDir = dirs[dirId]
+
+ var idx = ad.current
+ if (idx > 0) {
+ idx--
+ }
+
+ if (idx >= ad.files.size) {
+ return false
+ }
+
+ val fileName = ad.files[idx]
+ // List the contents of $fileName. If it's a file, it will be empty, otherwise it'll be a
+ // directory
+ val filePath = if (ad.path == "") fileName else "${ad.path}/${fileName}"
+ val fileContents = assetManager.list(filePath)
+ return (fileContents?.size?: 0) > 0
+ }
+
+ override fun isCurrentHidden(dirId: Int): Boolean {
+ val ad = dirs[dirId]
+
+ var idx = ad.current
+ if (idx > 0) {
+ idx--
+ }
+
+ if (idx >= ad.files.size) {
+ return false
+ }
+
+ val fileName = ad.files[idx]
+ return fileName.startsWith('.')
+ }
+
+ override fun dirNext(dirId: Int): String {
+ val ad: AssetDir = dirs[dirId]
+
+ if (ad.current >= ad.files.size) {
+ ad.current++
+ return ""
+ }
+
+ return ad.files[ad.current++]
+ }
+
+ override fun dirClose(dirId: Int) {
+ dirs.remove(dirId)
+ }
+
+ override fun getDriveCount() = 0
+
+ override fun getDrive(drive: Int) = ""
+
+ override fun makeDir(dir: String) = false
+
+ override fun getSpaceLeft() = 0L
+
+ override fun rename(from: String, to: String) = false
+
+ override fun remove(filename: String) = false
+}
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
new file mode 100644
index 0000000000..fedcf4843f
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt
@@ -0,0 +1,224 @@
+/*************************************************************************/
+/* DirectoryAccessHandler.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.io.directory
+
+import android.content.Context
+import android.util.Log
+import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_FILESYSTEM
+import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_RESOURCES
+
+/**
+ * Handles files and directories access and manipulation for the Android platform
+ */
+class DirectoryAccessHandler(context: Context) {
+
+ companion object {
+ private val TAG = DirectoryAccessHandler::class.java.simpleName
+
+ internal const val INVALID_DIR_ID = -1
+ internal const val STARTING_DIR_ID = 1
+
+ private fun getAccessTypeFromNative(accessType: Int): AccessType? {
+ return when (accessType) {
+ ACCESS_RESOURCES.nativeValue -> ACCESS_RESOURCES
+ ACCESS_FILESYSTEM.nativeValue -> ACCESS_FILESYSTEM
+ else -> null
+ }
+ }
+ }
+
+ private enum class AccessType(val nativeValue: Int) {
+ ACCESS_RESOURCES(0), ACCESS_FILESYSTEM(2)
+ }
+
+ internal interface DirectoryAccess {
+ fun dirOpen(path: String): Int
+ fun dirNext(dirId: Int): String
+ fun dirClose(dirId: Int)
+ fun dirIsDir(dirId: Int): Boolean
+ fun dirExists(path: String): Boolean
+ fun fileExists(path: String): Boolean
+ fun hasDirId(dirId: Int): Boolean
+ fun isCurrentHidden(dirId: Int): Boolean
+ fun getDriveCount() : Int
+ fun getDrive(drive: Int): String
+ fun makeDir(dir: String): Boolean
+ fun getSpaceLeft(): Long
+ fun rename(from: String, to: String): Boolean
+ fun remove(filename: String): Boolean
+ }
+
+ private val assetsDirAccess = AssetsDirectoryAccess(context)
+ private val fileSystemDirAccess = FilesystemDirectoryAccess(context)
+
+ private fun hasDirId(accessType: AccessType, dirId: Int): Boolean {
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.hasDirId(dirId)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.hasDirId(dirId)
+ }
+ }
+
+ fun dirOpen(nativeAccessType: Int, path: String?): Int {
+ val accessType = getAccessTypeFromNative(nativeAccessType)
+ if (path == null || accessType == null) {
+ return INVALID_DIR_ID
+ }
+
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.dirOpen(path)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.dirOpen(path)
+ }
+ }
+
+ fun dirNext(nativeAccessType: Int, dirId: Int): String {
+ val accessType = getAccessTypeFromNative(nativeAccessType)
+ if (accessType == null || !hasDirId(accessType, dirId)) {
+ Log.w(TAG, "dirNext: Invalid dir id: $dirId")
+ return ""
+ }
+
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.dirNext(dirId)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.dirNext(dirId)
+ }
+ }
+
+ fun dirClose(nativeAccessType: Int, dirId: Int) {
+ val accessType = getAccessTypeFromNative(nativeAccessType)
+ if (accessType == null || !hasDirId(accessType, dirId)) {
+ Log.w(TAG, "dirClose: Invalid dir id: $dirId")
+ return
+ }
+
+ when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.dirClose(dirId)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.dirClose(dirId)
+ }
+ }
+
+ fun dirIsDir(nativeAccessType: Int, dirId: Int): Boolean {
+ val accessType = getAccessTypeFromNative(nativeAccessType)
+ if (accessType == null || !hasDirId(accessType, dirId)) {
+ Log.w(TAG, "dirIsDir: Invalid dir id: $dirId")
+ return false
+ }
+
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.dirIsDir(dirId)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.dirIsDir(dirId)
+ }
+ }
+
+ fun isCurrentHidden(nativeAccessType: Int, dirId: Int): Boolean {
+ val accessType = getAccessTypeFromNative(nativeAccessType)
+ if (accessType == null || !hasDirId(accessType, dirId)) {
+ return false
+ }
+
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.isCurrentHidden(dirId)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.isCurrentHidden(dirId)
+ }
+ }
+
+ fun dirExists(nativeAccessType: Int, path: String?): Boolean {
+ val accessType = getAccessTypeFromNative(nativeAccessType)
+ if (path == null || accessType == null) {
+ return false
+ }
+
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.dirExists(path)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.dirExists(path)
+ }
+ }
+
+ fun fileExists(nativeAccessType: Int, path: String?): Boolean {
+ val accessType = getAccessTypeFromNative(nativeAccessType)
+ if (path == null || accessType == null) {
+ return false
+ }
+
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.fileExists(path)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.fileExists(path)
+ }
+ }
+
+ fun getDriveCount(nativeAccessType: Int): Int {
+ val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0
+ return when(accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.getDriveCount()
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.getDriveCount()
+ }
+ }
+
+ fun getDrive(nativeAccessType: Int, drive: Int): String {
+ val accessType = getAccessTypeFromNative(nativeAccessType) ?: return ""
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.getDrive(drive)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.getDrive(drive)
+ }
+ }
+
+ fun makeDir(nativeAccessType: Int, dir: String): Boolean {
+ val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.makeDir(dir)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.makeDir(dir)
+ }
+ }
+
+ fun getSpaceLeft(nativeAccessType: Int): Long {
+ val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0L
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.getSpaceLeft()
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.getSpaceLeft()
+ }
+ }
+
+ fun rename(nativeAccessType: Int, from: String, to: String): Boolean {
+ val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.rename(from, to)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.rename(from, to)
+ }
+ }
+
+ fun remove(nativeAccessType: Int, filename: String): Boolean {
+ val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.remove(filename)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.remove(filename)
+ }
+ }
+
+}
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
new file mode 100644
index 0000000000..54fc56fa3e
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt
@@ -0,0 +1,231 @@
+/*************************************************************************/
+/* FileSystemDirectoryAccess.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.io.directory
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Build
+import android.os.storage.StorageManager
+import android.util.Log
+import android.util.SparseArray
+import org.godotengine.godot.io.StorageScope
+import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.INVALID_DIR_ID
+import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.STARTING_DIR_ID
+import org.godotengine.godot.io.file.FileAccessHandler
+import java.io.File
+
+/**
+ * Handles directories access with the internal and external filesystem.
+ */
+internal class FilesystemDirectoryAccess(private val context: Context):
+ DirectoryAccessHandler.DirectoryAccess {
+
+ companion object {
+ private val TAG = FilesystemDirectoryAccess::class.java.simpleName
+ }
+
+ 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>()
+
+ private fun inScope(path: String): Boolean {
+ // 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 storageScopeIdentifier.identifyStorageScope(path) != StorageScope.UNKNOWN
+ }
+
+ override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0
+
+ override fun dirOpen(path: String): Int {
+ if (!inScope(path)) {
+ Log.w(TAG, "Path $path is not accessible.")
+ return INVALID_DIR_ID
+ }
+
+ // Check this is a directory.
+ val dirFile = File(path)
+ if (!dirFile.isDirectory) {
+ return INVALID_DIR_ID
+ }
+
+ // Get the files in the directory
+ val files = dirFile.listFiles()?: return INVALID_DIR_ID
+
+ // Create the data representing this directory
+ val dirData = DirData(dirFile, files)
+
+ dirs.put(++lastDirId, dirData)
+ return lastDirId
+ }
+
+ override fun dirExists(path: String): Boolean {
+ if (!inScope(path)) {
+ Log.w(TAG, "Path $path is not accessible.")
+ return false
+ }
+
+ try {
+ return File(path).isDirectory
+ } catch (e: SecurityException) {
+ return false
+ }
+ }
+
+ override fun fileExists(path: String) = FileAccessHandler.fileExists(context, storageScopeIdentifier, path)
+
+ override fun dirNext(dirId: Int): String {
+ val dirData = dirs[dirId]
+ if (dirData.current >= dirData.files.size) {
+ dirData.current++
+ return ""
+ }
+
+ return dirData.files[dirData.current++].name
+ }
+
+ override fun dirClose(dirId: Int) {
+ dirs.remove(dirId)
+ }
+
+ override fun dirIsDir(dirId: Int): Boolean {
+ val dirData = dirs[dirId]
+
+ var index = dirData.current
+ if (index > 0) {
+ index--
+ }
+
+ if (index >= dirData.files.size) {
+ return false
+ }
+
+ return dirData.files[index].isDirectory
+ }
+
+ override fun isCurrentHidden(dirId: Int): Boolean {
+ val dirData = dirs[dirId]
+
+ var index = dirData.current
+ if (index > 0) {
+ index--
+ }
+
+ if (index >= dirData.files.size) {
+ return false
+ }
+
+ return dirData.files[index].isHidden
+ }
+
+ override fun getDriveCount(): Int {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ storageManager.storageVolumes.size
+ } else {
+ 0
+ }
+ }
+
+ override fun getDrive(drive: Int): String {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ return ""
+ }
+
+ if (drive < 0 || drive >= storageManager.storageVolumes.size) {
+ return ""
+ }
+
+ val storageVolume = storageManager.storageVolumes[drive]
+ return storageVolume.getDescription(context)
+ }
+
+ override fun makeDir(dir: String): Boolean {
+ if (!inScope(dir)) {
+ Log.w(TAG, "Directory $dir is not accessible.")
+ return false
+ }
+
+ try {
+ val dirFile = File(dir)
+ return dirFile.isDirectory || dirFile.mkdirs()
+ } catch (e: SecurityException) {
+ return false
+ }
+ }
+
+ @SuppressLint("UsableSpace")
+ override fun getSpaceLeft() = context.getExternalFilesDir(null)?.usableSpace ?: 0L
+
+ override fun rename(from: String, to: String): Boolean {
+ if (!inScope(from) || !inScope(to)) {
+ Log.w(TAG, "Argument filenames are not accessible:\n" +
+ "from: $from\n" +
+ "to: $to")
+ return false
+ }
+
+ return try {
+ val fromFile = File(from)
+ if (fromFile.isDirectory) {
+ fromFile.renameTo(File(to))
+ } else {
+ FileAccessHandler.renameFile(context, storageScopeIdentifier, from, to)
+ }
+ } catch (e: SecurityException) {
+ false
+ }
+ }
+
+ override fun remove(filename: String): Boolean {
+ if (!inScope(filename)) {
+ Log.w(TAG, "Filename $filename is not accessible.")
+ return false
+ }
+
+ return try {
+ val deleteFile = File(filename)
+ if (deleteFile.exists()) {
+ if (deleteFile.isDirectory) {
+ deleteFile.delete()
+ } else {
+ FileAccessHandler.removeFile(context, storageScopeIdentifier, filename)
+ }
+ } else {
+ true
+ }
+ } catch (e: SecurityException) {
+ false
+ }
+ }
+}
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
new file mode 100644
index 0000000000..463dabfb23
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt
@@ -0,0 +1,187 @@
+/*************************************************************************/
+/* DataAccess.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.io.file
+
+import android.content.Context
+import android.os.Build
+import android.util.Log
+import org.godotengine.godot.io.StorageScope
+import java.io.IOException
+import java.nio.ByteBuffer
+import java.nio.channels.FileChannel
+import kotlin.math.max
+
+/**
+ * Base class for file IO operations.
+ *
+ * Its derived instances provide concrete implementations to handle regular file access, as well
+ * as file access through the media store API on versions of Android were scoped storage is enabled.
+ */
+internal abstract class DataAccess(private val filePath: String) {
+
+ companion object {
+ private val TAG = DataAccess::class.java.simpleName
+
+ fun generateDataAccess(
+ storageScope: StorageScope,
+ context: Context,
+ filePath: String,
+ accessFlag: FileAccessFlags
+ ): DataAccess? {
+ return when (storageScope) {
+ StorageScope.APP -> FileData(filePath, accessFlag)
+
+ StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ MediaStoreData(context, filePath, accessFlag)
+ } else {
+ null
+ }
+
+ StorageScope.UNKNOWN -> null
+ }
+ }
+
+ fun fileExists(storageScope: StorageScope, context: Context, path: String): Boolean {
+ return when(storageScope) {
+ StorageScope.APP -> FileData.fileExists(path)
+ StorageScope.SHARED -> MediaStoreData.fileExists(context, path)
+ StorageScope.UNKNOWN -> false
+ }
+ }
+
+ fun fileLastModified(storageScope: StorageScope, context: Context, path: String): Long {
+ return when(storageScope) {
+ StorageScope.APP -> FileData.fileLastModified(path)
+ StorageScope.SHARED -> MediaStoreData.fileLastModified(context, path)
+ StorageScope.UNKNOWN -> 0L
+ }
+ }
+
+ fun removeFile(storageScope: StorageScope, context: Context, path: String): Boolean {
+ return when(storageScope) {
+ StorageScope.APP -> FileData.delete(path)
+ StorageScope.SHARED -> MediaStoreData.delete(context, path)
+ StorageScope.UNKNOWN -> false
+ }
+ }
+
+ fun renameFile(storageScope: StorageScope, context: Context, from: String, to: String): Boolean {
+ return when(storageScope) {
+ StorageScope.APP -> FileData.rename(from, to)
+ StorageScope.SHARED -> MediaStoreData.rename(context, from, to)
+ StorageScope.UNKNOWN -> false
+ }
+ }
+ }
+
+ protected abstract val fileChannel: FileChannel
+ internal var endOfFile = false
+ private set
+
+ fun close() {
+ try {
+ fileChannel.close()
+ } catch (e: IOException) {
+ Log.w(TAG, "Exception when closing file $filePath.", e)
+ }
+ }
+
+ fun flush() {
+ try {
+ fileChannel.force(false)
+ } catch (e: IOException) {
+ Log.w(TAG, "Exception when flushing file $filePath.", e)
+ }
+ }
+
+ fun seek(position: Long) {
+ try {
+ fileChannel.position(position)
+ if (position <= size()) {
+ endOfFile = false
+ }
+ } catch (e: Exception) {
+ Log.w(TAG, "Exception when seeking file $filePath.", e)
+ }
+ }
+
+ fun seekFromEnd(positionFromEnd: Long) {
+ val positionFromBeginning = max(0, size() - positionFromEnd)
+ seek(positionFromBeginning)
+ }
+
+ fun position(): Long {
+ return try {
+ fileChannel.position()
+ } catch (e: IOException) {
+ Log.w(
+ TAG,
+ "Exception when retrieving position for file $filePath.",
+ e
+ )
+ 0L
+ }
+ }
+
+ fun size() = try {
+ fileChannel.size()
+ } catch (e: IOException) {
+ Log.w(TAG, "Exception when retrieving size for file $filePath.", e)
+ 0L
+ }
+
+ fun read(buffer: ByteBuffer): Int {
+ return try {
+ val readBytes = fileChannel.read(buffer)
+ endOfFile = readBytes == -1
+ || (fileChannel.position() >= fileChannel.size() && fileChannel.size() > 0)
+ if (readBytes == -1) {
+ 0
+ } else {
+ readBytes
+ }
+ } catch (e: IOException) {
+ Log.w(TAG, "Exception while reading from file $filePath.", e)
+ 0
+ }
+ }
+
+ fun write(buffer: ByteBuffer) {
+ try {
+ val writtenBytes = fileChannel.write(buffer)
+ if (writtenBytes > 0) {
+ endOfFile = false
+ }
+ } catch (e: IOException) {
+ Log.w(TAG, "Exception while writing to file $filePath.", e)
+ }
+ }
+}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt
new file mode 100644
index 0000000000..c6b242a4b6
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt
@@ -0,0 +1,87 @@
+/*************************************************************************/
+/* FileAccessFlags.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.io.file
+
+/**
+ * Android representation of Godot native access flags.
+ */
+internal enum class FileAccessFlags(val nativeValue: Int) {
+ /**
+ * Opens the file for read operations.
+ * The cursor is positioned at the beginning of the file.
+ */
+ READ(1),
+
+ /**
+ * Opens the file for write operations.
+ * The file is created if it does not exist, and truncated if it does.
+ */
+ WRITE(2),
+
+ /**
+ * Opens the file for read and write operations.
+ * Does not truncate the file. The cursor is positioned at the beginning of the file.
+ */
+ READ_WRITE(3),
+
+ /**
+ * Opens the file for read and write operations.
+ * The file is created if it does not exist, and truncated if it does.
+ * The cursor is positioned at the beginning of the file.
+ */
+ WRITE_READ(7);
+
+ fun getMode(): String {
+ return when (this) {
+ READ -> "r"
+ WRITE -> "w"
+ READ_WRITE, WRITE_READ -> "rw"
+ }
+ }
+
+ fun shouldTruncate(): Boolean {
+ return when (this) {
+ READ, READ_WRITE -> false
+ WRITE, WRITE_READ -> true
+ }
+ }
+
+ companion object {
+ fun fromNativeModeFlags(modeFlag: Int): FileAccessFlags? {
+ for (flag in values()) {
+ if (flag.nativeValue == modeFlag) {
+ return flag
+ }
+ }
+ return null
+ }
+ }
+}
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
new file mode 100644
index 0000000000..04b6772c45
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt
@@ -0,0 +1,203 @@
+/*************************************************************************/
+/* FileAccessHandler.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.io.file
+
+import android.content.Context
+import android.util.Log
+import android.util.SparseArray
+import org.godotengine.godot.io.StorageScope
+import java.io.FileNotFoundException
+import java.nio.ByteBuffer
+
+/**
+ * Handles regular and media store file access and interactions.
+ */
+class FileAccessHandler(val context: Context) {
+
+ companion object {
+ private val TAG = FileAccessHandler::class.java.simpleName
+
+ private const val FILE_NOT_FOUND_ERROR_ID = -1
+ private const val INVALID_FILE_ID = 0
+ private const val STARTING_FILE_ID = 1
+
+ internal fun fileExists(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean {
+ val storageScope = storageScopeIdentifier.identifyStorageScope(path)
+ if (storageScope == StorageScope.UNKNOWN) {
+ return false
+ }
+
+ return try {
+ DataAccess.fileExists(storageScope, context, path!!)
+ } catch (e: SecurityException) {
+ false
+ }
+ }
+
+ internal fun removeFile(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean {
+ val storageScope = storageScopeIdentifier.identifyStorageScope(path)
+ if (storageScope == StorageScope.UNKNOWN) {
+ return false
+ }
+
+ return try {
+ DataAccess.removeFile(storageScope, context, path!!)
+ } catch (e: Exception) {
+ false
+ }
+ }
+
+ internal fun renameFile(context: Context, storageScopeIdentifier: StorageScope.Identifier, from: String?, to: String?): Boolean {
+ val storageScope = storageScopeIdentifier.identifyStorageScope(from)
+ if (storageScope == StorageScope.UNKNOWN) {
+ return false
+ }
+
+ return try {
+ DataAccess.renameFile(storageScope, context, from!!, to!!)
+ } catch (e: Exception) {
+ false
+ }
+ }
+ }
+
+ 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 = storageScopeIdentifier.identifyStorageScope(path)
+ if (storageScope == StorageScope.UNKNOWN) {
+ return INVALID_FILE_ID
+ }
+
+ try {
+ val accessFlag = FileAccessFlags.fromNativeModeFlags(modeFlags) ?: return INVALID_FILE_ID
+ val dataAccess = DataAccess.generateDataAccess(storageScope, context, path!!, accessFlag) ?: return INVALID_FILE_ID
+
+ files.put(++lastFileId, dataAccess)
+ return lastFileId
+ } catch (e: FileNotFoundException) {
+ return FILE_NOT_FOUND_ERROR_ID
+ } catch (e: Exception) {
+ Log.w(TAG, "Error while opening $path", e)
+ return INVALID_FILE_ID
+ }
+ }
+
+ fun fileGetSize(fileId: Int): Long {
+ if (!hasFileId(fileId)) {
+ return 0L
+ }
+
+ return files[fileId].size()
+ }
+
+ fun fileSeek(fileId: Int, position: Long) {
+ if (!hasFileId(fileId)) {
+ return
+ }
+
+ files[fileId].seek(position)
+ }
+
+ fun fileSeekFromEnd(fileId: Int, position: Long) {
+ if (!hasFileId(fileId)) {
+ return
+ }
+
+ files[fileId].seekFromEnd(position)
+ }
+
+ fun fileRead(fileId: Int, byteBuffer: ByteBuffer?): Int {
+ if (!hasFileId(fileId) || byteBuffer == null) {
+ return 0
+ }
+
+ return files[fileId].read(byteBuffer)
+ }
+
+ fun fileWrite(fileId: Int, byteBuffer: ByteBuffer?) {
+ if (!hasFileId(fileId) || byteBuffer == null) {
+ return
+ }
+
+ files[fileId].write(byteBuffer)
+ }
+
+ fun fileFlush(fileId: Int) {
+ if (!hasFileId(fileId)) {
+ return
+ }
+
+ files[fileId].flush()
+ }
+
+ fun fileExists(path: String?) = Companion.fileExists(context, storageScopeIdentifier, path)
+
+ fun fileLastModified(filepath: String?): Long {
+ val storageScope = storageScopeIdentifier.identifyStorageScope(filepath)
+ if (storageScope == StorageScope.UNKNOWN) {
+ return 0L
+ }
+
+ return try {
+ DataAccess.fileLastModified(storageScope, context, filepath!!)
+ } catch (e: SecurityException) {
+ 0L
+ }
+ }
+
+ fun fileGetPosition(fileId: Int): Long {
+ if (!hasFileId(fileId)) {
+ return 0L
+ }
+
+ return files[fileId].position()
+ }
+
+ fun isFileEof(fileId: Int): Boolean {
+ if (!hasFileId(fileId)) {
+ return false
+ }
+
+ return files[fileId].endOfFile
+ }
+
+ fun fileClose(fileId: Int) {
+ if (hasFileId(fileId)) {
+ files[fileId].close()
+ files.remove(fileId)
+ }
+ }
+}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt
new file mode 100644
index 0000000000..5af694ad99
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt
@@ -0,0 +1,93 @@
+/*************************************************************************/
+/* FileData.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.io.file
+
+import java.io.File
+import java.io.FileOutputStream
+import java.io.RandomAccessFile
+import java.nio.channels.FileChannel
+
+/**
+ * Implementation of [DataAccess] which handles regular (not scoped) file access and interactions.
+ */
+internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAccess(filePath) {
+
+ companion object {
+ private val TAG = FileData::class.java.simpleName
+
+ fun fileExists(path: String): Boolean {
+ return try {
+ File(path).isFile
+ } catch (e: SecurityException) {
+ false
+ }
+ }
+
+ fun fileLastModified(filepath: String): Long {
+ return try {
+ File(filepath).lastModified()
+ } catch (e: SecurityException) {
+ 0L
+ }
+ }
+
+ fun delete(filepath: String): Boolean {
+ return try {
+ File(filepath).delete()
+ } catch (e: Exception) {
+ false
+ }
+ }
+
+ fun rename(from: String, to: String): Boolean {
+ return try {
+ val fromFile = File(from)
+ fromFile.renameTo(File(to))
+ } catch (e: Exception) {
+ false
+ }
+ }
+ }
+
+ override val fileChannel: FileChannel
+
+ init {
+ if (accessFlag == FileAccessFlags.WRITE) {
+ fileChannel = FileOutputStream(filePath, !accessFlag.shouldTruncate()).channel
+ } else {
+ fileChannel = RandomAccessFile(filePath, accessFlag.getMode()).channel
+ }
+
+ if (accessFlag.shouldTruncate()) {
+ fileChannel.truncate(0)
+ }
+ }
+}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt
new file mode 100644
index 0000000000..81a7dd1705
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt
@@ -0,0 +1,284 @@
+/*************************************************************************/
+/* MediaStoreData.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.io.file
+
+import android.content.ContentUris
+import android.content.ContentValues
+import android.content.Context
+import android.database.Cursor
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.provider.MediaStore
+import androidx.annotation.RequiresApi
+
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileNotFoundException
+import java.io.FileOutputStream
+import java.nio.channels.FileChannel
+
+/**
+ * Implementation of [DataAccess] which handles access and interactions with file and data
+ * under scoped storage via the MediaStore API.
+ */
+@RequiresApi(Build.VERSION_CODES.Q)
+internal class MediaStoreData(context: Context, filePath: String, accessFlag: FileAccessFlags) :
+ DataAccess(filePath) {
+
+ private data class DataItem(
+ val id: Long,
+ val uri: Uri,
+ val displayName: String,
+ val relativePath: String,
+ val size: Int,
+ val dateModified: Int,
+ val mediaType: Int
+ )
+
+ companion object {
+ private val TAG = MediaStoreData::class.java.simpleName
+
+ private val COLLECTION = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
+
+ private val PROJECTION = arrayOf(
+ MediaStore.Files.FileColumns._ID,
+ MediaStore.Files.FileColumns.DISPLAY_NAME,
+ MediaStore.Files.FileColumns.RELATIVE_PATH,
+ MediaStore.Files.FileColumns.SIZE,
+ MediaStore.Files.FileColumns.DATE_MODIFIED,
+ MediaStore.Files.FileColumns.MEDIA_TYPE,
+ )
+
+ private const val SELECTION_BY_PATH = "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ? " +
+ " AND ${MediaStore.Files.FileColumns.RELATIVE_PATH} = ?"
+
+ private fun getSelectionByPathArguments(path: String): Array<String> {
+ return arrayOf(getMediaStoreDisplayName(path), getMediaStoreRelativePath(path))
+ }
+
+ private const val SELECTION_BY_ID = "${MediaStore.Files.FileColumns._ID} = ? "
+
+ private fun getSelectionByIdArgument(id: Long) = arrayOf(id.toString())
+
+ private fun getMediaStoreDisplayName(path: String) = File(path).name
+
+ private fun getMediaStoreRelativePath(path: String): String {
+ val pathFile = File(path)
+ val environmentDir = Environment.getExternalStorageDirectory()
+ var relativePath = (pathFile.parent?.replace(environmentDir.absolutePath, "") ?: "").trim('/')
+ if (relativePath.isNotBlank()) {
+ relativePath += "/"
+ }
+ return relativePath
+ }
+
+ private fun queryById(context: Context, id: Long): List<DataItem> {
+ val query = context.contentResolver.query(
+ COLLECTION,
+ PROJECTION,
+ SELECTION_BY_ID,
+ getSelectionByIdArgument(id),
+ null
+ )
+ return dataItemFromCursor(query)
+ }
+
+ private fun queryByPath(context: Context, path: String): List<DataItem> {
+ val query = context.contentResolver.query(
+ COLLECTION,
+ PROJECTION,
+ SELECTION_BY_PATH,
+ getSelectionByPathArguments(path),
+ null
+ )
+ return dataItemFromCursor(query)
+ }
+
+ private fun dataItemFromCursor(query: Cursor?): List<DataItem> {
+ query?.use { cursor ->
+ cursor.count
+ if (cursor.count == 0) {
+ return emptyList()
+ }
+ val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
+ val displayNameColumn =
+ cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME)
+ val relativePathColumn =
+ cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.RELATIVE_PATH)
+ val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.SIZE)
+ val dateModifiedColumn =
+ cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED)
+ val mediaTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE)
+
+ val result = ArrayList<DataItem>()
+ while (cursor.moveToNext()) {
+ val id = cursor.getLong(idColumn)
+ result.add(
+ DataItem(
+ id,
+ ContentUris.withAppendedId(COLLECTION, id),
+ cursor.getString(displayNameColumn),
+ cursor.getString(relativePathColumn),
+ cursor.getInt(sizeColumn),
+ cursor.getInt(dateModifiedColumn),
+ cursor.getInt(mediaTypeColumn)
+ )
+ )
+ }
+ return result
+ }
+ return emptyList()
+ }
+
+ private fun addFile(context: Context, path: String): DataItem? {
+ val fileDetails = ContentValues().apply {
+ put(MediaStore.Files.FileColumns._ID, 0)
+ put(MediaStore.Files.FileColumns.DISPLAY_NAME, getMediaStoreDisplayName(path))
+ put(MediaStore.Files.FileColumns.RELATIVE_PATH, getMediaStoreRelativePath(path))
+ }
+
+ context.contentResolver.insert(COLLECTION, fileDetails) ?: return null
+
+ // File was successfully added, let's retrieve its info
+ val infos = queryByPath(context, path)
+ if (infos.isEmpty()) {
+ return null
+ }
+
+ return infos[0]
+ }
+
+ fun delete(context: Context, path: String): Boolean {
+ val itemsToDelete = queryByPath(context, path)
+ if (itemsToDelete.isEmpty()) {
+ return false
+ }
+
+ val resolver = context.contentResolver
+ var itemsDeleted = 0
+ for (item in itemsToDelete) {
+ itemsDeleted += resolver.delete(item.uri, null, null)
+ }
+
+ return itemsDeleted > 0
+ }
+
+ fun fileExists(context: Context, path: String): Boolean {
+ return queryByPath(context, path).isNotEmpty()
+ }
+
+ fun fileLastModified(context: Context, path: String): Long {
+ val result = queryByPath(context, path)
+ if (result.isEmpty()) {
+ return 0L
+ }
+
+ val dataItem = result[0]
+ return dataItem.dateModified.toLong()
+ }
+
+ fun rename(context: Context, from: String, to: String): Boolean {
+ // Ensure the source exists.
+ val sources = queryByPath(context, from)
+ if (sources.isEmpty()) {
+ return false
+ }
+
+ // Take the first source
+ val source = sources[0]
+
+ // Set up the updated values
+ val updatedDetails = ContentValues().apply {
+ put(MediaStore.Files.FileColumns.DISPLAY_NAME, getMediaStoreDisplayName(to))
+ put(MediaStore.Files.FileColumns.RELATIVE_PATH, getMediaStoreRelativePath(to))
+ }
+
+ val updated = context.contentResolver.update(
+ source.uri,
+ updatedDetails,
+ SELECTION_BY_ID,
+ getSelectionByIdArgument(source.id)
+ )
+ return updated > 0
+ }
+ }
+
+ private val id: Long
+ private val uri: Uri
+ override val fileChannel: FileChannel
+
+ init {
+ val contentResolver = context.contentResolver
+ val dataItems = queryByPath(context, filePath)
+
+ val dataItem = when (accessFlag) {
+ FileAccessFlags.READ -> {
+ // The file should already exist
+ if (dataItems.isEmpty()) {
+ throw FileNotFoundException("Unable to access file $filePath")
+ }
+
+ val dataItem = dataItems[0]
+ dataItem
+ }
+
+ FileAccessFlags.WRITE, FileAccessFlags.READ_WRITE, FileAccessFlags.WRITE_READ -> {
+ // Create the file if it doesn't exist
+ val dataItem = if (dataItems.isEmpty()) {
+ addFile(context, filePath)
+ } else {
+ dataItems[0]
+ }
+
+ if (dataItem == null) {
+ throw FileNotFoundException("Unable to access file $filePath")
+ }
+ dataItem
+ }
+ }
+
+ id = dataItem.id
+ uri = dataItem.uri
+
+ val parcelFileDescriptor = contentResolver.openFileDescriptor(uri, accessFlag.getMode())
+ ?: throw IllegalStateException("Unable to access file descriptor")
+ fileChannel = if (accessFlag == FileAccessFlags.READ) {
+ FileInputStream(parcelFileDescriptor.fileDescriptor).channel
+ } else {
+ FileOutputStream(parcelFileDescriptor.fileDescriptor).channel
+ }
+
+ if (accessFlag.shouldTruncate()) {
+ fileChannel.truncate(0)
+ }
+ }
+}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java
new file mode 100644
index 0000000000..2239ddac8e
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java
@@ -0,0 +1,298 @@
+/*************************************************************************/
+/* GodotTTS.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.tts;
+
+import org.godotengine.godot.GodotLib;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.UtteranceProgressListener;
+import android.speech.tts.Voice;
+
+import androidx.annotation.Keep;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Set;
+
+/**
+ * Wrapper for Android Text to Speech API and custom utterance query implementation.
+ * <p>
+ * A [GodotTTS] provides the following features:
+ * <p>
+ * <ul>
+ * <li>Access to the Android Text to Speech API.
+ * <li>Utterance pause / resume functions, unsupported by Android TTS API.
+ * </ul>
+ */
+@Keep
+public class GodotTTS extends UtteranceProgressListener {
+ // Note: These constants must be in sync with DisplayServer::TTSUtteranceEvent enum from "servers/display_server.h".
+ final private static int EVENT_START = 0;
+ final private static int EVENT_END = 1;
+ final private static int EVENT_CANCEL = 2;
+ final private static int EVENT_BOUNDARY = 3;
+
+ final private TextToSpeech synth;
+ final private LinkedList<GodotUtterance> queue;
+ final private Object lock = new Object();
+ private GodotUtterance lastUtterance;
+
+ private boolean speaking;
+ private boolean paused;
+
+ public GodotTTS(Activity p_activity) {
+ synth = new TextToSpeech(p_activity, null);
+ queue = new LinkedList<GodotUtterance>();
+
+ synth.setOnUtteranceProgressListener(this);
+ }
+
+ private void updateTTS() {
+ if (!speaking && queue.size() > 0) {
+ int mode = TextToSpeech.QUEUE_FLUSH;
+ GodotUtterance message = queue.pollFirst();
+
+ Set<Voice> voices = synth.getVoices();
+ for (Voice v : voices) {
+ if (v.getName().equals(message.voice)) {
+ synth.setVoice(v);
+ break;
+ }
+ }
+ synth.setPitch(message.pitch);
+ synth.setSpeechRate(message.rate);
+
+ Bundle params = new Bundle();
+ params.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, message.volume / 100.f);
+
+ lastUtterance = message;
+ lastUtterance.start = 0;
+ lastUtterance.offset = 0;
+ paused = false;
+
+ synth.speak(message.text, mode, params, String.valueOf(message.id));
+ speaking = true;
+ }
+ }
+
+ /**
+ * Called by TTS engine when the TTS service is about to speak the specified range.
+ */
+ @Override
+ public void onRangeStart(String utteranceId, int start, int end, int frame) {
+ synchronized (lock) {
+ if (lastUtterance != null && Integer.parseInt(utteranceId) == lastUtterance.id) {
+ lastUtterance.offset = start;
+ GodotLib.ttsCallback(EVENT_BOUNDARY, lastUtterance.id, start + lastUtterance.start);
+ }
+ }
+ }
+
+ /**
+ * Called by TTS engine when an utterance was canceled in progress.
+ */
+ @Override
+ public void onStop(String utteranceId, boolean interrupted) {
+ synchronized (lock) {
+ if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) {
+ GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0);
+ speaking = false;
+ updateTTS();
+ }
+ }
+ }
+
+ /**
+ * Called by TTS engine when an utterance has begun to be spoken..
+ */
+ @Override
+ public void onStart(String utteranceId) {
+ synchronized (lock) {
+ if (lastUtterance != null && lastUtterance.start == 0 && Integer.parseInt(utteranceId) == lastUtterance.id) {
+ GodotLib.ttsCallback(EVENT_START, lastUtterance.id, 0);
+ }
+ }
+ }
+
+ /**
+ * Called by TTS engine when an utterance was successfully finished.
+ */
+ @Override
+ public void onDone(String utteranceId) {
+ synchronized (lock) {
+ if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) {
+ GodotLib.ttsCallback(EVENT_END, lastUtterance.id, 0);
+ speaking = false;
+ updateTTS();
+ }
+ }
+ }
+
+ /**
+ * Called by TTS engine when an error has occurred during processing.
+ */
+ @Override
+ public void onError(String utteranceId, int errorCode) {
+ synchronized (lock) {
+ if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) {
+ GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0);
+ speaking = false;
+ updateTTS();
+ }
+ }
+ }
+
+ /**
+ * Called by TTS engine when an error has occurred during processing (pre API level 21 version).
+ */
+ @Override
+ public void onError(String utteranceId) {
+ synchronized (lock) {
+ if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) {
+ GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0);
+ speaking = false;
+ updateTTS();
+ }
+ }
+ }
+
+ /**
+ * Adds an utterance to the queue.
+ */
+ public void speak(String text, String voice, int volume, float pitch, float rate, int utterance_id, boolean interrupt) {
+ synchronized (lock) {
+ GodotUtterance message = new GodotUtterance(text, voice, volume, pitch, rate, utterance_id);
+ queue.addLast(message);
+
+ if (isPaused()) {
+ resumeSpeaking();
+ } else {
+ updateTTS();
+ }
+ }
+ }
+
+ /**
+ * Puts the synthesizer into a paused state.
+ */
+ public void pauseSpeaking() {
+ synchronized (lock) {
+ if (!paused) {
+ paused = true;
+ synth.stop();
+ }
+ }
+ }
+
+ /**
+ * Resumes the synthesizer if it was paused.
+ */
+ public void resumeSpeaking() {
+ synchronized (lock) {
+ if (lastUtterance != null && paused) {
+ int mode = TextToSpeech.QUEUE_FLUSH;
+
+ Set<Voice> voices = synth.getVoices();
+ for (Voice v : voices) {
+ if (v.getName().equals(lastUtterance.voice)) {
+ synth.setVoice(v);
+ break;
+ }
+ }
+ synth.setPitch(lastUtterance.pitch);
+ synth.setSpeechRate(lastUtterance.rate);
+
+ Bundle params = new Bundle();
+ params.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, lastUtterance.volume / 100.f);
+
+ lastUtterance.start = lastUtterance.offset;
+ lastUtterance.offset = 0;
+ paused = false;
+
+ synth.speak(lastUtterance.text.substring(lastUtterance.start), mode, params, String.valueOf(lastUtterance.id));
+ speaking = true;
+ } else {
+ paused = false;
+ }
+ }
+ }
+
+ /**
+ * Stops synthesis in progress and removes all utterances from the queue.
+ */
+ public void stopSpeaking() {
+ synchronized (lock) {
+ for (GodotUtterance u : queue) {
+ GodotLib.ttsCallback(EVENT_CANCEL, u.id, 0);
+ }
+ queue.clear();
+
+ if (lastUtterance != null) {
+ GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0);
+ }
+ lastUtterance = null;
+
+ paused = false;
+ speaking = false;
+
+ synth.stop();
+ }
+ }
+
+ /**
+ * Returns voice information.
+ */
+ public String[] getVoices() {
+ Set<Voice> voices = synth.getVoices();
+ String[] list = new String[voices.size()];
+ int i = 0;
+ for (Voice v : voices) {
+ list[i++] = v.getLocale().toString() + ";" + v.getName();
+ }
+ return list;
+ }
+
+ /**
+ * Returns true if the synthesizer is generating speech, or have utterance waiting in the queue.
+ */
+ public boolean isSpeaking() {
+ return speaking;
+ }
+
+ /**
+ * Returns true if the synthesizer is in a paused state.
+ */
+ public boolean isPaused() {
+ return paused;
+ }
+}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotUtterance.java
index 44f0a3eb3e..bde37e7315 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotUtterance.java
@@ -1,5 +1,5 @@
/*************************************************************************/
-/* GodotInstrumentation.java */
+/* GodotUtterance.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
@@ -28,23 +28,28 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
-package org.godotengine.godot;
+package org.godotengine.godot.tts;
-import android.app.Instrumentation;
-import android.content.Intent;
-import android.os.Bundle;
+/**
+ * A speech request for GodotTTS.
+ */
+class GodotUtterance {
+ final String text;
+ final String voice;
+ final int volume;
+ final float pitch;
+ final float rate;
+ final int id;
-public class GodotInstrumentation extends Instrumentation {
- private Intent intent;
+ int offset = -1;
+ int start = 0;
- @Override
- public void onCreate(Bundle arguments) {
- intent = arguments.getParcelable("intent");
- start();
- }
-
- @Override
- public void onStart() {
- startActivitySync(intent);
+ GodotUtterance(String text, String voice, int volume, float pitch, float rate, int id) {
+ this.text = text;
+ this.voice = voice;
+ this.volume = volume;
+ this.pitch = pitch;
+ this.rate = rate;
+ this.id = id;
}
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java b/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java
index 39a57f587a..47df23fe1a 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java
@@ -43,8 +43,8 @@ public class Crypt {
// Create Hex String
StringBuilder hexString = new StringBuilder();
- for (int i = 0; i < messageDigest.length; i++)
- hexString.append(Integer.toHexString(0xFF & messageDigest[i]));
+ for (byte b : messageDigest)
+ hexString.append(Integer.toHexString(0xFF & b));
return hexString.toString();
} catch (Exception e) {
diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java
index e5b4f41153..57db0709f0 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java
@@ -32,10 +32,14 @@ package org.godotengine.godot.utils;
import android.Manifest;
import android.app.Activity;
+import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PermissionInfo;
+import android.net.Uri;
import android.os.Build;
+import android.os.Environment;
+import android.provider.Settings;
import android.util.Log;
import androidx.core.content.ContextCompat;
@@ -53,7 +57,8 @@ public final class PermissionsUtil {
static final int REQUEST_RECORD_AUDIO_PERMISSION = 1;
static final int REQUEST_CAMERA_PERMISSION = 2;
static final int REQUEST_VIBRATE_PERMISSION = 3;
- static final int REQUEST_ALL_PERMISSION_REQ_CODE = 1001;
+ public static final int REQUEST_ALL_PERMISSION_REQ_CODE = 1001;
+ public static final int REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE = 2002;
private PermissionsUtil() {
}
@@ -108,13 +113,26 @@ public final class PermissionsUtil {
if (manifestPermissions.length == 0)
return true;
- List<String> dangerousPermissions = new ArrayList<>();
+ List<String> requestedPermissions = new ArrayList<>();
for (String manifestPermission : manifestPermissions) {
try {
- PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission);
- int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
- if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) != PackageManager.PERMISSION_GRANTED) {
- dangerousPermissions.add(manifestPermission);
+ if (manifestPermission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
+ try {
+ Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
+ intent.setData(Uri.parse(String.format("package:%s", activity.getPackageName())));
+ activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE);
+ } catch (Exception ignored) {
+ Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
+ activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE);
+ }
+ }
+ } else {
+ PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission);
+ int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
+ if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) != PackageManager.PERMISSION_GRANTED) {
+ requestedPermissions.add(manifestPermission);
+ }
}
} catch (PackageManager.NameNotFoundException e) {
// Skip this permission and continue.
@@ -122,13 +140,12 @@ public final class PermissionsUtil {
}
}
- if (dangerousPermissions.isEmpty()) {
+ if (requestedPermissions.isEmpty()) {
// If list is empty, all of dangerous permissions were granted.
return true;
}
- String[] requestedPermissions = dangerousPermissions.toArray(new String[0]);
- activity.requestPermissions(requestedPermissions, REQUEST_ALL_PERMISSION_REQ_CODE);
+ activity.requestPermissions(requestedPermissions.toArray(new String[0]), REQUEST_ALL_PERMISSION_REQ_CODE);
return false;
}
@@ -148,13 +165,19 @@ public final class PermissionsUtil {
if (manifestPermissions.length == 0)
return manifestPermissions;
- List<String> dangerousPermissions = new ArrayList<>();
+ List<String> grantedPermissions = new ArrayList<>();
for (String manifestPermission : manifestPermissions) {
try {
- PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission);
- int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
- if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) == PackageManager.PERMISSION_GRANTED) {
- dangerousPermissions.add(manifestPermission);
+ if (manifestPermission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) {
+ grantedPermissions.add(manifestPermission);
+ }
+ } else {
+ PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission);
+ int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
+ if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) == PackageManager.PERMISSION_GRANTED) {
+ grantedPermissions.add(manifestPermission);
+ }
}
} catch (PackageManager.NameNotFoundException e) {
// Skip this permission and continue.
@@ -162,7 +185,7 @@ public final class PermissionsUtil {
}
}
- return dangerousPermissions.toArray(new String[0]);
+ return grantedPermissions.toArray(new String[0]);
}
/**
@@ -177,7 +200,7 @@ public final class PermissionsUtil {
if (permission.equals(p))
return true;
}
- } catch (PackageManager.NameNotFoundException e) {
+ } catch (PackageManager.NameNotFoundException ignored) {
}
return false;
diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java b/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java
new file mode 100644
index 0000000000..2cc37b627a
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java
@@ -0,0 +1,141 @@
+// clang-format off
+
+/* Third-party library.
+ * Upstream: https://github.com/JakeWharton/ProcessPhoenix
+ * Commit: 12cb27c2cc9c3fc555e97f2db89e571667de82c4
+ */
+
+/*
+ * Copyright (C) 2014 Jake Wharton
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.godotengine.godot.utils;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Process;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+
+/**
+ * Process Phoenix facilitates restarting your application process. This should only be used for
+ * things like fundamental state changes in your debug builds (e.g., changing from staging to
+ * production).
+ * <p>
+ * Trigger process recreation by calling {@link #triggerRebirth} with a {@link Context} instance.
+ */
+public final class ProcessPhoenix extends Activity {
+ private static final String KEY_RESTART_INTENTS = "phoenix_restart_intents";
+ private static final String KEY_MAIN_PROCESS_PID = "phoenix_main_process_pid";
+
+ /**
+ * Call to restart the application process using the {@linkplain Intent#CATEGORY_DEFAULT default}
+ * activity as an intent.
+ * <p>
+ * Behavior of the current process after invoking this method is undefined.
+ */
+ public static void triggerRebirth(Context context) {
+ triggerRebirth(context, getRestartIntent(context));
+ }
+
+ /**
+ * Call to restart the application process using the specified intents.
+ * <p>
+ * Behavior of the current process after invoking this method is undefined.
+ */
+ public static void triggerRebirth(Context context, Intent... nextIntents) {
+ if (nextIntents.length < 1) {
+ throw new IllegalArgumentException("intents cannot be empty");
+ }
+ // create a new task for the first activity.
+ nextIntents[0].addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+
+ Intent intent = new Intent(context, ProcessPhoenix.class);
+ intent.addFlags(FLAG_ACTIVITY_NEW_TASK); // In case we are called with non-Activity context.
+ intent.putParcelableArrayListExtra(KEY_RESTART_INTENTS, new ArrayList<>(Arrays.asList(nextIntents)));
+ intent.putExtra(KEY_MAIN_PROCESS_PID, Process.myPid());
+ context.startActivity(intent);
+ }
+
+ // -- GODOT start --
+ /**
+ * Finish the activity and kill its process
+ */
+ public static void forceQuit(Activity activity) {
+ forceQuit(activity, Process.myPid());
+ }
+
+ /**
+ * Finish the activity and kill its process
+ * @param activity
+ * @param pid
+ */
+ public static void forceQuit(Activity activity, int pid) {
+ Process.killProcess(pid); // Kill original main process
+ activity.finish();
+ Runtime.getRuntime().exit(0); // Kill kill kill!
+ }
+
+ // -- GODOT end --
+
+ private static Intent getRestartIntent(Context context) {
+ String packageName = context.getPackageName();
+ Intent defaultIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
+ if (defaultIntent != null) {
+ return defaultIntent;
+ }
+
+ throw new IllegalStateException("Unable to determine default activity for "
+ + packageName
+ + ". Does an activity specify the DEFAULT category in its intent filter?");
+ }
+
+ @Override protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // -- GODOT start --
+ ArrayList<Intent> intents = getIntent().getParcelableArrayListExtra(KEY_RESTART_INTENTS);
+ startActivities(intents.toArray(new Intent[intents.size()]));
+ forceQuit(this, getIntent().getIntExtra(KEY_MAIN_PROCESS_PID, -1));
+ // -- GODOT end --
+ }
+
+ /**
+ * Checks if the current process is a temporary Phoenix Process.
+ * This can be used to avoid initialisation of unused resources or to prevent running code that
+ * is not multi-process ready.
+ *
+ * @return true if the current process is a temporary Phoenix Process
+ */
+ public static boolean isPhoenixProcess(Context context) {
+ int currentPid = Process.myPid();
+ ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ List<ActivityManager.RunningAppProcessInfo> runningProcesses = manager.getRunningAppProcesses();
+ if (runningProcesses != null) {
+ for (ActivityManager.RunningAppProcessInfo processInfo : runningProcesses) {
+ if (processInfo.pid == currentPid && processInfo.processName.endsWith(":phoenix")) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}