[Scummvm-git-logs] scummvm master -> ddfccef675bf94cb89266d48ae3f175516caf210

lephilousophe noreply at scummvm.org
Sun Jan 26 17:17:12 UTC 2025


This automated email contains information about 9 new commits which have been
pushed to the 'scummvm' repo located at https://github.com/scummvm/scummvm .

Summary:
acae0acf27 ANDROID: Update Android Gradle Plugin
2efc9dd830 ANDROID: Don't use a 0 API level
3c4df9614a ANDROID: Don't call error too early
57c4599dcd ANDROID: Make assets installation depend on config.mk
8ae47f50e2 ANDROID: Store the main activity layout in XML
e08b8203bc ANDROID: Use a SharedPtr to track SAF tree global references
402f31122c ANDROID: Don't track SAFFSNode using JNI global references anymore
b82bfc006b ANDROID: Track time spent in SAF queries
ddfccef675 ANDROID: Add a LED widget and use it to indicate IO activity in SAF


Commit: acae0acf27454e319eedb0695926557d6f1d579c
    https://github.com/scummvm/scummvm/commit/acae0acf27454e319eedb0695926557d6f1d579c
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2025-01-26T18:17:05+01:00

Commit Message:
ANDROID: Update Android Gradle Plugin

Changed paths:
    dists/android/build.gradle


diff --git a/dists/android/build.gradle b/dists/android/build.gradle
index 806301127c9..dcf561ba1c9 100644
--- a/dists/android/build.gradle
+++ b/dists/android/build.gradle
@@ -5,7 +5,7 @@ buildscript {
         mavenCentral()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:8.7.2'
+        classpath 'com.android.tools.build:gradle:8.7.3'
     }
 }
 


Commit: 2efc9dd830fe3e55267955dba50f03b40b5effba
    https://github.com/scummvm/scummvm/commit/2efc9dd830fe3e55267955dba50f03b40b5effba
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2025-01-26T18:17:05+01:00

Commit Message:
ANDROID: Don't use a 0 API level

That doesn't exist, use 1 instead.

Changed paths:
    backends/platform/android/org/scummvm/scummvm/SAFFSTree.java


diff --git a/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java b/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
index 01c657b82e1..fd61198d91b 100644
--- a/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
+++ b/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
@@ -64,7 +64,7 @@ public class SAFFSTree {
 		return _trees.get(name);
 	}
 
-	@RequiresApi(api = 0)
+	@RequiresApi(api = Build.VERSION_CODES.BASE)
 	public static void clearCaches() {
 		if (_trees == null) {
 			return;


Commit: 3c4df9614a3f8b2aa2c4613d1c8a12b148fb40d2
    https://github.com/scummvm/scummvm/commit/3c4df9614a3f8b2aa2c4613d1c8a12b148fb40d2
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2025-01-26T18:17:05+01:00

Commit Message:
ANDROID: Don't call error too early

When the backend is not yet set up, avoid using error which ends up
using exit without any useful error log.

Changed paths:
    backends/fs/android/android-saf-fs.cpp


diff --git a/backends/fs/android/android-saf-fs.cpp b/backends/fs/android/android-saf-fs.cpp
index 7554936be0b..104fad5b96b 100644
--- a/backends/fs/android/android-saf-fs.cpp
+++ b/backends/fs/android/android-saf-fs.cpp
@@ -85,15 +85,20 @@ void AndroidSAFFilesystemNode::initJNI() {
 
 	JNIEnv *env = JNI::getEnv();
 
+	// We can't call error here as the backend is not built yet
 #define FIND_METHOD(prefix, name, signature) do {                           \
     _MID_ ## prefix ## name = env->GetMethodID(cls, #name, signature);      \
-        if (_MID_ ## prefix ## name == 0)                                   \
-            error("Can't find method ID " #name);                           \
+        if (_MID_ ## prefix ## name == 0) {                                 \
+            LOGE("Can't find method ID " #name);                            \
+            abort();                                                        \
+        }                                                                   \
     } while (0)
 #define FIND_FIELD(prefix, name, signature) do {                            \
     _FID_ ## prefix ## name = env->GetFieldID(cls, #name, signature);       \
-        if (_FID_ ## prefix ## name == 0)                                   \
-            error("Can't find field ID " #name);                            \
+        if (_FID_ ## prefix ## name == 0) {                                 \
+            LOGE("Can't find field ID " #name);                             \
+            abort();                                                        \
+        }                                                                   \
     } while (0)
 #define SAFFSNodeSig "Lorg/scummvm/scummvm/SAFFSTree$SAFFSNode;"
 


Commit: 57c4599dcd9e0472d11076d68379b34b943828d6
    https://github.com/scummvm/scummvm/commit/57c4599dcd9e0472d11076d68379b34b943828d6
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2025-01-26T18:17:05+01:00

Commit Message:
ANDROID: Make assets installation depend on config.mk

This makes sure new needed files following a reconfigure are copied.
This does not do the cleanup though.

Changed paths:
    backends/platform/android/android.mk


diff --git a/backends/platform/android/android.mk b/backends/platform/android/android.mk
index 32f30fb5ee8..99184d7120a 100644
--- a/backends/platform/android/android.mk
+++ b/backends/platform/android/android.mk
@@ -45,7 +45,7 @@ $(PATH_BUILD)/src.properties: configure.stamp | $(PATH_BUILD)
 $(PATH_BUILD)/mainAssets/build.gradle: $(PATH_DIST)/mainAssets.gradle | $(PATH_BUILD_ASSETS)/MD5SUMS
 	$(INSTALL) -c -m 644 $< $@
 
-$(PATH_BUILD_ASSETS)/MD5SUMS: $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA) $(DIST_FILES_ENGINEDATA_BIG) $(DIST_FILES_SOUNDFONTS) $(DIST_FILES_NETWORKING) $(DIST_FILES_VKEYBD) $(DIST_FILES_DOCS) $(DIST_FILES_PLATFORM) $(DIST_FILES_SHADERS) | $(PATH_BUILD)
+$(PATH_BUILD_ASSETS)/MD5SUMS: config.mk $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA) $(DIST_FILES_ENGINEDATA_BIG) $(DIST_FILES_SOUNDFONTS) $(DIST_FILES_NETWORKING) $(DIST_FILES_VKEYBD) $(DIST_FILES_DOCS) $(DIST_FILES_PLATFORM) $(DIST_FILES_SHADERS) | $(PATH_BUILD)
 	$(INSTALL) -d $(PATH_BUILD_ASSETS)/assets/
 	$(INSTALL) -c -m 644 $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA) $(DIST_FILES_ENGINEDATA_BIG) $(DIST_FILES_SOUNDFONTS) $(DIST_FILES_NETWORKING) $(DIST_FILES_VKEYBD) $(DIST_FILES_DOCS) $(DIST_FILES_PLATFORM) $(PATH_BUILD_ASSETS)/assets/
 ifneq ($(DIST_FILES_SHADERS),)


Commit: 8ae47f50e2500ae040e72cf353999641c06fa633
    https://github.com/scummvm/scummvm/commit/8ae47f50e2500ae040e72cf353999641c06fa633
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2025-01-26T18:17:05+01:00

Commit Message:
ANDROID: Store the main activity layout in XML

This lightens the code a bit and make it simpler to understand.
In addition, migrate the buttons layout to GridLayout for future
additions.

Changed paths:
  A dists/android/res/layout/scummvm_activity.xml
    backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java


diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
index ddea0574aed..fa1bc4937cc 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
@@ -12,8 +12,6 @@ import android.content.pm.PackageManager;
 import android.content.res.AssetManager;
 import android.content.res.Configuration;
 import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
 import android.graphics.Rect;
 import android.media.AudioManager;
 import android.net.ConnectivityManager;
@@ -39,8 +37,8 @@ import android.view.ViewTreeObserver;
 import android.view.WindowManager;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.FrameLayout;
+import android.widget.GridLayout;
 import android.widget.ImageView;
-import android.widget.LinearLayout;
 import android.widget.Toast;
 
 import androidx.annotation.NonNull;
@@ -116,9 +114,10 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 	FrameLayout _videoLayout = null;
 
 	private EditableSurfaceView _main_surface = null;
-	private LinearLayout _buttonLayout = null;
+	private GridLayout _buttonLayout = null;
 	private ImageView _toggleTouchModeKeyboardBtnIcon = null;
 	private ImageView _openMenuBtnIcon = null;
+	private int _layoutOrientation;
 
 	public View _screenKeyboard = null;
 	static boolean keyboardWithoutTextInputShown = false;
@@ -519,25 +518,29 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 	}
 
 	private void layoutButtonLayout(int orientation, boolean force) {
-		int newOrientation = orientation == Configuration.ORIENTATION_LANDSCAPE ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL;
-
-		if (!force && newOrientation == _buttonLayout.getOrientation()) {
+		if (!force && orientation == _layoutOrientation) {
 			return;
 		}
 
-		_buttonLayout.setOrientation(newOrientation);
-		_buttonLayout.removeAllViews();
-		if (newOrientation == LinearLayout.VERTICAL) {
-			_buttonLayout.addView(_openMenuBtnIcon, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT));
-			_buttonLayout.bringChildToFront(_openMenuBtnIcon);
-			_buttonLayout.addView(_toggleTouchModeKeyboardBtnIcon, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT));
-			_buttonLayout.bringChildToFront(_toggleTouchModeKeyboardBtnIcon);
+		_layoutOrientation = orientation;
+		if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
+			GridLayout.LayoutParams params;
+			params = (GridLayout.LayoutParams)_openMenuBtnIcon.getLayoutParams();
+			params.rowSpec = GridLayout.spec(0);
+			params.columnSpec = GridLayout.spec(1);
+			params = (GridLayout.LayoutParams)_toggleTouchModeKeyboardBtnIcon.getLayoutParams();
+			params.rowSpec = GridLayout.spec(1);
+			params.columnSpec = GridLayout.spec(1);
 		} else {
-			_buttonLayout.addView(_toggleTouchModeKeyboardBtnIcon, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT));
-			_buttonLayout.bringChildToFront(_toggleTouchModeKeyboardBtnIcon);
-			_buttonLayout.addView(_openMenuBtnIcon, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT));
-			_buttonLayout.bringChildToFront(_openMenuBtnIcon);
-		}
+			GridLayout.LayoutParams params;
+			params = (GridLayout.LayoutParams)_openMenuBtnIcon.getLayoutParams();
+			params.rowSpec = GridLayout.spec(0);
+			params.columnSpec = GridLayout.spec(1);
+			params = (GridLayout.LayoutParams)_toggleTouchModeKeyboardBtnIcon.getLayoutParams();
+			params.rowSpec = GridLayout.spec(0);
+			params.columnSpec = GridLayout.spec(0);
+		}
+		_buttonLayout.requestLayout();
 	}
 
 	public void showScreenKeyboard(boolean force) {
@@ -921,37 +924,19 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 
 		safSyncObject = new Object();
 
-		_videoLayout = new FrameLayout(this);
-		_videoLayout.setLayerType(android.view.View.LAYER_TYPE_NONE, null);
 		getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
-		setContentView(_videoLayout);
-		_videoLayout.setFocusable(true);
-		_videoLayout.setFocusableInTouchMode(true);
-		_videoLayout.requestFocus();
-
-		_main_surface = new EditableSurfaceView(this);
-		_main_surface.setLayerType(android.view.View.LAYER_TYPE_NONE, null);
-
-		_videoLayout.addView(_main_surface, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
-
-		_buttonLayout = new LinearLayout(this);
-		FrameLayout.LayoutParams buttonLayoutParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.TOP | Gravity.RIGHT);
-		buttonLayoutParams.topMargin = 5;
-		buttonLayoutParams.rightMargin = 5;
-		_videoLayout.addView(_buttonLayout, buttonLayoutParams);
-		_videoLayout.bringChildToFront(_buttonLayout);
-
-		_openMenuBtnIcon = new ImageView(this);
-		_openMenuBtnIcon.setImageResource(R.drawable.ic_action_menu);
 
-		_toggleTouchModeKeyboardBtnIcon = new ImageView(this);
+		setContentView(R.layout.scummvm_activity);
+		_videoLayout = findViewById(R.id.video_layout);
+		_main_surface = findViewById(R.id.main_surface);
+		_buttonLayout = findViewById(R.id.button_layout);
+		_openMenuBtnIcon = findViewById(R.id.open_menu_button);
+		_toggleTouchModeKeyboardBtnIcon = findViewById(R.id.toggle_touch_button);
 
 		// Hide by default all buttons, they will be shown when native code will start
 		showToggleOnScreenBtnIcons(0);
 		layoutButtonLayout(getResources().getConfiguration().orientation, true);
 
-		_main_surface.setFocusable(true);
-		_main_surface.setFocusableInTouchMode(true);
 		_main_surface.requestFocus();
 
 		//Log.d(ScummVM.LOG_TAG, "onCreate - captureMouse(true)");
diff --git a/dists/android/res/layout/scummvm_activity.xml b/dists/android/res/layout/scummvm_activity.xml
new file mode 100644
index 00000000000..9cb4c23b74a
--- /dev/null
+++ b/dists/android/res/layout/scummvm_activity.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:app="http://schemas.android.com/apk/res-auto"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:id="@+id/video_layout"
+	android:layout_width="match_parent"
+	android:layout_height="match_parent"
+	android:focusable="true"
+	android:focusableInTouchMode="true"
+	android:layerType="none">
+
+	<org.scummvm.scummvm.EditableSurfaceView
+		android:id="@+id/main_surface"
+		android:layout_width="match_parent"
+		android:layout_height="match_parent"
+		android:focusable="true"
+		android:focusableInTouchMode="true"
+		android:layerType="none" />
+
+	<GridLayout
+		android:id="@+id/button_layout"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_gravity="right|top"
+		android:layout_marginTop="5dp"
+		android:layout_marginRight="5dp"
+		android:orientation="horizontal">
+
+		<ImageView
+			android:id="@+id/toggle_touch_button"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_row="0"
+			android:layout_column="0"
+			android:src="@drawable/ic_action_mouse" />
+
+		<ImageView
+			android:id="@+id/open_menu_button"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_row="0"
+			android:layout_column="1"
+			android:src="@drawable/ic_action_menu"
+			android:visibility="gone"
+			tools:visibility="visible" />
+	</GridLayout>
+
+</FrameLayout>


Commit: e08b8203bc5fc70b874d716ff0b4492f5f13e1a0
    https://github.com/scummvm/scummvm/commit/e08b8203bc5fc70b874d716ff0b4492f5f13e1a0
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2025-01-26T18:17:05+01:00

Commit Message:
ANDROID: Use a SharedPtr to track SAF tree global references

This makes us use a single global reference per tree instead of
duplicating them on each filesystem node creation.
Thanks to the SharedPtr, the global reference is automatically
deallocated when needed.
This is the first step to avoid global references overflow.

Changed paths:
    backends/fs/android/android-saf-fs.cpp
    backends/fs/android/android-saf-fs.h


diff --git a/backends/fs/android/android-saf-fs.cpp b/backends/fs/android/android-saf-fs.cpp
index 104fad5b96b..f9d40dc5ed6 100644
--- a/backends/fs/android/android-saf-fs.cpp
+++ b/backends/fs/android/android-saf-fs.cpp
@@ -132,6 +132,11 @@ void AndroidSAFFilesystemNode::initJNI() {
 	_JNIinit = true;
 }
 
+void AndroidSAFFilesystemNode::GlobalRef::Deleter::operator()(_jobject *obj) {
+			JNIEnv *env = JNI::getEnv();
+			env->DeleteGlobalRef((jobject)obj);
+}
+
 AndroidSAFFilesystemNode *AndroidSAFFilesystemNode::makeFromPath(const Common::String &path) {
 	if (!path.hasPrefix(SAF_MOUNT_POINT)) {
 		// Not a SAF mount point
@@ -174,7 +179,7 @@ AndroidSAFFilesystemNode *AndroidSAFFilesystemNode::makeFromPath(const Common::S
 	}
 
 	if (node) {
-		AndroidSAFFilesystemNode *ret = new AndroidSAFFilesystemNode(safTree, node);
+		AndroidSAFFilesystemNode *ret = new AndroidSAFFilesystemNode(GlobalRef(env, safTree), node);
 
 		env->DeleteLocalRef(node);
 		env->DeleteLocalRef(safTree);
@@ -219,7 +224,7 @@ AndroidSAFFilesystemNode *AndroidSAFFilesystemNode::makeFromPath(const Common::S
 	}
 
 	if (node) {
-		AndroidSAFFilesystemNode *parent = new AndroidSAFFilesystemNode(safTree, node);
+		AndroidSAFFilesystemNode *parent = new AndroidSAFFilesystemNode(GlobalRef(env, safTree), node);
 		env->DeleteLocalRef(node);
 		env->DeleteLocalRef(safTree);
 
@@ -244,7 +249,7 @@ AndroidSAFFilesystemNode *AndroidSAFFilesystemNode::makeFromTree(jobject safTree
 		return nullptr;
 	}
 
-	AndroidSAFFilesystemNode *ret = new AndroidSAFFilesystemNode(safTree, node);
+	AndroidSAFFilesystemNode *ret = new AndroidSAFFilesystemNode(GlobalRef(env, safTree), node);
 
 	env->DeleteLocalRef(node);
 	env->DeleteLocalRef(safTree);
@@ -252,27 +257,27 @@ AndroidSAFFilesystemNode *AndroidSAFFilesystemNode::makeFromTree(jobject safTree
 	return ret;
 }
 
-AndroidSAFFilesystemNode::AndroidSAFFilesystemNode(jobject safTree, jobject safNode) :
+AndroidSAFFilesystemNode::AndroidSAFFilesystemNode(const GlobalRef &safTree, jobject safNode) :
 	_flags(0), _safParent(nullptr) {
 
 	JNIEnv *env = JNI::getEnv();
 
-	_safTree = env->NewGlobalRef(safTree);
-	assert(_safTree);
+	_safTree = safTree;
+	assert(_safTree != nullptr);
 	_safNode = env->NewGlobalRef(safNode);
 	assert(_safNode);
 
 	cacheData();
 }
 
-AndroidSAFFilesystemNode::AndroidSAFFilesystemNode(jobject safTree, jobject safParent,
+AndroidSAFFilesystemNode::AndroidSAFFilesystemNode(const GlobalRef &safTree, jobject safParent,
         const Common::String &path, const Common::String &name) :
 	_safNode(nullptr), _flags(0), _safParent(nullptr) {
 
 	JNIEnv *env = JNI::getEnv();
+	_safTree = safTree;
+	assert(_safTree != nullptr);
 
-	_safTree = env->NewGlobalRef(safTree);
-	assert(_safTree);
 	_safParent = env->NewGlobalRef(safParent);
 	assert(_safParent);
 
@@ -287,8 +292,8 @@ AndroidSAFFilesystemNode::AndroidSAFFilesystemNode(const AndroidSAFFilesystemNod
 
 	JNIEnv *env = JNI::getEnv();
 
-	_safTree = env->NewGlobalRef(node._safTree);
-	assert(_safTree);
+	_safTree = node._safTree;
+	assert(_safTree != nullptr);
 
 	if (node._safNode) {
 		_safNode = env->NewGlobalRef(node._safNode);
@@ -308,7 +313,6 @@ AndroidSAFFilesystemNode::AndroidSAFFilesystemNode(const AndroidSAFFilesystemNod
 AndroidSAFFilesystemNode::~AndroidSAFFilesystemNode() {
 	JNIEnv *env = JNI::getEnv();
 
-	env->DeleteGlobalRef(_safTree);
 	env->DeleteGlobalRef(_safNode);
 	env->DeleteGlobalRef(_safParent);
 }
diff --git a/backends/fs/android/android-saf-fs.h b/backends/fs/android/android-saf-fs.h
index 624b69594dd..e3ae99d46e0 100644
--- a/backends/fs/android/android-saf-fs.h
+++ b/backends/fs/android/android-saf-fs.h
@@ -25,6 +25,7 @@
 #include <jni.h>
 
 #include "backends/fs/abstract-fs.h"
+#include "common/ptr.h"
 
 #include "backends/fs/android/android-fs.h"
 
@@ -35,6 +36,35 @@
  */
 class AndroidSAFFilesystemNode final : public AbstractFSNode, public AndroidFSNode {
 protected:
+	/**
+	 * A class managing a global reference.
+	 *
+	 * This handles the reference management and avoids duplicating them in JNI.
+	 */
+	class GlobalRef final : public Common::SharedPtr<_jobject> {
+		struct Deleter {
+			void operator()(_jobject *obj);
+		};
+	public:
+		GlobalRef() : Common::SharedPtr<_jobject>() {}
+		GlobalRef(const GlobalRef &ref) : Common::SharedPtr<_jobject>(ref) {}
+		GlobalRef(JNIEnv *env, jobject jobj) : Common::SharedPtr<_jobject>(jobj ? env->NewGlobalRef(jobj) : nullptr, Deleter()) {
+			// Make sure NewGlobalRef succeeded
+			assert((jobj == nullptr) == (get() == nullptr));
+		}
+		GlobalRef &operator=(const GlobalRef &r) {
+			Common::SharedPtr<_jobject>::reset(r);
+			return *this;
+		}
+
+		operator jobject() {
+			return Common::SharedPtr<_jobject>::get();
+		}
+		operator jobject() const {
+			return Common::SharedPtr<_jobject>::get();
+		}
+	};
+
 	// SAFFSTree
 	static jmethodID _MID_getTreeId;
 	static jmethodID _MID_pathToNode;
@@ -63,7 +93,7 @@ protected:
 	static const int WRITABLE  = 2;
 	static const int READABLE  = 4;
 
-	jobject _safTree;
+	GlobalRef _safTree;
 	// When null, node doesn't exist yet
 	// In this case _path is the parent path, _newName the node name and _safParent the parent SAF object
 	jobject _safNode;
@@ -82,7 +112,7 @@ protected:
 	 * @param safTree SAF root in Java side
 	 * @param safNode SAF node in Java side
 	 */
-	AndroidSAFFilesystemNode(jobject safTree, jobject safNode);
+	AndroidSAFFilesystemNode(const GlobalRef &safTree, jobject safNode);
 
 public:
 	static const char SAF_MOUNT_POINT[];
@@ -151,7 +181,7 @@ protected:
 	 * @param path Parent path
 	 * @param name Item name
 	 */
-	AndroidSAFFilesystemNode(jobject safTree, jobject safParent,
+	AndroidSAFFilesystemNode(const GlobalRef &safTree, jobject safParent,
 	                         const Common::String &path, const Common::String &name);
 
 	void cacheData();


Commit: 402f31122c231cf47b75fad058b6dc23d43dd194
    https://github.com/scummvm/scummvm/commit/402f31122c231cf47b75fad058b6dc23d43dd194
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2025-01-26T18:17:05+01:00

Commit Message:
ANDROID: Don't track SAFFSNode using JNI global references anymore

The JNI global references are limited in count and we overflow it for
games with a huge number of files (like Zork Nemesis).
Instead, track all the nodes from Java side using a unique identifier
generated using an atomic counter.
Then, all JNI calls either use a temporary local reference to access
node data or use the unique identifier.

Changed paths:
    backends/fs/android/android-saf-fs.cpp
    backends/fs/android/android-saf-fs.h
    backends/platform/android/org/scummvm/scummvm/SAFFSTree.java


diff --git a/backends/fs/android/android-saf-fs.cpp b/backends/fs/android/android-saf-fs.cpp
index f9d40dc5ed6..28715d1c90f 100644
--- a/backends/fs/android/android-saf-fs.cpp
+++ b/backends/fs/android/android-saf-fs.cpp
@@ -55,6 +55,12 @@
 #include "common/translation.h"
 #include "common/util.h"
 
+jclass AndroidSAFFilesystemNode::_CLS_SAFFSTree = nullptr;
+
+jmethodID AndroidSAFFilesystemNode::_MID_addNodeRef = 0;
+jmethodID AndroidSAFFilesystemNode::_MID_decNodeRef = 0;
+jmethodID AndroidSAFFilesystemNode::_MID_refToNode = 0;
+
 jmethodID AndroidSAFFilesystemNode::_MID_getTreeId = 0;
 jmethodID AndroidSAFFilesystemNode::_MID_pathToNode = 0;
 jmethodID AndroidSAFFilesystemNode::_MID_getChildren = 0;
@@ -69,6 +75,8 @@ jmethodID AndroidSAFFilesystemNode::_MID_removeTree = 0;
 jfieldID AndroidSAFFilesystemNode::_FID__treeName = 0;
 jfieldID AndroidSAFFilesystemNode::_FID__root = 0;
 
+jmethodID AndroidSAFFilesystemNode::_MID_addRef = 0;
+
 jfieldID AndroidSAFFilesystemNode::_FID__parent = 0;
 jfieldID AndroidSAFFilesystemNode::_FID__path = 0;
 jfieldID AndroidSAFFilesystemNode::_FID__documentId = 0;
@@ -86,48 +94,65 @@ void AndroidSAFFilesystemNode::initJNI() {
 	JNIEnv *env = JNI::getEnv();
 
 	// We can't call error here as the backend is not built yet
-#define FIND_METHOD(prefix, name, signature) do {                           \
-    _MID_ ## prefix ## name = env->GetMethodID(cls, #name, signature);      \
-        if (_MID_ ## prefix ## name == 0) {                                 \
-            LOGE("Can't find method ID " #name);                            \
-            abort();                                                        \
-        }                                                                   \
+#define FIND_STATIC_METHOD(prefix, name, signature) do {                     \
+    _MID_ ## prefix ## name = env->GetStaticMethodID(cls, #name, signature); \
+        if (_MID_ ## prefix ## name == 0) {                                  \
+            LOGE("Can't find method ID " #name);                             \
+            abort();                                                         \
+        }                                                                    \
+    } while (0)
+#define FIND_METHOD(prefix, name, signature) do {                            \
+    _MID_ ## prefix ## name = env->GetMethodID(cls, #name, signature);       \
+        if (_MID_ ## prefix ## name == 0) {                                  \
+            LOGE("Can't find method ID " #name);                             \
+            abort();                                                         \
+        }                                                                    \
     } while (0)
-#define FIND_FIELD(prefix, name, signature) do {                            \
-    _FID_ ## prefix ## name = env->GetFieldID(cls, #name, signature);       \
-        if (_FID_ ## prefix ## name == 0) {                                 \
-            LOGE("Can't find field ID " #name);                             \
-            abort();                                                        \
-        }                                                                   \
+#define FIND_FIELD(prefix, name, signature) do {                             \
+    _FID_ ## prefix ## name = env->GetFieldID(cls, #name, signature);        \
+        if (_FID_ ## prefix ## name == 0) {                                  \
+            LOGE("Can't find field ID " #name);                              \
+            abort();                                                         \
+        }                                                                    \
     } while (0)
 #define SAFFSNodeSig "Lorg/scummvm/scummvm/SAFFSTree$SAFFSNode;"
 
 	jclass cls = env->FindClass("org/scummvm/scummvm/SAFFSTree");
+	_CLS_SAFFSTree = (jclass)env->NewGlobalRef(cls);
+
+	FIND_STATIC_METHOD(, addNodeRef, "(J)V");
+	FIND_STATIC_METHOD(, decNodeRef, "(J)V");
+	FIND_STATIC_METHOD(, refToNode, "(J)" SAFFSNodeSig);
 
 	FIND_METHOD(, getTreeId, "()Ljava/lang/String;");
 	FIND_METHOD(, pathToNode, "(Ljava/lang/String;)" SAFFSNodeSig);
-	FIND_METHOD(, getChildren, "(" SAFFSNodeSig ")[" SAFFSNodeSig);
-	FIND_METHOD(, getChild, "(" SAFFSNodeSig "Ljava/lang/String;)" SAFFSNodeSig);
-	FIND_METHOD(, createDirectory, "(" SAFFSNodeSig "Ljava/lang/String;)" SAFFSNodeSig);
-	FIND_METHOD(, createFile, "(" SAFFSNodeSig "Ljava/lang/String;)" SAFFSNodeSig);
-	FIND_METHOD(, createReadStream, "(" SAFFSNodeSig ")I");
-	FIND_METHOD(, createWriteStream, "(" SAFFSNodeSig ")I");
-	FIND_METHOD(, removeNode, "(" SAFFSNodeSig ")Z");
+	FIND_METHOD(, getChildren, "(J)[" SAFFSNodeSig);
+	FIND_METHOD(, getChild, "(JLjava/lang/String;)" SAFFSNodeSig);
+	FIND_METHOD(, createDirectory, "(JLjava/lang/String;)" SAFFSNodeSig);
+	FIND_METHOD(, createFile, "(JLjava/lang/String;)" SAFFSNodeSig);
+	FIND_METHOD(, createReadStream, "(J)I");
+	FIND_METHOD(, createWriteStream, "(J)I");
+	FIND_METHOD(, removeNode, "(J)Z");
 	FIND_METHOD(, removeTree, "()V");
 
 	FIND_FIELD(, _treeName, "Ljava/lang/String;");
 	FIND_FIELD(, _root, SAFFSNodeSig);
 
+	env->DeleteLocalRef(cls);
 	cls = env->FindClass("org/scummvm/scummvm/SAFFSTree$SAFFSNode");
 
+	FIND_METHOD(, addRef, "()J");
+
 	FIND_FIELD(, _parent, SAFFSNodeSig);
 	FIND_FIELD(, _path, "Ljava/lang/String;");
 	FIND_FIELD(, _documentId, "Ljava/lang/String;");
 	FIND_FIELD(, _flags, "I");
 
+	env->DeleteLocalRef(cls);
 #undef SAFFSNodeSig
 #undef FIND_FIELD
 #undef FIND_METHOD
+#undef FIND_STATIC_METHOD
 
 	_JNIinit = true;
 }
@@ -137,6 +162,104 @@ void AndroidSAFFilesystemNode::GlobalRef::Deleter::operator()(_jobject *obj) {
 			env->DeleteGlobalRef((jobject)obj);
 }
 
+void AndroidSAFFilesystemNode::NodeRef::reset() {
+	if (_ref == 0) {
+		return;
+	}
+
+	JNIEnv *env = JNI::getEnv();
+
+	env->CallStaticVoidMethod(_CLS_SAFFSTree, _MID_decNodeRef, _ref);
+	if (env->ExceptionCheck()) {
+		LOGE("SAFFSTree::decNodeRef failed");
+		env->ExceptionDescribe();
+		env->ExceptionClear();
+	}
+	_ref = 0;
+}
+
+void AndroidSAFFilesystemNode::NodeRef::reset(const NodeRef &r) {
+	if (_ref == 0 && r._ref == 0) {
+		return;
+	}
+
+	JNIEnv *env = JNI::getEnv();
+
+	if (_ref) {
+		env->CallStaticVoidMethod(_CLS_SAFFSTree, _MID_decNodeRef, _ref);
+		if (env->ExceptionCheck()) {
+			LOGE("SAFFSTree::decNodeRef failed");
+			env->ExceptionDescribe();
+			env->ExceptionClear();
+		}
+	}
+
+	_ref = r._ref;
+	if (!_ref) {
+		return;
+	}
+
+	env->CallStaticVoidMethod(_CLS_SAFFSTree, _MID_addNodeRef, _ref);
+	if (env->ExceptionCheck()) {
+		LOGE("SAFFSTree::addNodeRef failed");
+
+		env->ExceptionDescribe();
+		env->ExceptionClear();
+		_ref = 0;
+		abort();
+	}
+}
+
+void AndroidSAFFilesystemNode::NodeRef::reset(JNIEnv *env, jobject node) {
+	if (_ref == 0 && node == nullptr) {
+		return;
+	}
+
+	if (_ref) {
+		env->CallStaticVoidMethod(_CLS_SAFFSTree, _MID_decNodeRef, _ref);
+		if (env->ExceptionCheck()) {
+			LOGE("SAFFSTree::decNodeRef failed");
+			env->ExceptionDescribe();
+			env->ExceptionClear();
+		}
+	}
+
+	if (node == nullptr) {
+		_ref = 0;
+		return;
+	}
+
+	_ref = env->CallLongMethod(node, _MID_addRef);
+	if (env->ExceptionCheck()) {
+		LOGE("SAFFSNode::addRef failed");
+
+		env->ExceptionDescribe();
+		env->ExceptionClear();
+		_ref = 0;
+		abort();
+	}
+
+	assert(_ref != 0);
+}
+
+jobject AndroidSAFFilesystemNode::NodeRef::localRef(JNIEnv *env) const {
+	if (_ref == 0) {
+		return nullptr;
+	}
+
+	jobject localRef = env->CallStaticObjectMethod(_CLS_SAFFSTree, _MID_refToNode, _ref);
+	if (env->ExceptionCheck()) {
+		LOGE("SAFFSTree::refToNode failed");
+
+		env->ExceptionDescribe();
+		env->ExceptionClear();
+
+		return nullptr;
+	}
+
+	return localRef;
+}
+
 AndroidSAFFilesystemNode *AndroidSAFFilesystemNode::makeFromPath(const Common::String &path) {
 	if (!path.hasPrefix(SAF_MOUNT_POINT)) {
 		// Not a SAF mount point
@@ -258,63 +381,40 @@ AndroidSAFFilesystemNode *AndroidSAFFilesystemNode::makeFromTree(jobject safTree
 }
 
 AndroidSAFFilesystemNode::AndroidSAFFilesystemNode(const GlobalRef &safTree, jobject safNode) :
-	_flags(0), _safParent(nullptr) {
+	_flags(0) {
 
 	JNIEnv *env = JNI::getEnv();
 
 	_safTree = safTree;
 	assert(_safTree != nullptr);
-	_safNode = env->NewGlobalRef(safNode);
-	assert(_safNode);
 
-	cacheData();
+	_safNode.reset(env, safNode);
+	cacheData(env, safNode);
 }
 
 AndroidSAFFilesystemNode::AndroidSAFFilesystemNode(const GlobalRef &safTree, jobject safParent,
-        const Common::String &path, const Common::String &name) :
-	_safNode(nullptr), _flags(0), _safParent(nullptr) {
+        const Common::String &path, const Common::String &name) : _flags(0) {
 
 	JNIEnv *env = JNI::getEnv();
-	_safTree = safTree;
-	assert(_safTree != nullptr);
 
-	_safParent = env->NewGlobalRef(safParent);
-	assert(_safParent);
+	_safTree = safTree;
+	_safParent.reset(env, safParent);
 
 	// In this case _path is the parent
 	_path = path;
 	_newName = name;
 }
 
-// We need the custom copy constructor because of the reference
-AndroidSAFFilesystemNode::AndroidSAFFilesystemNode(const AndroidSAFFilesystemNode &node)
-	: AbstractFSNode(), _safNode(nullptr), _safParent(nullptr) {
-
-	JNIEnv *env = JNI::getEnv();
-
-	_safTree = node._safTree;
-	assert(_safTree != nullptr);
-
-	if (node._safNode) {
-		_safNode = env->NewGlobalRef(node._safNode);
-		assert(_safNode);
-	}
-
-	if (node._safParent) {
-		_safParent = env->NewGlobalRef(node._safParent);
-		assert(_safParent);
-	}
-
-	_path = node._path;
-	_flags = node._flags;
-	_newName = node._newName;
-}
+AndroidSAFFilesystemNode::AndroidSAFFilesystemNode(const GlobalRef &safTree,
+		const NodeRef &safParent, const Common::String &path,
+		const Common::String &name) : _flags(0) {
 
-AndroidSAFFilesystemNode::~AndroidSAFFilesystemNode() {
-	JNIEnv *env = JNI::getEnv();
+	_safTree = safTree;
+	_safParent = safParent;
 
-	env->DeleteGlobalRef(_safNode);
-	env->DeleteGlobalRef(_safParent);
+	// In this case _path is the parent
+	_path = path;
+	_newName = name;
 }
 
 Common::String AndroidSAFFilesystemNode::getName() const {
@@ -329,7 +429,7 @@ Common::String AndroidSAFFilesystemNode::getName() const {
 Common::String AndroidSAFFilesystemNode::getPath() const {
 	assert(_safTree != nullptr);
 
-	if (_safNode != nullptr) {
+	if (_safNode) {
 		return _path;
 	}
 
@@ -339,7 +439,7 @@ Common::String AndroidSAFFilesystemNode::getPath() const {
 
 AbstractFSNode *AndroidSAFFilesystemNode::getChild(const Common::String &n) const {
 	assert(_safTree != nullptr);
-	assert(_safNode != nullptr);
+	assert(_safNode);
 
 	// Make sure the string contains no slashes
 	assert(!n.contains('/'));
@@ -348,7 +448,7 @@ AbstractFSNode *AndroidSAFFilesystemNode::getChild(const Common::String &n) cons
 
 	jstring name = env->NewStringUTF(n.c_str());
 
-	jobject child = env->CallObjectMethod(_safTree, _MID_getChild, _safNode, name);
+	jobject child = env->CallObjectMethod(_safTree, _MID_getChild, _safNode.get(), name);
 
 	env->DeleteLocalRef(name);
 
@@ -382,7 +482,7 @@ bool AndroidSAFFilesystemNode::getChildren(AbstractFSList &myList, ListMode mode
 	JNIEnv *env = JNI::getEnv();
 
 	jobjectArray array =
-	    (jobjectArray)env->CallObjectMethod(_safTree, _MID_getChildren, _safNode);
+	    (jobjectArray)env->CallObjectMethod(_safTree, _MID_getChildren, _safNode.get());
 
 	if (env->ExceptionCheck()) {
 		LOGE("SAFFSTree::getChildren failed");
@@ -419,11 +519,17 @@ AbstractFSNode *AndroidSAFFilesystemNode::getParent() const {
 	assert(_safTree != nullptr);
 	// No need to check for _safNode: if node doesn't exist yet parent is its parent
 
-	if (_safParent) {
-		return new AndroidSAFFilesystemNode(_safTree, _safParent);
+	JNIEnv *env = JNI::getEnv();
+	if (!_safParent) {
+		return AndroidFilesystemFactory::instance().makeRootFileNode();
 	}
 
-	return AndroidFilesystemFactory::instance().makeRootFileNode();
+	jobject parent = _safParent.localRef(env);
+	assert(parent);
+
+	AndroidSAFFilesystemNode *ret = new AndroidSAFFilesystemNode(_safTree, parent);
+	env->DeleteLocalRef(parent);
+	return ret;
 }
 
 Common::SeekableReadStream *AndroidSAFFilesystemNode::createReadStream() {
@@ -435,7 +541,7 @@ Common::SeekableReadStream *AndroidSAFFilesystemNode::createReadStream() {
 
 	JNIEnv *env = JNI::getEnv();
 
-	jint fd = env->CallIntMethod(_safTree, _MID_createReadStream, _safNode);
+	jint fd = env->CallIntMethod(_safTree, _MID_createReadStream, _safNode.get());
 
 	if (env->ExceptionCheck()) {
 		LOGE("SAFFSTree::createReadStream failed");
@@ -469,7 +575,7 @@ Common::SeekableWriteStream *AndroidSAFFilesystemNode::createWriteStream(bool at
 		jstring name = env->NewStringUTF(_newName.c_str());
 
 		// TODO: Add atomic support if possible
-		jobject child = env->CallObjectMethod(_safTree, _MID_createFile, _safParent, name);
+		jobject child = env->CallObjectMethod(_safTree, _MID_createFile, _safParent.get(), name);
 
 		env->DeleteLocalRef(name);
 
@@ -486,15 +592,13 @@ Common::SeekableWriteStream *AndroidSAFFilesystemNode::createWriteStream(bool at
 			return nullptr;
 		}
 
-		_safNode = env->NewGlobalRef(child);
-		assert(_safNode);
+		_safNode.reset(env, child);
+		cacheData(env, child);
 
 		env->DeleteLocalRef(child);
-
-		cacheData();
 	}
 
-	jint fd = env->CallIntMethod(_safTree, _MID_createWriteStream, _safNode);
+	jint fd = env->CallIntMethod(_safTree, _MID_createWriteStream, _safNode.get());
 	if (env->ExceptionCheck()) {
 		LOGE("SAFFSTree::createWriteStream failed");
 
@@ -530,7 +634,7 @@ bool AndroidSAFFilesystemNode::createDirectory() {
 
 	jstring name = env->NewStringUTF(_newName.c_str());
 
-	jobject child = env->CallObjectMethod(_safTree, _MID_createDirectory, _safParent, name);
+	jobject child = env->CallObjectMethod(_safTree, _MID_createDirectory, _safParent.get(), name);
 
 	env->DeleteLocalRef(name);
 
@@ -547,12 +651,11 @@ bool AndroidSAFFilesystemNode::createDirectory() {
 		return false;
 	}
 
-	_safNode = env->NewGlobalRef(child);
-	assert(_safNode);
+	_safNode.reset(env, child);
 
-	env->DeleteLocalRef(child);
+	cacheData(env, child);
 
-	cacheData();
+	env->DeleteLocalRef(child);
 
 	return true;
 }
@@ -576,7 +679,7 @@ bool AndroidSAFFilesystemNode::remove() {
 
 	JNIEnv *env = JNI::getEnv();
 
-	bool result = env->CallBooleanMethod(_safTree, _MID_removeNode, _safNode);
+	bool result = env->CallBooleanMethod(_safTree, _MID_removeNode, _safNode.get());
 
 	if (env->ExceptionCheck()) {
 		LOGE("SAFFSTree::removeNode failed");
@@ -591,11 +694,15 @@ bool AndroidSAFFilesystemNode::remove() {
 		return false;
 	}
 
-	env->DeleteGlobalRef(_safNode);
-	_safNode = nullptr;
+	_safNode.reset();
 
 	// Create the parent node to fetch informations needed to make us a non-existent node
-	AndroidSAFFilesystemNode *parent = new AndroidSAFFilesystemNode(_safTree, _safParent);
+
+	jobject jparent = _safParent.localRef(env);
+	if (!jparent)
+		return false;
+
+	AndroidSAFFilesystemNode *parent = new AndroidSAFFilesystemNode(_safTree, jparent);
 
 	size_t pos = _path.findLastOf('/');
 	if (pos == Common::String::npos) {
@@ -611,7 +718,7 @@ bool AndroidSAFFilesystemNode::remove() {
 }
 
 void AndroidSAFFilesystemNode::removeTree() {
-	assert(_safParent == nullptr);
+	assert(!_safParent);
 
 	JNIEnv *env = JNI::getEnv();
 
@@ -625,22 +732,13 @@ void AndroidSAFFilesystemNode::removeTree() {
 	}
 }
 
-void AndroidSAFFilesystemNode::cacheData() {
-	JNIEnv *env = JNI::getEnv();
+void AndroidSAFFilesystemNode::cacheData(JNIEnv *env, jobject node) {
+	_flags = env->GetIntField(node, _FID__flags);
 
-	_flags = env->GetIntField(_safNode, _FID__flags);
+	jobject safParent = env->GetObjectField(node, _FID__parent);
+	_safParent.reset(env, safParent);
 
-	jobject safParent = env->GetObjectField(_safNode, _FID__parent);
-	if (safParent) {
-		if (_safParent) {
-			env->DeleteGlobalRef(_safParent);
-		}
-		_safParent = env->NewGlobalRef(safParent);
-		assert(_safParent);
-		env->DeleteLocalRef(safParent);
-	}
-
-	if (_safParent == nullptr) {
+	if (!_safParent) {
 		jstring nameObj = (jstring)env->GetObjectField(_safTree, _FID__treeName);
 		const char *nameP = env->GetStringUTFChars(nameObj, 0);
 		if (nameP != 0) {
@@ -652,7 +750,7 @@ void AndroidSAFFilesystemNode::cacheData() {
 
 	Common::String workingPath;
 
-	jstring pathObj = (jstring)env->GetObjectField(_safNode, _FID__path);
+	jstring pathObj = (jstring)env->GetObjectField(node, _FID__path);
 	const char *path = env->GetStringUTFChars(pathObj, 0);
 	if (path == nullptr) {
 		env->DeleteLocalRef(pathObj);
diff --git a/backends/fs/android/android-saf-fs.h b/backends/fs/android/android-saf-fs.h
index e3ae99d46e0..765e878cc34 100644
--- a/backends/fs/android/android-saf-fs.h
+++ b/backends/fs/android/android-saf-fs.h
@@ -65,7 +65,53 @@ protected:
 		}
 	};
 
+	/**
+	 * A class managing our SAFFSNode references.
+	 *
+	 * Reference counting is managed by SAFFSNode in Java and this class uses
+	 * RAII to call the reference counting methods at the appropriate time.
+	 */
+	class NodeRef final {
+	private:
+		jlong _ref;
+
+	public:
+		NodeRef() : _ref(0) {}
+		~NodeRef() { reset(); }
+		NodeRef(const NodeRef &r) { reset(r); }
+		NodeRef(JNIEnv *env, jobject node) { reset(env, node); }
+
+		void reset();
+		void reset(const NodeRef &r);
+		void reset(JNIEnv *env, jobject node);
+
+		NodeRef &operator=(const NodeRef &r) {
+			reset(r);
+			return *this;
+		}
+
+		bool operator==(const NodeRef &r) const {
+			return _ref == r._ref;
+		}
+
+		bool operator!=(const NodeRef &r) const {
+			return _ref != r._ref;
+		}
+
+		explicit operator bool() const {
+			return _ref != 0;
+		}
+
+		jlong get() const { return _ref; }
+		jobject localRef(JNIEnv *env) const;
+	};
+
 	// SAFFSTree
+	static jclass    _CLS_SAFFSTree;
+
+	static jmethodID _MID_addNodeRef;
+	static jmethodID _MID_decNodeRef;
+	static jmethodID _MID_refToNode;
 	static jmethodID _MID_getTreeId;
 	static jmethodID _MID_pathToNode;
 	static jmethodID _MID_getChildren;
@@ -81,6 +127,8 @@ protected:
 	static jfieldID _FID__root;
 
 	// SAFFSNode
+	static jmethodID _MID_addRef;
+
 	static jfieldID _FID__parent;
 	static jfieldID _FID__path;
 	static jfieldID _FID__documentId;
@@ -94,26 +142,18 @@ protected:
 	static const int READABLE  = 4;
 
 	GlobalRef _safTree;
-	// When null, node doesn't exist yet
+	// When 0, node doesn't exist yet
 	// In this case _path is the parent path, _newName the node name and _safParent the parent SAF object
-	jobject _safNode;
+	NodeRef _safNode;
 
 	Common::String _path;
 	int _flags;
-	jobject _safParent;
+	NodeRef _safParent;
 
 	// Used when creating a new node
 	// Also used for root node to store its pretty name
 	Common::String _newName;
 
-	/**
-	 * Creates an AndroidSAFFilesystemNode given its tree and its node
-	 *
-	 * @param safTree SAF root in Java side
-	 * @param safNode SAF node in Java side
-	 */
-	AndroidSAFFilesystemNode(const GlobalRef &safTree, jobject safNode);
-
 public:
 	static const char SAF_MOUNT_POINT[];
 
@@ -137,19 +177,7 @@ public:
 	 */
 	static AndroidSAFFilesystemNode *makeFromTree(jobject safTree);
 
-	/**
-	 * Copy constructor.
-	 *
-	 * @note Needed because we keep references
-	 */
-	AndroidSAFFilesystemNode(const AndroidSAFFilesystemNode &node);
-
-	/**
-	 * Destructor.
-	 */
-	~AndroidSAFFilesystemNode() override;
-
-	bool exists() const override { return _safNode != nullptr; }
+	bool exists() const override { return (bool)_safNode; }
 	Common::U32String getDisplayName() const override { return Common::U32String(getName()); }
 	Common::String getName() const override;
 	Common::String getPath() const override;
@@ -173,6 +201,14 @@ public:
 	 */
 	void removeTree();
 protected:
+	/**
+	 * Creates an AndroidSAFFilesystemNode given its tree and its node
+	 *
+	 * @param safTree SAF root in Java side
+	 * @param safNode SAF node in Java side
+	 */
+	AndroidSAFFilesystemNode(const GlobalRef &safTree, jobject safNode);
+
 	/**
 	 * Creates an non-existent AndroidSAFFilesystemNode given its tree, parent node and name
 	 *
@@ -184,7 +220,18 @@ protected:
 	AndroidSAFFilesystemNode(const GlobalRef &safTree, jobject safParent,
 	                         const Common::String &path, const Common::String &name);
 
-	void cacheData();
+	/**
+	 * Creates an non-existent AndroidSAFFilesystemNode given its tree, parent node and name
+	 *
+	 * @param safTree SAF root in Java side
+	 * @param safParent SAF parent node reference in Java side
+	 * @param path Parent path
+	 * @param name Item name
+	 */
+	AndroidSAFFilesystemNode(const GlobalRef &safTree, const NodeRef &safParent,
+	                         const Common::String &path, const Common::String &name);
+
+	void cacheData(JNIEnv *env, jobject node);
 };
 
 class AddSAFFakeNode final : public AbstractFSNode, public AndroidFSNode {
diff --git a/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java b/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
index fd61198d91b..fb49ace9c9e 100644
--- a/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
+++ b/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
@@ -18,6 +18,8 @@ import java.lang.ref.SoftReference;
 import java.util.ArrayDeque;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
 
 /**
  * SAF primitives for C++ FSNode
@@ -26,9 +28,23 @@ import java.util.HashMap;
 public class SAFFSTree {
 	private static HashMap<String, SAFFSTree> _trees;
 
+	// This map will store the references of all our objects used
+	// by the native side.
+	// This avoids overflowing JNI will a pile of global references
+	private static ConcurrentHashMap<Long, SAFFSNode> _nodes;
+	// This atomic variable will generate unique identifiers for our objects
+	private static AtomicLong _idCounter;
+
 	public static void loadSAFTrees(Context context) {
 		final ContentResolver resolver = context.getContentResolver();
 
+		// As this function is called before starting to emit nodes,
+		// we can take the opportunity to setup the reference related stuff here
+		if (_nodes == null) {
+			_nodes = new ConcurrentHashMap<>();
+			_idCounter = new AtomicLong();
+		}
+
 		_trees = new HashMap<>();
 		for (UriPermission permission : resolver.getPersistedUriPermissions()) {
 			final Uri uri = permission.getUri();
@@ -74,6 +90,29 @@ public class SAFFSTree {
 		}
 	}
 
+	public static void addNodeRef(long nodeId) {
+		assert(nodeId != 0);
+		SAFFSNode node = _nodes.get(nodeId);
+		assert(node != null);
+
+		long newId = node.addRef();
+		assert(newId == nodeId);
+	}
+
+	public static void decNodeRef(long nodeId) {
+		assert(nodeId != 0);
+		SAFFSNode node = _nodes.get(nodeId);
+		assert(node != null);
+
+		node.decRef();
+	}
+
+	public static SAFFSNode refToNode(long nodeId) {
+		assert(nodeId != 0);
+		SAFFSNode node = _nodes.get(nodeId);
+		return node;
+	}
+
 	public static class SAFFSNode implements Comparable<SAFFSNode> {
 		public static final int DIRECTORY = 0x01;
 		public static final int WRITABLE  = 0x02;
@@ -88,9 +127,8 @@ public class SAFFSTree {
 
 		private HashMap<String, SoftReference<SAFFSNode>> _children;
 		private boolean _dirty;
-
-		private SAFFSNode() {
-		}
+		private int _refCnt; // Reference counter for the native side
+		private long _id; // Identifier for the native side
 
 		private SAFFSNode reset(SAFFSNode parent, String path, String documentId, int flags) {
 			_parent = parent;
@@ -129,6 +167,29 @@ public class SAFFSTree {
 			}
 			return _path.compareTo(o._path);
 		}
+
+		public synchronized long addRef() {
+			_refCnt += 1;
+			if (_refCnt > 1) {
+				return _id;
+			}
+			assert(_refCnt == 1);
+
+			if (_id == 0) {
+				_id = _idCounter.incrementAndGet();
+			}
+			_nodes.put(_id, this);
+
+			return _id;
+		}
+
+		public synchronized void decRef() {
+			if (_refCnt == 1) {
+				SAFFSNode tmp = _nodes.remove(_id);
+				assert(tmp == this);
+			}
+			_refCnt -= 1;
+		}
 	}
 
 	private Context _context;
@@ -221,6 +282,14 @@ public class SAFFSTree {
 		return results.toArray(new SAFFSNode[0]);
 	}
 
+	// This version is used by the C++ side
+	public SAFFSNode[] getChildren(long nodeId) {
+		SAFFSNode node = _nodes.get(nodeId);
+		assert(node != null);
+
+		return getChildren(node);
+	}
+
 	public Collection<SAFFSNode> fetchChildren(SAFFSNode node) {
 		final ContentResolver resolver = _context.getContentResolver();
 		final Uri searchUri = DocumentsContract.buildChildDocumentsUriUsingTree(_treeUri, node._documentId);
@@ -277,10 +346,6 @@ public class SAFFSTree {
 	}
 
 	public SAFFSNode getChild(SAFFSNode node, String name) {
-		final Uri searchUri = DocumentsContract.buildChildDocumentsUriUsingTree(_treeUri, node._documentId);
-
-		SAFFSNode newnode;
-
 		// This variable is used to hold a strong reference on every children nodes
 		Collection<SAFFSNode> children;
 
@@ -298,7 +363,7 @@ public class SAFFSTree {
 			return null;
 		}
 
-		newnode = ref.get();
+		SAFFSNode newnode = ref.get();
 		if (newnode != null) {
 			return newnode;
 		}
@@ -324,22 +389,62 @@ public class SAFFSTree {
 		return newnode;
 	}
 
+	// This version is used by the C++ side
+	public SAFFSNode getChild(long nodeId, String name) {
+		SAFFSNode node = _nodes.get(nodeId);
+		assert(node != null);
+
+		return getChild(node, name);
+	}
+
 	public SAFFSNode createDirectory(SAFFSNode node, String name) {
 		return createDocument(node, name, DocumentsContract.Document.MIME_TYPE_DIR);
 	}
 
+	// This version is used by the C++ side
+	public SAFFSNode createDirectory(long nodeId, String name) {
+		SAFFSNode node = _nodes.get(nodeId);
+		assert(node != null);
+
+		return createDirectory(node, name);
+	}
+
 	public SAFFSNode createFile(SAFFSNode node, String name) {
 		return createDocument(node, name, "application/octet-stream");
 	}
 
+	// This version is used by the C++ side
+	public SAFFSNode createFile(long nodeId, String name) {
+		SAFFSNode node = _nodes.get(nodeId);
+		assert(node != null);
+
+		return createFile(node, name);
+	}
+
 	public int createReadStream(SAFFSNode node) {
 		return createStream(node, "r");
 	}
 
+	// This version is used by the C++ side
+	public int createReadStream(long nodeId) {
+		SAFFSNode node = _nodes.get(nodeId);
+		assert(node != null);
+
+		return createReadStream(node);
+	}
+
 	public int createWriteStream(SAFFSNode node) {
 		return createStream(node, "wt");
 	}
 
+	// This version is used by the C++ side
+	public int createWriteStream(long nodeId) {
+		SAFFSNode node = _nodes.get(nodeId);
+		assert(node != null);
+
+		return createWriteStream(node);
+	}
+
 	public boolean removeNode(SAFFSNode node) {
 		final ContentResolver resolver = _context.getContentResolver();
 		final Uri uri = DocumentsContract.buildDocumentUriUsingTree(_treeUri, node._documentId);
@@ -372,6 +477,14 @@ public class SAFFSTree {
 		return true;
 	}
 
+	// This version is used by the C++ side
+	public boolean removeNode(long nodeId) {
+		SAFFSNode node = _nodes.get(nodeId);
+		assert(node != null);
+
+		return removeNode(node);
+	}
+
 	public void removeTree() {
 		final ContentResolver resolver = _context.getContentResolver();
 


Commit: b82bfc006be8c4836bf63ad4e1496ec76c9e91f2
    https://github.com/scummvm/scummvm/commit/b82bfc006be8c4836bf63ad4e1496ec76c9e91f2
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2025-01-26T18:17:05+01:00

Commit Message:
ANDROID: Track time spent in SAF queries

This allows to monitor times where we are busy because of I/Os and we
will be able to notify the user.

Changed paths:
    backends/platform/android/org/scummvm/scummvm/SAFFSTree.java


diff --git a/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java b/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
index fb49ace9c9e..5c700a18d16 100644
--- a/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
+++ b/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
@@ -18,7 +18,9 @@ import java.lang.ref.SoftReference;
 import java.util.ArrayDeque;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.atomic.AtomicLong;
 
 /**
@@ -26,6 +28,23 @@ import java.util.concurrent.atomic.AtomicLong;
  */
 @RequiresApi(api = Build.VERSION_CODES.N)
 public class SAFFSTree {
+	@RequiresApi(api = Build.VERSION_CODES.BASE)
+	public interface IOBusyListener {
+		public void onIOBusy(float ratio);
+	}
+
+	private static class IOTime {
+		long start;
+		long end;
+		long duration;
+	}
+	// Declare us as busy when I/O waits took more than 90% in 2 secs
+	private static final long IO_BUSINESS_TIMESPAN = 2000;
+	private static final long IO_BUSINESS_THRESHOLD = 1800;
+
+	private static ConcurrentLinkedQueue<IOTime> _lastIOs;
+	private static IOBusyListener _listener;
+
 	private static HashMap<String, SAFFSTree> _trees;
 
 	// This map will store the references of all our objects used
@@ -35,6 +54,51 @@ public class SAFFSTree {
 	// This atomic variable will generate unique identifiers for our objects
 	private static AtomicLong _idCounter;
 
+	@RequiresApi(api = Build.VERSION_CODES.BASE)
+	public static void setIOBusyListener(IOBusyListener l) {
+		if (_lastIOs == null) {
+			_lastIOs = new ConcurrentLinkedQueue<>();
+		}
+		_listener = l;
+	}
+
+	private static void reportIO(long start, long end) {
+		if (_listener == null) {
+			return;
+		}
+
+		// Register this new query
+		IOTime entry = new IOTime();
+		entry.start = start;
+		entry.end = end;
+		entry.duration = end - start;
+		_lastIOs.add(entry);
+
+		long deadline = end - IO_BUSINESS_TIMESPAN;
+		long duration = 0;
+
+		// Remove outdated entries and compute the time spent in I/Os
+		Iterator<IOTime> it = _lastIOs.iterator();
+		while (it.hasNext()) {
+			entry = it.next();
+			//Log.d(ScummVM.LOG_TAG, "ENTRY <" + Long.toString(entry.start) + " " + Long.toString(entry.end) + " " + Long.toString(entry.duration) + ">");
+			if (entry.end <= deadline) {
+				// entry is too old
+				it.remove();
+			} else if (entry.start < deadline) {
+				// This entry crossed the deadline
+				duration += entry.end - deadline;
+			} else {
+				duration += entry.duration;
+			}
+		}
+		//Log.d(ScummVM.LOG_TAG, "SUM: " + Long.toString(duration) + " DEADLINE WAS: " + Long.toString(deadline));
+
+		if (duration >= IO_BUSINESS_THRESHOLD && _listener != null) {
+			_listener.onIOBusy((float)duration / IO_BUSINESS_TIMESPAN);
+		}
+	}
+
 	public static void loadSAFTrees(Context context) {
 		final ContentResolver resolver = context.getContentResolver();
 
@@ -304,6 +368,9 @@ public class SAFFSTree {
 		HashMap<String, SoftReference<SAFFSNode>> newChildren = new HashMap<>();
 
 		Cursor c = null;
+
+		long startIO = System.currentTimeMillis();
+
 		try {
 			c = resolver.query(searchUri, new String[] { DocumentsContract.Document.COLUMN_DISPLAY_NAME,
 				DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_MIME_TYPE,
@@ -340,6 +407,9 @@ public class SAFFSTree {
 			if (c != null) {
 				c.close();
 			}
+
+			long endIO = System.currentTimeMillis();
+			reportIO(startIO, endIO);
 		}
 
 		return results;
@@ -451,20 +521,31 @@ public class SAFFSTree {
 
 		if ((node._flags & SAFFSNode.REMOVABLE) != 0) {
 			final Uri parentUri = DocumentsContract.buildDocumentUriUsingTree(_treeUri, node._parent._documentId);
+
+			long startIO = System.currentTimeMillis();
+
 			try {
 				if (!DocumentsContract.removeDocument(resolver, uri, parentUri)) {
 					return false;
 				}
 			} catch(FileNotFoundException e) {
 				return false;
+			} finally {
+				long endIO = System.currentTimeMillis();
+				reportIO(startIO, endIO);
 			}
 		} else if ((node._flags & SAFFSNode.DELETABLE) != 0) {
+			long startIO = System.currentTimeMillis();
+
 			try {
 				if (!DocumentsContract.deleteDocument(resolver, uri)) {
 					return false;
 				}
 			} catch(FileNotFoundException e) {
 				return false;
+			} finally {
+				long endIO = System.currentTimeMillis();
+				reportIO(startIO, endIO);
 			}
 		} else {
 			return false;
@@ -513,10 +594,15 @@ public class SAFFSTree {
 			}
 		}
 
+		long startIO = System.currentTimeMillis();
+
 		try {
 			newDocUri = DocumentsContract.createDocument(resolver, parentUri, mimeType, name);
 		} catch(FileNotFoundException e) {
 			return null;
+		} finally {
+			long endIO = System.currentTimeMillis();
+			reportIO(startIO, endIO);
 		}
 		if (newDocUri == null) {
 			return null;
@@ -556,10 +642,16 @@ public class SAFFSTree {
 		final Uri uri = DocumentsContract.buildDocumentUriUsingTree(_treeUri, node._documentId);
 
 		ParcelFileDescriptor pfd;
+
+		long startIO = System.currentTimeMillis();
+
 		try {
 			pfd = resolver.openFileDescriptor(uri, mode);
 		} catch(FileNotFoundException e) {
 			return null;
+		} finally {
+			long endIO = System.currentTimeMillis();
+			reportIO(startIO, endIO);
 		}
 
 		return pfd;
@@ -578,6 +670,9 @@ public class SAFFSTree {
 		final Uri uri = DocumentsContract.buildDocumentUriUsingTree(_treeUri, node._documentId);
 
 		Cursor c = null;
+
+		long startIO = System.currentTimeMillis();
+
 		try {
 			c = resolver.query(uri, new String[] { DocumentsContract.Document.COLUMN_DISPLAY_NAME,
 				DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_FLAGS }, null, null, null);
@@ -601,6 +696,9 @@ public class SAFFSTree {
 				} catch (Exception e) {
 				}
 			}
+
+			long endIO = System.currentTimeMillis();
+			reportIO(startIO, endIO);
 		}
 		// We should never end up here...
 		return null;


Commit: ddfccef675bf94cb89266d48ae3f175516caf210
    https://github.com/scummvm/scummvm/commit/ddfccef675bf94cb89266d48ae3f175516caf210
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2025-01-26T18:17:05+01:00

Commit Message:
ANDROID: Add a LED widget and use it to indicate IO activity in SAF

This will notify the user of IOs by blinking the LED. This indicates
that ScummVM is busy and not hung.

Changed paths:
  A backends/platform/android/org/scummvm/scummvm/LedView.java
  A dists/android/res/values/attrs.xml
    backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
    dists/android/res/layout/scummvm_activity.xml


diff --git a/backends/platform/android/org/scummvm/scummvm/LedView.java b/backends/platform/android/org/scummvm/scummvm/LedView.java
new file mode 100644
index 00000000000..a0be48df4b7
--- /dev/null
+++ b/backends/platform/android/org/scummvm/scummvm/LedView.java
@@ -0,0 +1,168 @@
+package org.scummvm.scummvm;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+public class LedView extends View {
+	public static final int DEFAULT_LED_COLOR = 0xffff0000;
+	private static final int BLINK_TIME = 30; // ms
+
+	private boolean _state;
+	private Runnable _blink;
+	private Paint _painter;
+	private int _radius;
+	private int _centerX;
+	private int _centerY;
+
+	public LedView(Context context) {
+		this(context, true, DEFAULT_LED_COLOR);
+	}
+
+	public LedView(Context context, boolean state) {
+			this(context, state, DEFAULT_LED_COLOR);
+	}
+
+	public LedView(Context context, boolean state, int color) {
+		super(context);
+		_state = state;
+		init(color);
+	}
+
+	public LedView(Context context, @Nullable AttributeSet attrs) {
+		this(context, attrs, 0);
+	}
+
+	public LedView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+		super(context, attrs, defStyleAttr);
+		init(context, attrs, defStyleAttr, 0);
+	}
+
+	@RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP)
+	public LedView(
+		Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+		super(context, attrs, defStyleAttr, defStyleRes);
+		init(context, attrs, defStyleAttr, defStyleRes);
+	}
+
+	private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+		TypedArray a = context.getTheme().obtainStyledAttributes(
+			attrs,
+			R.styleable.LedView,
+			defStyleAttr, defStyleRes);
+
+		try {
+			_state = a.getBoolean(R.styleable.LedView_state, true);
+			int color = a.getColor(R.styleable.LedView_color, DEFAULT_LED_COLOR);
+			init(color);
+		} finally {
+			a.recycle();
+		}
+	}
+
+	private void init(int color) {
+		_painter = new Paint();
+		_painter.setStyle(Paint.Style.FILL);
+		if (isInEditMode()) {
+			_painter.setStrokeWidth(2);
+			_painter.setStyle(_state ? Paint.Style.FILL : Paint.Style.STROKE);
+		}
+		_painter.setColor(color);
+		_painter.setAntiAlias(true);
+	}
+
+	@Override
+	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+		int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
+		int w = resolveSizeAndState(minw, widthMeasureSpec, 0);
+
+		int minh = MeasureSpec.getSize(w) - getPaddingLeft() - getPaddingRight() +
+			getPaddingBottom() + getPaddingTop();
+		int h = resolveSizeAndState(minh, heightMeasureSpec, 0);
+
+		setMeasuredDimension(w, h);
+	}
+
+	@Override
+	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+		super.onSizeChanged(w, h, oldw, oldh);
+
+		int xpad = (getPaddingLeft() + getPaddingRight());
+		int ypad = (getPaddingTop() + getPaddingBottom());
+
+		int ww = w - xpad;
+		int hh = h - ypad;
+
+		_radius = Math.min(ww, hh) / 2 - 2;
+		_centerX = w / 2;
+		_centerY = h / 2;
+	}
+
+	@Override
+	protected void onDraw(@NonNull Canvas canvas) {
+		super.onDraw(canvas);
+
+		if (!_state && !isInEditMode()) {
+			return;
+		}
+
+		canvas.drawCircle(_centerX, _centerY, _radius, _painter);
+	}
+
+	public void on() {
+		setState(true);
+	}
+
+	public void off() {
+		setState(false);
+	}
+
+	public void setState(boolean state) {
+		if (_blink != null) {
+			removeCallbacks(_blink);
+			_blink = null;
+		}
+
+		if (_state == state) {
+			return;
+		}
+		_state = state;
+		invalidate();
+	}
+
+	public void blinkOnce() {
+		if (_blink != null) {
+			return;
+		}
+
+		boolean oldState = _state;
+		_state = !oldState;
+		invalidate();
+
+		_blink = new Runnable() {
+			private boolean _ran;
+
+			@Override
+			public void run() {
+				if (_ran) {
+					_blink = null;
+					return;
+				}
+
+				_ran = true;
+				_state = oldState;
+				invalidate();
+
+				postDelayed(this, BLINK_TIME);
+			}
+		};
+		postDelayed(_blink, BLINK_TIME);
+	}
+}
diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
index fa1bc4937cc..32f44cb8b3d 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
@@ -117,6 +117,7 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 	private GridLayout _buttonLayout = null;
 	private ImageView _toggleTouchModeKeyboardBtnIcon = null;
 	private ImageView _openMenuBtnIcon = null;
+	private LedView _ioLed = null;
 	private int _layoutOrientation;
 
 	public View _screenKeyboard = null;
@@ -531,6 +532,9 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 			params = (GridLayout.LayoutParams)_toggleTouchModeKeyboardBtnIcon.getLayoutParams();
 			params.rowSpec = GridLayout.spec(1);
 			params.columnSpec = GridLayout.spec(1);
+			params = (GridLayout.LayoutParams)_ioLed.getLayoutParams();
+			params.rowSpec = GridLayout.spec(0, 2, GridLayout.TOP);
+			params.columnSpec = GridLayout.spec(0, GridLayout.RIGHT);
 		} else {
 			GridLayout.LayoutParams params;
 			params = (GridLayout.LayoutParams)_openMenuBtnIcon.getLayoutParams();
@@ -539,6 +543,9 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 			params = (GridLayout.LayoutParams)_toggleTouchModeKeyboardBtnIcon.getLayoutParams();
 			params.rowSpec = GridLayout.spec(0);
 			params.columnSpec = GridLayout.spec(0);
+			params = (GridLayout.LayoutParams)_ioLed.getLayoutParams();
+			params.rowSpec = GridLayout.spec(1, GridLayout.TOP);
+			params.columnSpec = GridLayout.spec(0, 2, GridLayout.RIGHT);
 		}
 		_buttonLayout.requestLayout();
 	}
@@ -932,6 +939,7 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 		_buttonLayout = findViewById(R.id.button_layout);
 		_openMenuBtnIcon = findViewById(R.id.open_menu_button);
 		_toggleTouchModeKeyboardBtnIcon = findViewById(R.id.toggle_touch_button);
+		_ioLed = findViewById(R.id.io_led);
 
 		// Hide by default all buttons, they will be shown when native code will start
 		showToggleOnScreenBtnIcons(0);
@@ -1043,6 +1051,18 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 			_main_surface.setOnHoverListener(_mouseHelper);
 		}
 
+		SAFFSTree.setIOBusyListener(new SAFFSTree.IOBusyListener() {
+			@Override
+			public void onIOBusy(float ratio) {
+				runOnUiThread(new Runnable() {
+					@Override
+					public void run() {
+						_ioLed.blinkOnce();
+					}
+				});
+			}
+		});
+
 		_scummvm_thread = new Thread(null, _scummvm, "ScummVM", 8388608); // 8MB
 		_scummvm_thread.start();
 	}
@@ -1159,6 +1179,8 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 
 		super.onDestroy();
 
+		SAFFSTree.setIOBusyListener(null);
+
 		if (isScreenKeyboardShown()) {
 			hideScreenKeyboard();
 		}
diff --git a/dists/android/res/layout/scummvm_activity.xml b/dists/android/res/layout/scummvm_activity.xml
index 9cb4c23b74a..7f1ed905053 100644
--- a/dists/android/res/layout/scummvm_activity.xml
+++ b/dists/android/res/layout/scummvm_activity.xml
@@ -43,6 +43,16 @@
 			android:src="@drawable/ic_action_menu"
 			android:visibility="gone"
 			tools:visibility="visible" />
+
+		<org.scummvm.scummvm.LedView
+			android:id="@+id/io_led"
+			android:layout_width="10dp"
+			android:layout_height="wrap_content"
+			android:layout_row="1"
+			android:layout_column="0"
+			android:layout_columnSpan="2"
+			android:layout_gravity="right|top"
+			app:state="false" />
 	</GridLayout>
 
 </FrameLayout>
diff --git a/dists/android/res/values/attrs.xml b/dists/android/res/values/attrs.xml
new file mode 100644
index 00000000000..5fb1fcb147f
--- /dev/null
+++ b/dists/android/res/values/attrs.xml
@@ -0,0 +1,6 @@
+<resources>
+   <declare-styleable name="LedView">
+	   <attr name="color" format="reference|color" />
+	   <attr name="state" format="boolean" />
+   </declare-styleable>
+</resources>




More information about the Scummvm-git-logs mailing list